docs(arch): ADR-066 — move from_jsonschema to alknet-http as HTTP-backed single-endpoint adapter

from_jsonschema was in alknet-call as a schema-only placeholder with a
NOT_FOUND handler — broken (an op in the registry needs a real handler)
and in the wrong crate (alknet-call has no HTTP client; a useful
from_jsonschema needs reqwest like from_openapi). ADR-066 moves it to
alknet-http as a real reqwest-backed single-endpoint adapter for
non-standard/non-OpenAPI REST endpoints, functionally similar to
from_openapi but one endpoint at a time. FromJsonSchema provenance
stays in alknet-call (now a handler-bearing leaf).

- New ADR-066 (supersedes ADR-017 §5 from_jsonschema clause + ADR-022
  FromJsonSchema row; both amended with strikethrough + pointer)
- Updated specs: call/client-and-adapters, call/README,
  call/operation-registry, http/http-adapters (new from_jsonschema
  section), http/overview, http/README, arch README + overview
- New task: tasks/http/adapters/from-jsonschema.md (depends on
  from-openapi; includes alknet-call cleanup of the broken placeholder)
- Old task tasks/call/client/from-jsonschema.md marked superseded

taskgraph validate: 116 tasks valid
This commit is contained in:
glm-5.2 committed 2026-07-09 11:42:56 +00:00
1 parent 06ab5db459
commit 97456bc608
13 files changed
+739 -95

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ scope: narrow
risk: low
impact: isolated
level: implementation
superseded_by: [http/adapters/from-jsonschema]
---
## Description
+280
View File
@@ -0,0 +1,280 @@
---
id: http/adapters/from-jsonschema
name: Move from_jsonschema to alknet-http as a real HTTP-backed single-endpoint adapter (ADR-066); remove broken placeholder from alknet-call
status: pending
depends_on: [http/adapters/from-openapi]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
`from_jsonschema` was originally implemented in `alknet-call`
(`crates/alknet-call/src/client/from_jsonschema.rs`) as a schema-only
adapter with a `NOT_FOUND`-returning placeholder handler. That is broken:
an operation in the `OperationRegistry` needs a real handler, and a
placeholder that returns `NOT_FOUND` does not work with how the registry
is supposed to function. It was also in the wrong crate — `alknet-call`
is supposed to stay lean (no HTTP client), but a useful `from_jsonschema`
needs reqwest, exactly like `from_openapi`.
ADR-066 moves `from_jsonschema` to `alknet-http` as a real HTTP-backed
single-endpoint adapter, functionally similar to `from_openapi` but
registering one endpoint at a time instead of parsing a full OpenAPI
document. The use case is non-standard, non-OpenAPI, or basic REST
endpoints that don't have a full OpenAPI document — the caller supplies
the schema directly.
This task implements the move: adds the real adapter in `alknet-http`
and removes the broken placeholder from `alknet-call`. The
`FromJsonSchema` provenance variant stays in `alknet-call`
(`OperationProvenance` in `registry/registration.rs`) — only the adapter
implementation moves.
### The adapter (http-adapters.md §"from_jsonschema", ADR-066)
```rust
pub struct FromJsonSchema {
spec: OperationSpec,
config: HttpServiceConfig,
path_template: String,
method: String,
http_client: Arc<SharedHttpClient>,
}
impl FromJsonSchema {
pub fn new(
spec: OperationSpec,
config: HttpServiceConfig,
path_template: String,
method: String,
http_client: Arc<SharedHttpClient>,
) -> Self;
}
#[async_trait]
impl OperationAdapter for FromJsonSchema {
async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
}
```
The caller supplies:
- An `OperationSpec` (name, op type, input/output JSON Schema,
`error_schemas`, `access_control`, `visibility`) — the caller already
has the JSON Schemas; no parsing needed.
- An `HttpServiceConfig` (base URL, auth scheme, default headers — the
same config type `from_openapi` uses, re-exported from
`crate::adapters::from_openapi`).
- A path template (e.g. `/users/{id}/posts`) and an HTTP method (e.g.
`GET`).
### The import flow
The adapter builds one `HandlerRegistration`:
- `spec` = the caller-supplied `OperationSpec` (no parsing, no
`operationId` normalization — the caller named the op).
- `handler` = a reqwest forwarding handler, **identical in shape to
`from_openapi`'s**. Reuse `from_openapi`'s `build_request`, `forward`,
and `forward_stream` functions (or factor the shared logic if those
are not already reusable as-is — they should be, since the handler
shape is the same; the only thing that differs is where the path
template / method / error status codes come from). See
"Implementation note" below.
- `provenance` = `FromJsonSchema` (leaf, `composition_authority: None`,
`scoped_env: None` — ADR-022).
- `capabilities` = the credentials the forwarding handler needs (same
no-env-vars path as `from_openapi` — injected at registration, read
from `context.capabilities` at call time).
For a `Subscription` op type, register a `StreamingHandler`
(`HandlerKind::Stream`) expecting `text/event-stream`, same as
`from_openapi` (ADR-049). For `Query`/`Mutation`, register
`HandlerKind::Once`.
Returns the single bundle. The caller registers it in the
`OperationRegistry`.
### Implementation note — reuse from_openapi's forwarding logic
`from_openapi` (in `crates/alknet-http/src/adapters/from_openapi.rs`)
already implements the full forwarding handler: `build_request` (path
template substitution, query params, body, auth header injection from
`context.capabilities`), `forward` (the `Once` handler — sends via
`SharedHttpClient`, content-type branching JSON/text/binary, error
mapping), and `forward_stream` (the `Stream` handler — SSE parsing).
These functions are currently free functions in `from_openapi.rs`. They
take `base_url`, `path_template`, `method`, `auth_scheme`,
`default_headers`, `namespace`, `error_status_codes`, `op_type`, `input`,
`context` — exactly the parameters a single-endpoint adapter has. The
cleanest implementation is one of:
1. **Call them directly** if they're `pub(crate)` — `from_jsonschema` is
in the same crate (`alknet-http`), same module tree
(`adapters/`). Make `build_request`, `forward`, `forward_stream` (and
the helpers `value_to_path_segment`, `value_to_query`,
`parse_sse_frames`) `pub(crate)` and call them from
`from_jsonschema.rs`.
2. **Factor a small shared module** (e.g. `adapters/forwarding.rs`) if
you prefer the dependency to be explicit rather than reaching into
`from_openapi`'s module. This is cleaner if you anticipate a third
HTTP-backed adapter, but not required for this task.
Either is fine — pick whichever is less code churn. The handler logic
is identical; do not duplicate it. The point of ADR-066 is that
`from_jsonschema` shares `from_openapi`'s forwarding implementation.
### Error fidelity (ADR-023)
Same rule as `from_openapi`: error codes prefixed `HTTP_<status>` to
avoid collision with protocol-level codes. The `error_schemas` come
from the caller-supplied `OperationSpec` (the caller declares them);
the handler maps non-2xx HTTP responses to the declared
`ErrorDefinition` by status code, same as `from_openapi`. If the
`OperationSpec`'s `error_schemas` is empty, fall back to
`HTTP_<status>` (same fallback `from_openapi` uses).
### No-env-vars invariant (ADR-014)
The forwarding handler reads
`context.capabilities.get(config.namespace)`, never `std::env::var`.
Same invariant as `from_openapi`. The handler implementation is verified
against this.
### Removal of the broken placeholder (alknet-call cleanup)
Remove the old broken implementation from `alknet-call`:
- Delete `crates/alknet-call/src/client/from_jsonschema.rs`.
- Remove `mod from_jsonschema;` and the
`pub use from_jsonschema::{from_jsonschema, FromJsonSchema};` re-export
from `crates/alknet-call/src/client/mod.rs`.
- Do **not** remove the `FromJsonSchema` variant from
`OperationProvenance` in `registry/registration.rs` — it stays. It is
now a handler-bearing leaf (the handler lives in `alknet-http`, but
the provenance type lives in `alknet-call` where the registry types
live).
The old task `call/client/from-jsonschema` (the one that built the
broken placeholder) is already marked `status: completed`; this task
supersedes that work. No downstream consumer depends on the old
`from_jsonschema` / `FromJsonSchema` exports from `alknet-call` (the
only consumer was the call crate's own tests, which are removed with
the file).
## Acceptance Criteria
### alknet-http (new adapter)
- [ ] `crates/alknet-http/src/adapters/from_jsonschema.rs` exists with
`FromJsonSchema` struct + `new()` constructor
- [ ] `FromJsonSchema` holds `spec`, `config`, `path_template`,
`method`, `http_client`
- [ ] `FromJsonSchema` implements `OperationAdapter` (`import()` returns
one `HandlerRegistration`)
- [ ] The `HandlerRegistration` has `provenance: FromJsonSchema`,
`composition_authority: None`, `scoped_env: None`
- [ ] The handler is a real reqwest forwarding handler (not a
placeholder) — reuses `from_openapi`'s forwarding logic
(`build_request` / `forward` / `forward_stream`)
- [ ] `Query`/`Mutation` → `HandlerKind::Once`; `Subscription` →
`HandlerKind::Stream` (ADR-049)
- [ ] Path-template substitution (`{id}` → input value), query params
from non-path fields, `body` field for the request body
- [ ] Credential injection from `context.capabilities` (Bearer / ApiKey
/ Basic), never `std::env::var` (ADR-014)
- [ ] Response parsing: JSON / text / binary (same content-type
branching as `from_openapi`)
- [ ] SSE streaming for `Subscription` ops (same `parse_sse_frames` as
`from_openapi`)
- [ ] Error fidelity: non-2xx mapped to declared `ErrorDefinition` by
status code, `HTTP_<status>` prefix (ADR-023)
- [ ] `HttpServiceConfig` and `HttpAuthScheme` reused from
`from_openapi` (not redefined)
- [ ] Exported from `crates/alknet-http/src/adapters/mod.rs`
(`pub use from_jsonschema::FromJsonSchema;`)
- [ ] Unit test: `import()` produces one `HandlerRegistration` with
`FromJsonSchema` provenance + `None` authority/env
- [ ] Unit test: forwarding handler builds the correct URL (path
substitution + query)
- [ ] Unit test: forwarding handler injects Bearer token from
`context.capabilities`
- [ ] Unit test: `Query` op → `HandlerKind::Once`, `Subscription` op →
`HandlerKind::Stream`
- [ ] Integration test: forwarding handler calls an external endpoint
via `SharedHttpClient` and returns the response (use the
`spawn_echo_server` pattern from `from_openapi`'s tests)
- [ ] Integration test: non-2xx response → declared error code
(`HTTP_<status>`)
- [ ] Integration test: SSE subscription streams `call.responded` events
- [ ] No `std::env::var` reads in the forwarding handler
### alknet-call (cleanup)
- [ ] `crates/alknet-call/src/client/from_jsonschema.rs` deleted
- [ ] `mod from_jsonschema;` removed from
`crates/alknet-call/src/client/mod.rs`
- [ ] `pub use from_jsonschema::{from_jsonschema, FromJsonSchema};`
removed from `crates/alknet-call/src/client/mod.rs`
- [ ] `FromJsonSchema` variant **kept** in `OperationProvenance`
(`registry/registration.rs`) — do not remove
- [ ] `AdapterError::SchemaParse` doc comment in
`crates/alknet-call/src/client/mod.rs` — update the
"`from_openapi` / `from_jsonschema` couldn't parse the spec"
wording if it now reads oddly (the variant stays; `from_jsonschema`
in `alknet-http` can still return `SchemaParse` if the
caller-supplied `OperationSpec` is somehow invalid, though that's
unlikely since the caller constructs it — use judgment)
### Build / lint
- [ ] `cargo build -p alknet-call -p alknet-http` succeeds
- [ ] `cargo test -p alknet-call -p alknet-http` succeeds
- [ ] `cargo clippy -p alknet-call -p alknet-http --all-targets` succeeds
with no warnings
- [ ] `cargo fmt --check` succeeds
## References
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md —
ADR-066 (the decision this task implements)
- docs/architecture/crates/http/http-adapters.md — §"from_jsonschema"
(the spec: API, flow, relationship to from_openapi, origin)
- docs/architecture/crates/call/client-and-adapters.md — §"from_jsonschema"
(the move note + pointer to the http spec)
- docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md —
ADR-017 §5 (amended — `from_jsonschema` clause superseded by ADR-066)
- docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md —
ADR-022 (amended — `FromJsonSchema` row now handler-bearing leaf)
- docs/architecture/decisions/023-operation-error-schemas.md — ADR-023
(`HTTP_<status>` prefix, error fidelity)
- docs/architecture/decisions/049-streaming-handler-for-subscriptions.md —
ADR-049 (`StreamingHandler` for `Subscription` ops)
- docs/architecture/decisions/014-secret-material-flow-and-capability-injection.md —
ADR-014 (no-env-vars invariant)
- tasks/http/adapters/from-openapi.md — the `from_openapi` task
(completed; the forwarding logic to reuse lives in its implementation)
- tasks/http/client/shared-http-client.md — `SharedHttpClient` (the
shared reqwest client both adapters use)
- tasks/call/client/from-jsonschema.md — the old task that built the
broken placeholder (superseded by this task)
## Notes
> This is a contained move, not new architecture. ADR-066 already
> landed the architecture; the spec docs already reflect the move.
> The implementation is: (1) write the real adapter in `alknet-http`
> reusing `from_openapi`'s forwarding logic (`build_request` /
> `forward` / `forward_stream` — make them `pub(crate)` or factor a
> shared `adapters/forwarding.rs`, whichever is less churn), (2) delete
> the broken placeholder from `alknet-call`, (3) keep the
> `FromJsonSchema` provenance variant in `registration.rs`. The handler
> shape is identical to `from_openapi` — do not duplicate the
> forwarding logic. The difference between the two adapters is purely
> the input shape: `from_openapi` parses a full OpenAPI doc and
> iterates `(path, method)` pairs; `from_jsonschema` takes one
> `(path_template, method)` from the caller and produces one bundle.
> Risk is low because the forwarding logic is already tested in
> `from_openapi`; the new code is the thin `FromJsonSchema` adapter
> struct + `import()` + wiring.