Compare commits
5 Commits
0b0bceec7a
...
b3ab6ef097
| Author | SHA1 | Date | |
|---|---|---|---|
| b3ab6ef097 | |||
| ccaac7e157 | |||
| 18156ac9d2 | |||
| dd6aacc598 | |||
| 2695a19502 |
@@ -16,7 +16,10 @@ pub mod from_mcp;
|
||||
#[cfg(feature = "mcp")]
|
||||
pub mod to_mcp;
|
||||
|
||||
pub mod to_openapi;
|
||||
|
||||
pub use from_openapi::{FromOpenAPI, HttpAuthScheme, HttpServiceConfig, OpenAPISpec};
|
||||
pub use to_openapi::to_openapi;
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
pub use from_mcp::FromMCP;
|
||||
|
||||
1052
crates/alknet-http/src/adapters/to_openapi.rs
Normal file
1052
crates/alknet-http/src/adapters/to_openapi.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@
|
||||
//! custom routes + decoy fallback) and drives hyper's HTTP/1.1 or HTTP/2
|
||||
//! connection driver over a single QUIC bidirectional stream. The 5 gateway
|
||||
//! endpoints (`/search`/`/schema`/`/call`/`/batch`/`/subscribe`) are wired in
|
||||
//! from `gateway_routes`; `/openapi.json`, the MCP route, and the WS upgrade
|
||||
//! handler remain placeholder 501 handlers pending their respective tasks.
|
||||
//! from `gateway_routes`; `/openapi.json` serves the `to_openapi` projection
|
||||
//! of the registry.
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
@@ -14,6 +14,7 @@ use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::middleware::from_fn_with_state;
|
||||
use axum::response::IntoResponse;
|
||||
@@ -35,6 +36,7 @@ use super::gateway_routes;
|
||||
use super::healthz::healthz;
|
||||
#[cfg(feature = "mcp")]
|
||||
use crate::adapters::to_mcp_service;
|
||||
use crate::adapters::to_openapi;
|
||||
#[cfg(feature = "mcp")]
|
||||
use crate::gateway::GatewayDispatch;
|
||||
use crate::websocket::upgrade::ws_upgrade_handler;
|
||||
@@ -183,7 +185,7 @@ fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
|
||||
|
||||
let default: Router<RouterState> = Router::new()
|
||||
.merge(gateway_routes::gateway_router())
|
||||
.route("/openapi.json", get(not_implemented))
|
||||
.route("/openapi.json", get(openapi_json_handler))
|
||||
.route(WS_UPGRADE_PATH, get(ws_upgrade_handler))
|
||||
.route_layer(from_fn_with_state(
|
||||
auth_state.clone(),
|
||||
@@ -204,8 +206,16 @@ fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
|
||||
with_extras.with_state(state)
|
||||
}
|
||||
|
||||
async fn not_implemented() -> impl IntoResponse {
|
||||
(StatusCode::NOT_IMPLEMENTED, "501 Not Implemented")
|
||||
async fn openapi_json_handler(State(registry): State<Arc<OperationRegistry>>) -> impl IntoResponse {
|
||||
let spec = to_openapi(®istry);
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
axum::http::HeaderValue::from_static("application/json"),
|
||||
)],
|
||||
axum::Json(spec.raw),
|
||||
)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -684,4 +694,22 @@ mod tests {
|
||||
);
|
||||
assert!(response.contains("location: https://example.com"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn openapi_json_route_serves_gateway_spec() {
|
||||
let adapter = HttpAdapter::new(provider(), empty_registry());
|
||||
let request = b"GET /openapi.json HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
let response = serve_and_read(adapter, request).await;
|
||||
assert!(
|
||||
response.starts_with("HTTP/1.1 200"),
|
||||
"expected 200 for /openapi.json, got: {response}"
|
||||
);
|
||||
assert!(response.contains("\"openapi\""));
|
||||
assert!(response.contains("\"/search\""));
|
||||
assert!(response.contains("\"/schema\""));
|
||||
assert!(response.contains("\"/call\""));
|
||||
assert!(response.contains("\"/batch\""));
|
||||
assert!(response.contains("\"/subscribe\""));
|
||||
assert!(response.contains("\"1.0.0\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +315,10 @@ mod tests {
|
||||
let ctx = hub_root_context(
|
||||
"hub-acl-ok",
|
||||
&["ui/dragged"],
|
||||
Some(CompositionAuthority::new("hub", vec!["ui:write".to_string()])),
|
||||
Some(CompositionAuthority::new(
|
||||
"hub",
|
||||
vec!["ui:write".to_string()],
|
||||
)),
|
||||
env.clone(),
|
||||
);
|
||||
|
||||
@@ -342,7 +345,10 @@ mod tests {
|
||||
let ctx = hub_root_context(
|
||||
"hub-acl-deny",
|
||||
&["ui/dragged"],
|
||||
Some(CompositionAuthority::new("hub", vec!["ui:read".to_string()])),
|
||||
Some(CompositionAuthority::new(
|
||||
"hub",
|
||||
vec!["ui:read".to_string()],
|
||||
)),
|
||||
env.clone(),
|
||||
);
|
||||
|
||||
@@ -499,10 +505,12 @@ mod tests {
|
||||
assert!(conn.pending().lock().contains("ws-sub-root"));
|
||||
assert!(conn.pending().lock().contains("ws-sub-child"));
|
||||
|
||||
let failed = conn
|
||||
.pending()
|
||||
.lock()
|
||||
.fail_all(alknet_call::protocol::wire::CallError::internal("connection closed"));
|
||||
let failed =
|
||||
conn.pending()
|
||||
.lock()
|
||||
.fail_all(alknet_call::protocol::wire::CallError::internal(
|
||||
"connection closed",
|
||||
));
|
||||
assert!(failed.contains(&"ws-sub-root".to_string()));
|
||||
assert!(failed.contains(&"ws-sub-child".to_string()));
|
||||
assert!(conn.pending().lock().is_empty());
|
||||
@@ -526,10 +534,12 @@ mod tests {
|
||||
)
|
||||
};
|
||||
|
||||
let failed = conn
|
||||
.pending()
|
||||
.lock()
|
||||
.fail_all(alknet_call::protocol::wire::CallError::internal("connection closed"));
|
||||
let failed =
|
||||
conn.pending()
|
||||
.lock()
|
||||
.fail_all(alknet_call::protocol::wire::CallError::internal(
|
||||
"connection closed",
|
||||
));
|
||||
assert!(failed.contains(&"hub-call-inflight".to_string()));
|
||||
|
||||
let result = tokio::time::timeout(Duration::from_millis(100), rx).await;
|
||||
@@ -566,7 +576,10 @@ mod tests {
|
||||
.await;
|
||||
let envelope: EventEnvelope = response.into();
|
||||
assert_eq!(envelope.r#type, EVENT_RESPONDED);
|
||||
assert_eq!(envelope.payload.get("output"), Some(&serde_json::json!({ "v": 9 })));
|
||||
assert_eq!(
|
||||
envelope.payload.get("output"),
|
||||
Some(&serde_json::json!({ "v": 9 }))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -667,10 +680,10 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_identity_resolved_at_upgrade_is_stored_on_connection() {
|
||||
let provider = Arc::new(
|
||||
StaticIdentityProvider::new()
|
||||
.with_token("browser-token", identity_with_scopes("browser-user", &["ui:read"])),
|
||||
);
|
||||
let provider = Arc::new(StaticIdentityProvider::new().with_token(
|
||||
"browser-token",
|
||||
identity_with_scopes("browser-user", &["ui:read"]),
|
||||
));
|
||||
let registry = echo_registry();
|
||||
let dp = dispatcher(registry, Arc::clone(&provider) as Arc<dyn IdentityProvider>);
|
||||
|
||||
@@ -693,4 +706,4 @@ mod tests {
|
||||
let peer_ids = composed_env.peer_ids();
|
||||
assert_eq!(peer_ids, vec!["browser-user".to_string()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: http/adapters/to-openapi
|
||||
name: Implement to_openapi gateway projection (5-endpoint OpenAPI doc, info.version semver, ADR-042/045)
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [http/server/gateway-endpoints, http/gateway/gateway-dispatch-spine]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
@@ -185,4 +185,11 @@ out of scope.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
> Implemented to_openapi(registry: &OperationRegistry) -> OpenAPISpec in src/adapters/
|
||||
> to_openapi.rs — pure projection generating fixed 5-endpoint gateway doc (/search,
|
||||
> /schema, /call, /batch, /subscribe) with info.version = 1.0.0 (ADR-045). /call responses
|
||||
> carry protocol-level errors (400/401/403/404/500/504) + operation-level errors from
|
||||
> registry error_schemas mapped by http_status (ADR-023). Per-caller operation surface
|
||||
> NOT preloaded (discovered via /search, ADR-042). /subscribe response is text/event-stream.
|
||||
> Wired GET /openapi.json in adapter.rs replacing placeholder 501. 16 new tests. 230
|
||||
> total tests pass. Clippy clean. Formatting fixed during merge.
|
||||
Reference in New Issue
Block a user