Skip to content

feat: add per-call options channel and OperationParams projection SPI#154

Open
OmarAlJarrah wants to merge 1 commit into
mainfrom
feat/per-call-options-and-operation-params
Open

feat: add per-call options channel and OperationParams projection SPI#154
OmarAlJarrah wants to merge 1 commit into
mainfrom
feat/per-call-options-and-operation-params

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Two hand-written sdk-core runtime primitives: a per-call options channel threaded through the context chain, and a minimal SPI for projecting an operation’s inputs into a request. Both are usable by hand today; no code generation is introduced.

Per-call options (Closes #27)

There was no way to override client behaviour for a single call without mutating the shared client. This adds an immutable options bag carried on every link of the DispatchContext -> RequestContext -> ExchangeContext chain.

  • CallOptions — a per-call timeout overlay, a response-validation decision, an optional ad-hoc credential, and an open set of typed extension attributes keyed by CallOption<T>. applyDefaults merges a call-site set onto a client baseline with per-field precedence (the receiver wins, falling back to the defaults); CallOptions.NONE is the identity element.
  • CallTimeout — a per-phase (connect/write/read/call) overlay where a null phase means "inherit", with the same applyDefaults overlay semantics. The per-call timeout overlay the issue calls for.
  • ResponseValidation — a three-state enum (INHERIT/ENABLED/DISABLED) so the inherit case is a first-class non-null value rather than a nullable boolean, matching the issue’s "non-null with defaults" guidance.
  • CallContext now exposes callOptions. DispatchContext mints it once at the head of the chain and each promotion (toRequestContext/toExchangeContext) carries it forward unchanged. It defaults to CallOptions.NONE, so every existing construction site is unaffected and the transport SPIs are untouched.

OperationParams projection (Closes #57)

Thin services need a way to project an operation’s inputs into path/query/headers/body that feeds the context chain. The QueryParams multimap (#28) is not in yet, so query contributions are modeled as flat ordered name/value pairs that migrate mechanically once it lands.

  • OperationParams — a narrow SPI with pathParams(), queryParams(), headers(), body(), each defaulting to empty so an implementation overrides only what it contributes.
  • PathParam / QueryParam — the raw, unencoded value types the projection returns.
  • RequestProjector.project(baseUrl, method, pathTemplate, params) — materializes a Request, substituting {name} path placeholders and appending the query string, and owning percent-encoding (path segments escape /; query names/values are form-encoded with + rewritten to %20).
  • RequestProjector.projectInto(dispatch, ...) — projects and promotes a DispatchContext straight into a RequestContext, registering it on the chain’s store slot and carrying the chain’s instrumentation, call key, and call options forward.

Tests

New suites: CallOptionsTest, CallTimeoutTest, ResponseValidationTest, CallOptionsPropagationTest (chain threading), OperationParamsTest, RequestProjectorTest (substitution, encoding, repeated/valueless query params, header/body application, error cases, projectInto promotion).

Gated build (module-scoped)

./gradlew :sdk-core:test :sdk-core:ktlintCheck :sdk-core:detekt :sdk-core:apiCheck --no-daemon

All green. Public API snapshot regenerated with ./gradlew :sdk-core:apiDump --no-daemon and committed.

Introduce two sdk-core runtime primitives that let a single call override
client behaviour and let an operation describe its inputs, without mutating
the shared client or generating code.

Per-call options:
- CallOptions: an immutable, per-call options bag carrying a timeout overlay,
  a response-validation decision, an optional ad-hoc credential, and an open
  set of typed extension attributes keyed by CallOption<T>. applyDefaults
  merges a call-site set onto a client baseline with per-field precedence
  (receiver wins, falling back to defaults); CallOptions.NONE is the identity.
- CallTimeout: a per-phase (connect/write/read/call) timeout overlay where a
  null phase means "inherit", with the same applyDefaults overlay semantics.
- ResponseValidation: a three-state enum (INHERIT/ENABLED/DISABLED) so the
  inherit case is a first-class non-null value rather than a nullable boolean.
- CallContext now exposes callOptions; DispatchContext mints it once at the
  head of the chain and every promotion carries it forward unchanged, so the
  dispatch / request / exchange phases all observe the same overrides. It
  defaults to CallOptions.NONE, preserving existing construction sites.

Operation projection:
- OperationParams: a narrow SPI projecting an operation's inputs into path,
  query, headers, and body, each with an empty default so an implementation
  overrides only what it contributes. PathParam and QueryParam are the raw,
  unencoded value types it returns.
- RequestProjector: materializes an OperationParams plus a method and path
  template into a Request (owning percent-encoding for path segments and the
  query string), and projectInto promotes a DispatchContext straight into a
  RequestContext, tying the SPI to the context chain in one call.

Public API surface regenerated via apiDump.
@OmarAlJarrah

Copy link
Copy Markdown
Member Author

This adds a per-call options channel (CallOptions: per-phase timeout overlay, tri-state response validation, ad-hoc credential, typed attribute map) threaded through the CallContext -> DispatchContext -> RequestContext -> ExchangeContext chain, plus an operation package (OperationParams SPI with PathParam/QueryParam and a RequestProjector that builds a Request from a path template with owned percent-encoding). The context constructors take a trailing callOptions defaulted to CallOptions.NONE, and the API snapshot is regenerated to match. Looks good to merge; just one minor note below.

Issues

Unresolved-placeholder check can miss a malformed segment containing a slashsdk-core/src/main/kotlin/org/dexpace/sdk/core/operation/RequestProjector.kt:117-123. The guard only inspects the first {...} pair and treats any pair containing a / as not-a-placeholder, so a template like users/{id/extra} with no matching param passes through silently and produces a malformed segment instead of failing fast. The common cases (missing param, extra param) are correctly rejected and tested, and percent-encoding escapes braces in substituted values so a real value can't forge a placeholder, so impact is low. Worth either tightening the check or calling out the slash assumption at the public API boundary so callers know malformed-with-slash templates aren't validated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define a minimal OperationParams SPI feeding the context chain Add a per-call options channel on the request context

1 participant