Skip to content

Introduce MeasureEnvironment: consolidate endpoint params, resolve at construction#981

Merged
JPercival merged 14 commits intomainfrom
measure-environment
Mar 27, 2026
Merged

Introduce MeasureEnvironment: consolidate endpoint params, resolve at construction#981
JPercival merged 14 commits intomainfrom
measure-environment

Conversation

@JPercival
Copy link
Copy Markdown
Contributor

Summary

Consolidates contentEndpoint, terminologyEndpoint, dataEndpoint, and additionalData into a single MeasureEnvironment record, then moves resolution to the right place in the pipeline.

MeasureEnvironment record

public record MeasureEnvironment(
    @Nullable IBaseResource contentEndpoint,
    @Nullable IBaseResource terminologyEndpoint,
    @Nullable IBaseResource dataEndpoint,
    @Nullable IBaseBundle additionalData) {

    public static final MeasureEnvironment EMPTY = ...;
    public IRepository resolve(IRepository base) { ... }
}

Version-agnostic (IBaseResource/IBaseBundle) so it lives cleanly in measure/common without importing version-specific FHIR types. MeasureEnvironment.resolve() is the single place that composes the repository: endpoint proxy if all three endpoints are set, then additional-data federation on top.

Environment resolved at construction, not at evaluation

Per the operation pipeline (Architecture.md):

HAPI Provider → Environment Config → Runtime → Denormalization

Environment config must happen before the runtime, not inside it. Previously contentEndpoint, terminologyEndpoint, dataEndpoint, additionalData flowed through every layer as four separate parameters and were resolved deep inside R4MultiMeasureService.evaluateToListOfList().

After this change:

  1. HAPI providers build MeasureEnvironment from @OperationParam inputs
  2. Pass it to factory.create(requestDetails, environment)
  3. Factory calls environment.resolve(repositoryFactory.create(requestDetails))
  4. Service is constructed with the fully-configured IRepository
  5. evaluate() methods take only domain parameters — no MeasureEnvironment

MeasureEnvironment is completely absent from R4MeasureEvaluatorSingle, R4MeasureEvaluatorMultiple, Dstu3MeasureEvaluatorSingle, and both service implementations.

resolveRepository lifecycle

Environment resolution builds up the repo in two steps, now made explicit:

  • Endpoint proxy: Repositories.proxy() when all three endpoints are configured
  • Additional data federation: FederatedRepository overlay when additionalData is non-null

Both steps happen once, at construction. The CQL engine receives the already-federated resolvedRepo (not the bundle again separately).

Test plan

  • All R4 measure tests pass
  • spotlessCheck passes

🤖 Generated with Claude Code

Replace Either3<CanonicalType, IdType, Measure> and the three-separate-lists
pattern (List<IdType>, List<String>, List<String>) with a single sealed type:

  sealed interface MeasureReference {
      record ById(IIdType id)
      record ByIdentifier(String identifier)
      record ByCanonicalUrl(String url)
  }

HAPI providers convert at the boundary via fromOperationParams().
R4MeasureServiceUtils.getMeasures() dispatches on the sealed type.
R4CareGapsParameters uses List<MeasureReference> instead of 3 fields.
R4MultiMeasureService, R4CareGapsProcessor accept List<MeasureReference>.
R4CareGapsService.liftMeasureParameters() deleted -- boundary conversion
now happens once in the transport layer.
R4CollectDataService reads Measure directly via repository.read() instead
of wrapping in Either3.

The type is self-documenting: a measure can be referenced by ID,
identifier, or canonical URL. Compiler enforces exhaustive handling.
contentEndpoint, terminologyEndpoint, dataEndpoint, and additionalData
were passed as four separate parameters through every layer of the
measure evaluation stack -- both public interfaces and internal methods.
Replace all four with MeasureEnvironment.

  record MeasureEnvironment(
      IBaseResource contentEndpoint,
      IBaseResource terminologyEndpoint,
      IBaseResource dataEndpoint,
      IBaseBundle additionalData)

Per the pipeline architecture, environment resolution is step 2 --
separate from the operation parameters. HAPI providers (the transport
boundary) now construct MeasureEnvironment from the resolved endpoint
params before calling the service layer. All downstream code receives
a single typed object.

R4MultiMeasureService.evaluateToListOfList() now delegates repository
construction to a private resolveRepository(MeasureEnvironment) helper,
making the step explicit and testable in isolation.

MeasureEnvironment.EMPTY is used by R4CareGapsBundleBuilder (which has
no endpoint configuration) and by test helpers that only set additionalData.

IBaseResource / IBaseBundle carry the endpoints version-agnostically,
consistent with the domain-core invariant (no version-specific types).
federateWithAdditionalData(IRepository, MeasureEnvironment) is now a
named private method alongside resolveRepository(MeasureEnvironment),
making the two-step environment setup explicit in evaluateToListOfList:

  var effectiveRepo = resolveRepository(environment);   // endpoint proxy
  var subjectRepo   = federateWithAdditionalData(effectiveRepo, environment); // bundle overlay

Previously the FederatedRepository construction was buried inside
getSubjectsForEvaluateSingle, which also hardcoded this.repository as
the federation base instead of effectiveRepo. That meant additional-data
subjects were resolved against the base repository even when an endpoint
proxy was active.

getSubjectsForEvaluateSingle now takes the already-prepared subjectRepo
directly — one job, one level of abstraction.
All environment config is resolved upfront in resolveRepository():
- endpoint proxy (all three endpoints → Repositories.proxy)
- additional data federation (bundle → FederatedRepository overlay)

The CQL engine receives resolvedRepo and null for additionalData —
the bundle is already accessible through the federated repository.
Passing it twice was wrong: the repo is the resolved environment.
Once resolveRepository() runs, there is one repo. No switching.
The pre-cached standard processor and utils fields were a remnant
of the old conditional — delete them and construct directly from
resolvedRepo every time.
…al signatures

The resolved IRepository is now a constructor parameter, not a method
parameter threaded through the call stack.

MeasureEnvironment.resolve(IRepository) composes the final repository:
endpoint proxy (when all three endpoints present) then additional-data
federation. This is the single place that knows how to build the repo.

Operation providers (HAPI transport boundary) resolve environment
before constructing the service via the factory:
  factory.create(requestDetails, environment)
  → environment.resolve(repositoryFactory.create(requestDetails))
  → new R4MultiMeasureService(resolvedRepo, ...)

R4MeasureEvaluatorSingle, R4MeasureEvaluatorMultiple, and
Dstu3MeasureEvaluatorSingle no longer carry MeasureEnvironment.
R4MultiMeasureService.resolveRepository() is deleted. The service
receives a repo that is already fully configured; it just uses it.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 25, 2026

Formatting check succeeded!

Base automatically changed from measure-reference to main March 26, 2026 21:07
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
74.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@JPercival JPercival merged commit ce78e93 into main Mar 27, 2026
6 of 9 checks passed
@JPercival JPercival deleted the measure-environment branch March 27, 2026 21:07
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.

2 participants