5 Commits

Author SHA1 Message Date
b3ab6ef097 docs(http): mark http/adapters/to-openapi completed + fix formatting
to_openapi gateway projection merged: 5-endpoint OpenAPI doc (ADR-042/045), pure
projection, info.version 1.0.0, error fidelity (ADR-023). 230 tests pass. Clippy clean.
2026-07-01 23:37:35 +00:00
ccaac7e157 Merge feat/http-to-openapi: to_openapi gateway projection (5-endpoint OpenAPI doc, ADR-042/045)
Implements to_openapi(registry) -> 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 + operation-level errors 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. 16 new tests.
2026-07-01 23:36:48 +00:00
18156ac9d2 Merge origin/develop: integrate connection-overlay (resolve adapter.rs test conflict, keep /openapi.json route test) 2026-07-01 20:17:06 +00:00
dd6aacc598 feat(http): complete to_openapi gateway projection with error fidelity and route wiring
Refine to_openapi to project operation-level errors (with http_status)
onto /call and /subscribe responses via oneOf merge with protocol-level
errors, preserving HTTP_<status> prefix codes without collision. Fix
BTreeMap→serde_json::Map for Value::Object compatibility. Wire GET
/openapi.json route test. Apply cargo fmt across the crate.
2026-07-01 20:11:09 +00:00
2695a19502 feat(http): implement to_openapi gateway projection (5-endpoint OpenAPI doc, info.version 1.0.0)
to_openapi(registry) -> OpenAPISpec generates the fixed 5-endpoint
gateway doc (/search, /schema, /call, /batch, /subscribe) — pure
projection (ADR-017 §5), gateway pattern (ADR-042). info.version is
1.0.0 tracking the gateway contract (ADR-045). /call responses carry
protocol-level errors (400/401/403/404/500/504) plus operation-level
errors mapped by http_status (ADR-023). GET /openapi.json wired to
serve the generated spec.
2026-07-01 19:52:57 +00:00
5 changed files with 1126 additions and 23 deletions

View File

@@ -16,7 +16,10 @@ pub mod from_mcp;
#[cfg(feature = "mcp")] #[cfg(feature = "mcp")]
pub mod to_mcp; pub mod to_mcp;
pub mod to_openapi;
pub use from_openapi::{FromOpenAPI, HttpAuthScheme, HttpServiceConfig, OpenAPISpec}; pub use from_openapi::{FromOpenAPI, HttpAuthScheme, HttpServiceConfig, OpenAPISpec};
pub use to_openapi::to_openapi;
#[cfg(feature = "mcp")] #[cfg(feature = "mcp")]
pub use from_mcp::FromMCP; pub use from_mcp::FromMCP;

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,8 @@
//! custom routes + decoy fallback) and drives hyper's HTTP/1.1 or HTTP/2 //! 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 //! connection driver over a single QUIC bidirectional stream. The 5 gateway
//! endpoints (`/search`/`/schema`/`/call`/`/batch`/`/subscribe`) are wired in //! endpoints (`/search`/`/schema`/`/call`/`/batch`/`/subscribe`) are wired in
//! from `gateway_routes`; `/openapi.json`, the MCP route, and the WS upgrade //! from `gateway_routes`; `/openapi.json` serves the `to_openapi` projection
//! handler remain placeholder 501 handlers pending their respective tasks. //! of the registry.
use std::io; use std::io;
use std::path::PathBuf; use std::path::PathBuf;
@@ -14,6 +14,7 @@ use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use axum::extract::State;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::middleware::from_fn_with_state; use axum::middleware::from_fn_with_state;
use axum::response::IntoResponse; use axum::response::IntoResponse;
@@ -35,6 +36,7 @@ use super::gateway_routes;
use super::healthz::healthz; use super::healthz::healthz;
#[cfg(feature = "mcp")] #[cfg(feature = "mcp")]
use crate::adapters::to_mcp_service; use crate::adapters::to_mcp_service;
use crate::adapters::to_openapi;
#[cfg(feature = "mcp")] #[cfg(feature = "mcp")]
use crate::gateway::GatewayDispatch; use crate::gateway::GatewayDispatch;
use crate::websocket::upgrade::ws_upgrade_handler; 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() let default: Router<RouterState> = Router::new()
.merge(gateway_routes::gateway_router()) .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(WS_UPGRADE_PATH, get(ws_upgrade_handler))
.route_layer(from_fn_with_state( .route_layer(from_fn_with_state(
auth_state.clone(), auth_state.clone(),
@@ -204,8 +206,16 @@ fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
with_extras.with_state(state) with_extras.with_state(state)
} }
async fn not_implemented() -> impl IntoResponse { async fn openapi_json_handler(State(registry): State<Arc<OperationRegistry>>) -> impl IntoResponse {
(StatusCode::NOT_IMPLEMENTED, "501 Not Implemented") let spec = to_openapi(&registry);
(
StatusCode::OK,
[(
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static("application/json"),
)],
axum::Json(spec.raw),
)
} }
#[async_trait] #[async_trait]
@@ -684,4 +694,22 @@ mod tests {
); );
assert!(response.contains("location: https://example.com")); 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\""));
}
} }

