Skip to content

Commit ce78e93

Browse files
authored
Introduce MeasureEnvironment: consolidate endpoint params, resolve at construction (#981)
* Introduce MeasureReference sealed interface 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. * Fix benchmark test: replace Either3 with MeasureReference * spotless * Introduce MeasureEnvironment: consolidate endpoint params 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). * Pull up additionalData federation as explicit pipeline step 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. * resolveRepository is the single environment setup step 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. * Remove repo branching: resolvedRepo is the only repo 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. * Environment resolved at construction: MeasureEnvironment gone from eval 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. * feat: test for measure enviornment * bug: spotless issues
1 parent 8ffcaad commit ce78e93

18 files changed

Lines changed: 226 additions & 141 deletions

File tree

cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,6 @@ public When evaluate() {
149149
null,
150150
null,
151151
null,
152-
null,
153-
additionalData,
154-
null,
155-
null,
156152
null);
157153
return this;
158154
}

cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public class CrDstu3Config {
2626
@Bean
2727
IMeasureServiceFactory dstu3MeasureServiceFactory(
2828
IRepositoryFactory repositoryFactory, MeasureEvaluationOptions evaluationOptions) {
29-
return rd -> new Dstu3MeasureService(repositoryFactory.create(rd), evaluationOptions);
29+
return (requestDetails, environment) -> new Dstu3MeasureService(
30+
environment.resolve(repositoryFactory.create(requestDetails)), evaluationOptions);
3031
}
3132

3233
@Bean

cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ R4MeasureEvaluatorSingleFactory r4MeasureServiceFactory(
7575
MeasureEvaluationOptions evaluationOptions,
7676
MeasurePeriodValidator measurePeriodValidator) {
7777
// We are effectively returning an R4MeasureEvaluatorSingle her
78-
return requestDetails -> new R4MultiMeasureService(
79-
repositoryFactory.create(requestDetails),
78+
return (requestDetails, environment) -> new R4MultiMeasureService(
79+
environment.resolve(repositoryFactory.create(requestDetails)),
8080
evaluationOptions,
8181
requestDetails.getFhirServerBase(),
8282
measurePeriodValidator);
@@ -87,8 +87,11 @@ R4MeasureEvaluatorMultipleFactory r4MeasureEvaluatorMultipleFactory(
8787
IRepositoryFactory repositoryFactory,
8888
MeasureEvaluationOptions evaluationOptions,
8989
MeasurePeriodValidator measurePeriodValidator) {
90-
return rd -> new R4MultiMeasureService(
91-
repositoryFactory.create(rd), evaluationOptions, rd.getFhirServerBase(), measurePeriodValidator);
90+
return (requestDetails, environment) -> new R4MultiMeasureService(
91+
environment.resolve(repositoryFactory.create(requestDetails)),
92+
evaluationOptions,
93+
requestDetails.getFhirServerBase(),
94+
measurePeriodValidator);
9295
}
9396

9497
@Bean
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.opencds.cqf.fhir.cr.hapi.dstu3;
22

33
import ca.uhn.fhir.rest.api.server.RequestDetails;
4+
import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment;
45
import org.opencds.cqf.fhir.cr.measure.dstu3.Dstu3MeasureService;
56

67
@FunctionalInterface
78
public interface IMeasureServiceFactory {
8-
Dstu3MeasureService create(RequestDetails requestDetails);
9+
Dstu3MeasureService create(RequestDetails requestDetails, MeasureEnvironment environment);
910
}

cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/measure/MeasureOperationsProvider.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent;
1919
import org.hl7.fhir.exceptions.FHIRException;
2020
import org.opencds.cqf.fhir.cr.hapi.dstu3.IMeasureServiceFactory;
21+
import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment;
2122
import org.springframework.stereotype.Component;
2223

2324
@Component
@@ -72,8 +73,9 @@ public MeasureReport evaluateMeasure(
7273
RequestDetails requestDetails)
7374
throws InternalErrorException, FHIRException {
7475
var terminologyEndpointParam = (Endpoint) getEndpoint(fhirVersion, terminologyEndpoint);
76+
var environment = new MeasureEnvironment(null, terminologyEndpointParam, null, additionalData);
7577
return dstu3MeasureProcessorFactory
76-
.create(requestDetails)
78+
.create(requestDetails, environment)
7779
.evaluateMeasure(
7880
id,
7981
periodStart,
@@ -83,8 +85,6 @@ public MeasureReport evaluateMeasure(
8385
practitioner,
8486
lastReceivedOn,
8587
productLine,
86-
additionalData,
87-
parameters,
88-
terminologyEndpointParam);
88+
parameters);
8989
}
9090
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.opencds.cqf.fhir.cr.hapi.r4;
22

33
import ca.uhn.fhir.rest.api.server.RequestDetails;
4+
import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment;
45
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureEvaluatorMultiple;
56

67
@FunctionalInterface
78
public interface R4MeasureEvaluatorMultipleFactory {
8-
R4MeasureEvaluatorMultiple create(RequestDetails requestDetails);
9+
R4MeasureEvaluatorMultiple create(RequestDetails requestDetails, MeasureEnvironment environment);
910
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.opencds.cqf.fhir.cr.hapi.r4;
22

33
import ca.uhn.fhir.rest.api.server.RequestDetails;
4+
import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment;
45
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureEvaluatorSingle;
56

67
@FunctionalInterface
78
public interface R4MeasureEvaluatorSingleFactory {
8-
R4MeasureEvaluatorSingle create(RequestDetails requestDetails);
9+
R4MeasureEvaluatorSingle create(RequestDetails requestDetails, MeasureEnvironment environment);
910
}

cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/measure/MeasureOperationsProvider.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.opencds.cqf.fhir.cr.hapi.common.StringTimePeriodHandler;
2121
import org.opencds.cqf.fhir.cr.hapi.r4.R4MeasureEvaluatorMultipleFactory;
2222
import org.opencds.cqf.fhir.cr.hapi.r4.R4MeasureEvaluatorSingleFactory;
23+
import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment;
2324
import org.opencds.cqf.fhir.cr.measure.common.MeasureReference;
2425

2526
@SuppressWarnings("java:S107")
@@ -87,19 +88,17 @@ public MeasureReport evaluateMeasure(
8788
var contentEndpointParam = (Endpoint) getEndpoint(fhirVersion, contentEndpoint);
8889
var terminologyEndpointParam = (Endpoint) getEndpoint(fhirVersion, terminologyEndpoint);
8990
var dataEndpointParam = (Endpoint) getEndpoint(fhirVersion, dataEndpoint);
91+
var environment = new MeasureEnvironment(
92+
contentEndpointParam, terminologyEndpointParam, dataEndpointParam, additionalData);
9093
return r4MeasureServiceFactory
91-
.create(requestDetails)
94+
.create(requestDetails, environment)
9295
.evaluate(
9396
new MeasureReference.ById(id),
9497
stringTimePeriodHandler.getStartZonedDateTime(periodStart, requestDetails),
9598
stringTimePeriodHandler.getEndZonedDateTime(periodEnd, requestDetails),
9699
reportType,
97100
subject,
98101
lastReceivedOn,
99-
contentEndpointParam,
100-
terminologyEndpointParam,
101-
dataEndpointParam,
102-
additionalData,
103102
parameters,
104103
productLine,
105104
practitioner);
@@ -157,18 +156,16 @@ public Parameters evaluate(
157156
var terminologyEndpointParam = (Endpoint) getEndpoint(fhirVersion, terminologyEndpoint);
158157
var dataEndpointParam = (Endpoint) getEndpoint(fhirVersion, dataEndpoint);
159158
var measureRefs = MeasureReference.fromOperationParams(measureId, measureIdentifier, measureUrl);
159+
var environment = new MeasureEnvironment(
160+
contentEndpointParam, terminologyEndpointParam, dataEndpointParam, additionalData);
160161
return r4MultiMeasureServiceFactory
161-
.create(requestDetails)
162+
.create(requestDetails, environment)
162163
.evaluate(
163164
measureRefs,
164165
stringTimePeriodHandler.getStartZonedDateTime(periodStart, requestDetails),
165166
stringTimePeriodHandler.getEndZonedDateTime(periodEnd, requestDetails),
166167
reportType,
167168
subject,
168-
contentEndpointParam,
169-
terminologyEndpointParam,
170-
dataEndpointParam,
171-
additionalData,
172169
parameters,
173170
productLine,
174171
reporter);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.opencds.cqf.fhir.cr.measure.common;
2+
3+
import ca.uhn.fhir.repository.IRepository;
4+
import jakarta.annotation.Nullable;
5+
import org.hl7.fhir.instance.model.api.IBaseBundle;
6+
import org.hl7.fhir.instance.model.api.IBaseResource;
7+
import org.opencds.cqf.fhir.utility.repository.FederatedRepository;
8+
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
9+
import org.opencds.cqf.fhir.utility.repository.Repositories;
10+
11+
/**
12+
* Version-agnostic environment configuration for measure evaluation.
13+
*
14+
* <p>Separates the <em>infrastructure inputs</em> (where data, content, and terminology come from)
15+
* from the <em>operation parameters</em> (what to evaluate). Per the pipeline architecture,
16+
* environment resolution happens before domain logic: the service layer composes a
17+
* {@code ProxyRepository} from these endpoints and passes it to the evaluator.
18+
*
19+
* <p>Endpoint resources are typed as {@link IBaseResource} so both R4 and DSTU3
20+
* {@code Endpoint} instances can be carried without version coupling.
21+
* {@code Repositories.proxy()} already accepts {@code IBaseResource} endpoints directly.
22+
*
23+
* @param contentEndpoint endpoint for library/content resolution (nullable)
24+
* @param terminologyEndpoint endpoint for terminology resolution (nullable)
25+
* @param dataEndpoint endpoint for clinical data retrieval (nullable)
26+
* @param additionalData supplemental data bundle for repository federation and CQL engine
27+
* configuration (nullable)
28+
*/
29+
public record MeasureEnvironment(
30+
@Nullable IBaseResource contentEndpoint,
31+
@Nullable IBaseResource terminologyEndpoint,
32+
@Nullable IBaseResource dataEndpoint,
33+
@Nullable IBaseBundle additionalData) {
34+
35+
/** Empty environment — no endpoints, no additional data. */
36+
public static final MeasureEnvironment EMPTY = new MeasureEnvironment(null, null, null, null);
37+
38+
/**
39+
* Resolves this environment against a base repository.
40+
*
41+
* <p>If any endpoint is present, wraps {@code base} in a {@code ProxyRepository}; null
42+
* endpoints fall back to {@code base}. If {@code additionalData} is present, federates
43+
* the result with an in-memory repository seeded from that bundle.
44+
*
45+
* @param base the base repository to build on top of
46+
* @return the resolved repository, possibly wrapped
47+
*/
48+
public IRepository resolve(IRepository base) {
49+
IRepository repo = base;
50+
if (dataEndpoint() != null || contentEndpoint() != null || terminologyEndpoint() != null) {
51+
repo = Repositories.proxy(repo, true, dataEndpoint(), contentEndpoint(), terminologyEndpoint());
52+
}
53+
if (additionalData() != null) {
54+
repo = new FederatedRepository(repo, new InMemoryFhirRepository(repo.fhirContext(), additionalData()));
55+
}
56+
return repo;
57+
}
58+
}

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureEvaluatorSingle.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package org.opencds.cqf.fhir.cr.measure.dstu3;
22

3-
import org.hl7.fhir.dstu3.model.Bundle;
4-
import org.hl7.fhir.dstu3.model.Endpoint;
53
import org.hl7.fhir.dstu3.model.IdType;
64
import org.hl7.fhir.dstu3.model.MeasureReport;
75
import org.hl7.fhir.dstu3.model.Parameters;
@@ -21,7 +19,5 @@ MeasureReport evaluateMeasure(
2119
String practitioner,
2220
String lastReceivedOn,
2321
String productLine,
24-
Bundle additionalData,
25-
Parameters parameters,
26-
Endpoint terminologyEndpoint);
22+
Parameters parameters);
2723
}

0 commit comments

Comments
 (0)