Skip to content

Implement a solid and reusable RepositoryProxyFactory pattern for Repository proxies.#963

Draft
lukedegruchy wants to merge 1 commit intomasterfrom
ld-20260210-repository-proxy
Draft

Implement a solid and reusable RepositoryProxyFactory pattern for Repository proxies.#963
lukedegruchy wants to merge 1 commit intomasterfrom
ld-20260210-repository-proxy

Conversation

@lukedegruchy
Copy link
Contributor

@lukedegruchy lukedegruchy commented Mar 10, 2026

Summary

This MR introduces a RepositoryProxyFactory abstraction to decouple all clinical reasoning processors from the static Repositories.proxy() / Repositories.createRestRepository() helpers. Previously, processors (ActivityDefinition, PlanDefinition, Library, CQL, Questionnaire, QuestionnaireResponse, GraphDefinition, Measure, CareGaps) hard-coded calls to these static methods, making it impossible to substitute proxy behaviour in tests without standing up live FHIR servers. The factory is injected via constructors throughout the processor hierarchy and wired as a Spring bean in the HAPI config layer.

  1. New RepositoryProxyFactory interface and DefaultRepositoryProxyFactory in cqf-fhir-utility — the interface accepts raw endpoint resources (IBaseResource) rather than pre-resolved IRepository instances, so processors no longer need to call createRestRepository() themselves.
  2. Processor constructor consolidation — telescope constructors (e.g. new Processor(repo) -> new Processor(repo, settings) -> new Processor(repo, settings, ops)) are removed in favour of a single constructor that requires the factory, enforced with requireNonNull.
  3. R4MultiMeasureService proxy simplification — the duplicated null-check-then-proxy pattern is replaced by unconditionally calling proxy() + withRepository() on cached processor/utils instances. withRepository() returns this when the repository is the same instance, avoiding unnecessary object creation.
  4. Test infrastructureNoOpRepositoryProxyFactory (always returns the local repo) replaces DefaultRepositoryProxyFactory in all tests that don't exercise proxy routing. R4MultiMeasureServiceProxyTest with TestRepositoryProxyFactory validates that endpoint-based routing actually works by splitting resources across InMemoryFhirRepository instances.

Code Review Suggestions

  • Breaking API change: All processor constructors now require RepositoryProxyFactory as a mandatory parameter, and the 1-arg and 2-arg telescope constructors are removed. Verify that no downstream consumers (HAPI FHIR starter, Android SDK, other integrators) rely on the removed constructors — the compiler will catch direct callers, but reflection-based or Spring XML-based instantiation will fail silently at runtime.
  • ApplyRequestBuilder removed withDataRepository(), withContentRepository(), withTerminologyRepository() setters — these accepted IRepository directly and are replaced by endpoint-based wiring. Check whether any HAPI provider or external code calls these removed methods.
  • R4MultiMeasureService original proxy condition used && (all three non-null) while the new code calls proxy() unconditionally — verify this is the intended semantic change. The old code only proxied when all endpoints were non-null; the new code delegates to the factory regardless, which handles partial endpoints correctly via Repositories.proxy().
  • QuestionnaireResponseProcessor is now constructed inside ApplyProcessor with new QuestionnaireResponseProcessor(repo, crSettings, null, repositoryProxyFactory) — previously it used a no-arg-ish constructor. Confirm CrSettings propagation here is correct (it receives the PlanDefinition processor's settings, not QuestionnaireResponse-specific settings).
  • Spring bean repositoryProxyFactory() in CrBaseConfig is not annotated with @ConditionalOnMissingBean — if a downstream project (like Smile CDR) defines its own RepositoryProxyFactory bean, it will conflict. Verify whether this should be conditional.

QA Test Suggestions

Setup

  • Deploy a HAPI FHIR server (or Smile CDR) with the cqf-fhir-cr-hapi module loaded
  • Ensure clinical reasoning operations are enabled (Measure, PlanDefinition, ActivityDefinition, Questionnaire, Library, CareGaps, CQL, GraphDefinition)
  • Load a test IG with at least one Measure, PlanDefinition, Library, and Questionnaire resource

Test Cases

  • $evaluate-measure without endpoints: Call Measure/$evaluate-measure with no dataEndpoint, contentEndpoint, or terminologyEndpoint parameters. Verify the measure evaluates successfully using the local server's data — this exercises the default RepositoryProxyFactory path.
  • $evaluate-measure with endpoint parameters: If a remote FHIR terminology server is available, call $evaluate-measure with a terminologyEndpoint pointing to it. Verify that ValueSet expansion comes from the remote server.
  • $apply (PlanDefinition): Call PlanDefinition/$apply with and without endpoint parameters. Verify the CarePlan/RequestGroup is returned correctly in both cases.
  • $apply (ActivityDefinition): Call ActivityDefinition/$apply with and without endpoint parameters. Verify the generated resource (e.g. MedicationRequest, ServiceRequest) is correct.
  • $populate (Questionnaire): Call Questionnaire/$populate with a subject. Verify pre-population works against local data.
  • $extract (QuestionnaireResponse): Submit a QuestionnaireResponse for $extract. Verify the extracted resources are correct — this validates the ApplyProcessor -> QuestionnaireResponseProcessor construction chain.
  • $care-gaps: Call Measure/$care-gaps with required parameters. Verify the care gaps bundle is returned with correct composition and detected issues.
  • Multi-measure evaluation: Call the multi-measure endpoint with multiple measure IDs. Verify all MeasureReports are returned in the Parameters response.
  • Spring context startup: Verify the application context starts cleanly with no bean definition conflicts or missing dependencies related to RepositoryProxyFactory.

…ries.proxy()

Processors (ActivityDefinition, PlanDefinition, Library, CQL,
Questionnaire, QuestionnaireResponse, GraphDefinition, Measure/CareGaps)
previously called Repositories.proxy() and createRestRepository() directly,
coupling them to the static helper and making proxy behaviour untestable
in isolation.

This change:

- Adds a RepositoryProxyFactory interface in cqf-fhir-utility with a
  DefaultRepositoryProxyFactory that delegates to Repositories.proxy()
- Injects RepositoryProxyFactory into all processors, replacing direct
  static calls to Repositories.proxy() and createRestRepository()
- Removes telescope constructors from processors in favour of a single
  constructor that requires the factory
- Adds withRepository() to R4MeasureProcessor and R4MeasureServiceUtils
  so R4MultiMeasureService can unconditionally call proxy() and avoid
  duplicated null-checking logic
- Introduces NoOpRepositoryProxyFactory for tests that don't exercise
  proxy routing, replacing DefaultRepositoryProxyFactory in all test
  files except R4MultiMeasureServiceProxyTest
- Adds R4MultiMeasureServiceProxyTest and TestRepositoryProxyFactory to
  verify proxy routing with custom endpoint repositories
- Wires the factory through HAPI Spring configs (CrBaseConfig,
  CrProcessorConfig, CrR4Config)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link

Formatting check succeeded!

@lukedegruchy lukedegruchy changed the title Introduce RepositoryProxyFactory to decouple processors from Reposito… Implement a solid and reusable RepositoryProxyFactory pattern for Repository proxies. Mar 10, 2026
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

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.

1 participant