View File

@@ -315,7 +315,10 @@ mod tests {
let ctx = hub_root_context( let ctx = hub_root_context(
"hub-acl-ok", "hub-acl-ok",
&["ui/dragged"], &["ui/dragged"],
Some(CompositionAuthority::new("hub", vec!["ui:write".to_string()])), Some(CompositionAuthority::new(
"hub",
vec!["ui:write".to_string()],
)),
env.clone(), env.clone(),
); );
@@ -342,7 +345,10 @@ mod tests {
let ctx = hub_root_context( let ctx = hub_root_context(
"hub-acl-deny", "hub-acl-deny",
&["ui/dragged"], &["ui/dragged"],
Some(CompositionAuthority::new("hub", vec!["ui:read".to_string()])), Some(CompositionAuthority::new(
"hub",
vec!["ui:read".to_string()],
)),
env.clone(), env.clone(),
); );
@@ -499,10 +505,12 @@ mod tests {
assert!(conn.pending().lock().contains("ws-sub-root")); assert!(conn.pending().lock().contains("ws-sub-root"));
assert!(conn.pending().lock().contains("ws-sub-child")); assert!(conn.pending().lock().contains("ws-sub-child"));
let failed = conn let failed =
.pending() conn.pending()
.lock() .lock()
.fail_all(alknet_call::protocol::wire::CallError::internal("connection closed")); .fail_all(alknet_call::protocol::wire::CallError::internal(
"connection closed",
));
assert!(failed.contains(&"ws-sub-root".to_string())); assert!(failed.contains(&"ws-sub-root".to_string()));
assert!(failed.contains(&"ws-sub-child".to_string())); assert!(failed.contains(&"ws-sub-child".to_string()));
assert!(conn.pending().lock().is_empty()); assert!(conn.pending().lock().is_empty());
@@ -526,10 +534,12 @@ mod tests {
) )
}; };
let failed = conn let failed =
.pending() conn.pending()
.lock() .lock()
.fail_all(alknet_call::protocol::wire::CallError::internal("connection closed")); .fail_all(alknet_call::protocol::wire::CallError::internal(
"connection closed",
));
assert!(failed.contains(&"hub-call-inflight".to_string())); assert!(failed.contains(&"hub-call-inflight".to_string()));
let result = tokio::time::timeout(Duration::from_millis(100), rx).await; let result = tokio::time::timeout(Duration::from_millis(100), rx).await;
@@ -566,7 +576,10 @@ mod tests {
.await; .await;
let envelope: EventEnvelope = response.into(); let envelope: EventEnvelope = response.into();
assert_eq!(envelope.r#type, EVENT_RESPONDED); 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] #[tokio::test]
@@ -667,10 +680,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn browser_identity_resolved_at_upgrade_is_stored_on_connection() { async fn browser_identity_resolved_at_upgrade_is_stored_on_connection() {
let provider = Arc::new( let provider = Arc::new(StaticIdentityProvider::new().with_token(
StaticIdentityProvider::new() "browser-token",
.with_token("browser-token", identity_with_scopes("browser-user", &["ui:read"])), identity_with_scopes("browser-user", &["ui:read"]),
); ));
let registry = echo_registry(); let registry = echo_registry();
let dp = dispatcher(registry, Arc::clone(&provider) as Arc<dyn IdentityProvider>); let dp = dispatcher(registry, Arc::clone(&provider) as Arc<dyn IdentityProvider>);

View File

@@ -1,7 +1,7 @@
--- ---
id: http/adapters/to-openapi id: http/adapters/to-openapi
name: Implement to_openapi gateway projection (5-endpoint OpenAPI doc, info.version semver, ADR-042/045) 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] depends_on: [http/server/gateway-endpoints, http/gateway/gateway-dispatch-spine]
scope: moderate scope: moderate
risk: medium risk: medium
@@ -185,4 +185,11 @@ out of scope.
## Summary ## 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.