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
13 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| http/adapters/from-jsonschema | Move from_jsonschema to alknet-http as a real HTTP-backed single-endpoint adapter (ADR-066); remove broken placeholder from alknet-call | pending |
|
narrow | low | component | 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)
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 typefrom_openapiuses, re-exported fromcrate::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-suppliedOperationSpec(no parsing, nooperationIdnormalization — the caller named the op).handler= a reqwest forwarding handler, identical in shape tofrom_openapi's. Reusefrom_openapi'sbuild_request,forward, andforward_streamfunctions (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 asfrom_openapi— injected at registration, read fromcontext.capabilitiesat 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:
- Call them directly if they're
pub(crate)—from_jsonschemais in the same crate (alknet-http), same module tree (adapters/). Makebuild_request,forward,forward_stream(and the helpersvalue_to_path_segment,value_to_query,parse_sse_frames)pub(crate)and call them fromfrom_jsonschema.rs. - Factor a small shared module (e.g.
adapters/forwarding.rs) if you prefer the dependency to be explicit rather than reaching intofrom_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 thepub use from_jsonschema::{from_jsonschema, FromJsonSchema};re-export fromcrates/alknet-call/src/client/mod.rs. - Do not remove the
FromJsonSchemavariant fromOperationProvenanceinregistry/registration.rs— it stays. It is now a handler-bearing leaf (the handler lives inalknet-http, but the provenance type lives inalknet-callwhere 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.rsexists withFromJsonSchemastruct +new()constructorFromJsonSchemaholdsspec,config,path_template,method,http_clientFromJsonSchemaimplementsOperationAdapter(import()returns oneHandlerRegistration)- The
HandlerRegistrationhasprovenance: 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,bodyfield for the request body - Credential injection from
context.capabilities(Bearer / ApiKey / Basic), neverstd::env::var(ADR-014) - Response parsing: JSON / text / binary (same content-type
branching as
from_openapi) - SSE streaming for
Subscriptionops (sameparse_sse_framesasfrom_openapi) - Error fidelity: non-2xx mapped to declared
ErrorDefinitionby status code,HTTP_<status>prefix (ADR-023) HttpServiceConfigandHttpAuthSchemereused fromfrom_openapi(not redefined)- Exported from
crates/alknet-http/src/adapters/mod.rs(pub use from_jsonschema::FromJsonSchema;) - Unit test:
import()produces oneHandlerRegistrationwithFromJsonSchemaprovenance +Noneauthority/env - Unit test: forwarding handler builds the correct URL (path substitution + query)
- Unit test: forwarding handler injects Bearer token from
context.capabilities - Unit test:
Queryop →HandlerKind::Once,Subscriptionop →HandlerKind::Stream - Integration test: forwarding handler calls an external endpoint
via
SharedHttpClientand returns the response (use thespawn_echo_serverpattern fromfrom_openapi's tests) - Integration test: non-2xx response → declared error code
(
HTTP_<status>) - Integration test: SSE subscription streams
call.respondedevents - No
std::env::varreads in the forwarding handler
alknet-call (cleanup)
crates/alknet-call/src/client/from_jsonschema.rsdeletedmod from_jsonschema;removed fromcrates/alknet-call/src/client/mod.rspub use from_jsonschema::{from_jsonschema, FromJsonSchema};removed fromcrates/alknet-call/src/client/mod.rsFromJsonSchemavariant kept inOperationProvenance(registry/registration.rs) — do not removeAdapterError::SchemaParsedoc comment incrates/alknet-call/src/client/mod.rs— update the "from_openapi/from_jsonschemacouldn't parse the spec" wording if it now reads oddly (the variant stays;from_jsonschemainalknet-httpcan still returnSchemaParseif the caller-suppliedOperationSpecis somehow invalid, though that's unlikely since the caller constructs it — use judgment)
Build / lint
cargo build -p alknet-call -p alknet-httpsucceedscargo test -p alknet-call -p alknet-httpsucceedscargo clippy -p alknet-call -p alknet-http --all-targetssucceeds with no warningscargo fmt --checksucceeds
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_jsonschemaclause superseded by ADR-066) - docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md —
ADR-022 (amended —
FromJsonSchemarow 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 (
StreamingHandlerforSubscriptionops) - 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_openapitask (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-httpreusingfrom_openapi's forwarding logic (build_request/forward/forward_stream— make thempub(crate)or factor a sharedadapters/forwarding.rs, whichever is less churn), (2) delete the broken placeholder fromalknet-call, (3) keep theFromJsonSchemaprovenance variant inregistration.rs. The handler shape is identical tofrom_openapi— do not duplicate the forwarding logic. The difference between the two adapters is purely the input shape:from_openapiparses a full OpenAPI doc and iterates(path, method)pairs;from_jsonschematakes one(path_template, method)from the caller and produces one bundle. Risk is low because the forwarding logic is already tested infrom_openapi; the new code is the thinFromJsonSchemaadapter struct +import()+ wiring.