Skip to content

Commit 40ca16a

Browse files
Chris0296claude
andcommitted
Reset exp-params and input-exp-params on $draft
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent c423c0b commit 40ca16a

7 files changed

Lines changed: 650 additions & 0 deletions

File tree

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/DraftVisitor.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ public IBase visit(IKnowledgeArtifactAdapter adapter, IBaseParameters draftParam
5151
adapter.setExtension(removeReleaseLabelAndDescription);
5252
// remove approval date
5353
adapter.setApprovalDate(null);
54+
// reset expansion parameters to the author-specified input, in preparation for future releases
55+
switch (repository.fhirContext().getVersion().getVersion()) {
56+
case DSTU3 -> org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter);
57+
case R4 -> org.opencds.cqf.fhir.cr.visitor.r4.DraftVisitor.restoreInputExpansionParams(adapter);
58+
case R5 -> org.opencds.cqf.fhir.cr.visitor.r5.DraftVisitor.restoreInputExpansionParams(adapter);
59+
default ->
60+
throw new UnprocessableEntityException("Unsupported FHIR version: "
61+
+ repository.fhirContext().getVersion().getVersion());
62+
}
5463
// new draft version
5564
String draftVersion = version + "-draft";
5665
String draftVersionUrl = Canonicals.getUrl(adapter.getUrl()) + "|" + draftVersion;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.opencds.cqf.fhir.cr.visitor.dstu3;
2+
3+
import java.util.ArrayList;
4+
import org.hl7.fhir.dstu3.model.DomainResource;
5+
import org.hl7.fhir.dstu3.model.Parameters;
6+
import org.hl7.fhir.dstu3.model.Reference;
7+
import org.opencds.cqf.fhir.cr.visitor.r4.ReleaseVisitor;
8+
import org.opencds.cqf.fhir.utility.Constants;
9+
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter;
10+
11+
public class DraftVisitor {
12+
13+
private DraftVisitor() {}
14+
15+
/**
16+
* Reverses {@link ReleaseVisitor#captureInputExpansionParams}. Restores the author-specified
17+
* input-exp-params back onto exp-params (overwriting any processing-time values), then removes the
18+
* input-exp-params contained resource and the extension referencing it. No-op if the artifact does
19+
* not have an input expansion parameters extension.
20+
*/
21+
public static void restoreInputExpansionParams(IKnowledgeArtifactAdapter rootAdapter) {
22+
var rootResource = (DomainResource) rootAdapter.get();
23+
var inputExpansionParamsExtension = rootResource.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS);
24+
if (inputExpansionParamsExtension == null) {
25+
return;
26+
}
27+
28+
var reference = ((Reference) inputExpansionParamsExtension.getValue()).getReference();
29+
var inputExpansionParams = rootResource.getContained().stream()
30+
.filter(contained -> reference.equals("#" + contained.getId()))
31+
.filter(Parameters.class::isInstance)
32+
.map(Parameters.class::cast)
33+
.findFirst();
34+
35+
// exp-params is set if input-exp-params is actually found
36+
inputExpansionParams.ifPresent(inputParams -> {
37+
var expansionParams =
38+
(Parameters) rootAdapter.getExpansionParameters().orElseThrow();
39+
expansionParams.setParameter(new ArrayList<>(inputParams.getParameter()));
40+
});
41+
42+
// cleanup of input-exp-params
43+
rootResource.getContained().removeIf(contained -> reference.equals("#" + contained.getId()));
44+
rootResource.getExtension().removeIf(ext -> Constants.CQF_INPUT_EXPANSION_PARAMETERS.equals(ext.getUrl()));
45+
}
46+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.opencds.cqf.fhir.cr.visitor.r4;
2+
3+
import java.util.ArrayList;
4+
import org.hl7.fhir.r4.model.DomainResource;
5+
import org.hl7.fhir.r4.model.Parameters;
6+
import org.hl7.fhir.r4.model.Reference;
7+
import org.opencds.cqf.fhir.utility.Constants;
8+
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter;
9+
10+
public class DraftVisitor {
11+
12+
private DraftVisitor() {}
13+
14+
/**
15+
* Reverses {@link ReleaseVisitor#captureInputExpansionParams}. Restores the author-specified
16+
* input-exp-params back onto exp-params (overwriting any processing-time values), then removes the
17+
* input-exp-params contained resource and the extension referencing it. No-op if the artifact does
18+
* not have an input expansion parameters extension.
19+
*/
20+
public static void restoreInputExpansionParams(IKnowledgeArtifactAdapter rootAdapter) {
21+
var rootResource = (DomainResource) rootAdapter.get();
22+
var inputExpansionParamsExtension = rootResource.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS);
23+
if (inputExpansionParamsExtension == null) {
24+
return;
25+
}
26+
27+
var reference = ((Reference) inputExpansionParamsExtension.getValue()).getReference();
28+
var inputExpansionParams = rootResource.getContained().stream()
29+
.filter(contained -> reference.equals("#" + contained.getId()))
30+
.filter(Parameters.class::isInstance)
31+
.map(Parameters.class::cast)
32+
.findFirst();
33+
34+
// exp-params is set if input-exp-params is actually found
35+
inputExpansionParams.ifPresent(inputParams -> {
36+
var expansionParams =
37+
(Parameters) rootAdapter.getExpansionParameters().orElseThrow();
38+
expansionParams.setParameter(new ArrayList<>(inputParams.getParameter()));
39+
});
40+
41+
// cleanup of input-exp-params
42+
rootResource.getContained().removeIf(contained -> reference.equals("#" + contained.getId()));
43+
rootResource.getExtension().removeIf(ext -> Constants.CQF_INPUT_EXPANSION_PARAMETERS.equals(ext.getUrl()));
44+
}
45+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.opencds.cqf.fhir.cr.visitor.r5;
2+
3+
import java.util.ArrayList;
4+
import org.hl7.fhir.r5.model.DomainResource;
5+
import org.hl7.fhir.r5.model.Parameters;
6+
import org.hl7.fhir.r5.model.Reference;
7+
import org.opencds.cqf.fhir.cr.visitor.r4.ReleaseVisitor;
8+
import org.opencds.cqf.fhir.utility.Constants;
9+
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter;
10+
11+
public class DraftVisitor {
12+
13+
private DraftVisitor() {}
14+
15+
/**
16+
* Reverses {@link ReleaseVisitor#captureInputExpansionParams}. Restores the author-specified
17+
* input-exp-params back onto exp-params (overwriting any processing-time values), then removes the
18+
* input-exp-params contained resource and the extension referencing it. No-op if the artifact does
19+
* not have an input expansion parameters extension.
20+
*/
21+
public static void restoreInputExpansionParams(IKnowledgeArtifactAdapter rootAdapter) {
22+
var rootResource = (DomainResource) rootAdapter.get();
23+
var inputExpansionParamsExtension = rootResource.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS);
24+
if (inputExpansionParamsExtension == null) {
25+
return;
26+
}
27+
28+
var reference = ((Reference) inputExpansionParamsExtension.getValue()).getReference();
29+
var inputExpansionParams = rootResource.getContained().stream()
30+
.filter(contained -> reference.equals("#" + contained.getId()))
31+
.filter(Parameters.class::isInstance)
32+
.map(Parameters.class::cast)
33+
.findFirst();
34+
35+
// exp-params is set if input-exp-params is actually found
36+
inputExpansionParams.ifPresent(inputParams -> {
37+
var expansionParams =
38+
(Parameters) rootAdapter.getExpansionParameters().orElseThrow();
39+
expansionParams.setParameter(new ArrayList<>(inputParams.getParameter()));
40+
});
41+
42+
// cleanup of input-exp-params
43+
rootResource.getContained().removeIf(contained -> reference.equals("#" + contained.getId()));
44+
rootResource.getExtension().removeIf(ext -> Constants.CQF_INPUT_EXPANSION_PARAMETERS.equals(ext.getUrl()));
45+
}
46+
}

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/dstu3/DraftVisitorTests.java

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,25 @@
1919
import java.util.Optional;
2020
import org.hl7.fhir.dstu3.model.Bundle;
2121
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
22+
import org.hl7.fhir.dstu3.model.CodeType;
2223
import org.hl7.fhir.dstu3.model.Enumerations;
24+
import org.hl7.fhir.dstu3.model.Extension;
2325
import org.hl7.fhir.dstu3.model.IdType;
2426
import org.hl7.fhir.dstu3.model.Library;
2527
import org.hl7.fhir.dstu3.model.Parameters;
2628
import org.hl7.fhir.dstu3.model.Period;
2729
import org.hl7.fhir.dstu3.model.PlanDefinition;
30+
import org.hl7.fhir.dstu3.model.Reference;
2831
import org.hl7.fhir.dstu3.model.RelatedArtifact;
2932
import org.hl7.fhir.dstu3.model.StringType;
33+
import org.hl7.fhir.dstu3.model.UriType;
3034
import org.hl7.fhir.dstu3.model.ValueSet;
3135
import org.junit.jupiter.api.BeforeEach;
3236
import org.junit.jupiter.api.Test;
3337
import org.opencds.cqf.fhir.cr.visitor.DraftVisitor;
38+
import org.opencds.cqf.fhir.cr.visitor.ReleaseVisitor;
3439
import org.opencds.cqf.fhir.utility.Canonicals;
40+
import org.opencds.cqf.fhir.utility.Constants;
3541
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter;
3642
import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter;
3743
import org.opencds.cqf.fhir.utility.adapter.dstu3.AdapterFactory;
@@ -239,4 +245,170 @@ void draftOperation_version_format_test() {
239245
assertNull(versionException, "Non-semver version '" + version + "' should not throw for format");
240246
}
241247
}
248+
249+
private static Parameters getContainedParameters(Library library, String reference) {
250+
return (Parameters) library.getContained().stream()
251+
.filter(contained -> contained.getId().equals(reference.substring(1)))
252+
.findFirst()
253+
.orElse(null);
254+
}
255+
256+
@Test
257+
void draft_should_restore_authored_expansion_params_after_release() {
258+
var bundle = (Bundle) jsonParser.parseResource(
259+
DraftVisitorTests.class.getResourceAsStream("Bundle-versioned-and-unversioned-dependency.json"));
260+
repo.transaction(bundle);
261+
var releaseVisitor = new ReleaseVisitor(repo);
262+
var originalLibrary = repo.read(Library.class, new IdType("Library/SpecificationLibrary"))
263+
.copy();
264+
var releaseLibraryAdapter = new AdapterFactory().createLibrary(originalLibrary.copy());
265+
var releaseParams =
266+
parameters(part("version", new StringType("1.2.3")), part("versionBehavior", new CodeType("force")));
267+
var releaseReturnBundle = (Bundle) releaseLibraryAdapter.accept(releaseVisitor, releaseParams);
268+
var maybeReleasedLib = releaseReturnBundle.getEntry().stream()
269+
.filter(entry -> entry.getResponse().getLocation().contains("Library/SpecificationLibrary"))
270+
.findFirst();
271+
assertTrue(maybeReleasedLib.isPresent());
272+
var releasedLibrary = repo.read(
273+
Library.class, new IdType(maybeReleasedLib.get().getResponse().getLocation()));
274+
275+
// Sanity check the release step actually captured the author's input separately from the
276+
// processing timen exp-params
277+
var releasedInputExt = releasedLibrary.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS);
278+
assertNotNull(releasedInputExt);
279+
var releasedInputParams =
280+
getContainedParameters(releasedLibrary, ((Reference) releasedInputExt.getValue()).getReference());
281+
assertEquals(1, releasedInputParams.getParameter().size());
282+
var releasedExpExt = releasedLibrary.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS);
283+
assertNotNull(releasedExpExt);
284+
var releasedExpParams =
285+
getContainedParameters(releasedLibrary, ((Reference) releasedExpExt.getValue()).getReference());
286+
assertEquals(3, releasedExpParams.getParameter().size());
287+
288+
// Now draft the released library and confirm exp-params is reset to the 1 authored
289+
// parameter, and input-exp-params (and its extension) are gone
290+
var draftLibraryAdapter = new AdapterFactory().createLibrary(releasedLibrary.copy());
291+
var draftVisitor = new DraftVisitor(repo);
292+
var draftParams = parameters(part("version", new StringType("1.2.4")));
293+
var draftReturnBundle = (Bundle) draftLibraryAdapter.accept(draftVisitor, draftParams);
294+
var maybeDraftLib = draftReturnBundle.getEntry().stream()
295+
.filter(entry -> entry.getResponse().getLocation().contains("Library"))
296+
.map(entry ->
297+
repo.read(Library.class, new IdType(entry.getResponse().getLocation())))
298+
.filter(lib -> originalLibrary.getUrl().equals(lib.getUrl()))
299+
.findFirst();
300+
assertTrue(maybeDraftLib.isPresent());
301+
var draftLibrary = maybeDraftLib.get();
302+
303+
assertNull(draftLibrary.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS));
304+
assertNull(getContainedParameters(draftLibrary, "#input-exp-params"));
305+
var draftExpExt = draftLibrary.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS);
306+
assertNotNull(draftExpExt);
307+
var draftExpParams = getContainedParameters(draftLibrary, ((Reference) draftExpExt.getValue()).getReference());
308+
assertEquals(1, draftExpParams.getParameter().size());
309+
assertEquals(
310+
"http://loinc.org|2.76",
311+
((UriType) draftExpParams.getParameter().get(0).getValue()).getValue());
312+
}
313+
314+
@Test
315+
void restoreInputExpansionParams_copies_authored_params_and_cleans_up() {
316+
var library = new Library();
317+
library.setId("test-library");
318+
319+
var inputExpParams = new Parameters();
320+
inputExpParams.setId("input-exp-params");
321+
inputExpParams.addParameter().setName("authored-param").setValue(new StringType("authored-value"));
322+
library.addContained(inputExpParams);
323+
library.addExtension(
324+
new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params")));
325+
326+
var expParams = new Parameters();
327+
expParams.setId("exp-params");
328+
expParams.addParameter().setName("system-version").setValue(new StringType("http://example.com|1.0.0"));
329+
expParams.addParameter().setName("system-version").setValue(new StringType("http://example.com/other|2.0.0"));
330+
library.addContained(expParams);
331+
library.addExtension(new Extension(Constants.CQF_EXPANSION_PARAMETERS, new Reference("#exp-params")));
332+
333+
var adapter = new AdapterFactory().createLibrary(library);
334+
org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter);
335+
336+
// assert input-exp-params contained element and extension have been removed
337+
assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS));
338+
assertNull(getContainedParameters(library, "#input-exp-params"));
339+
340+
// assert params from input-exp-params have been copied to exp-params
341+
var restoredExpParamsExt = library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS);
342+
assertNotNull(restoredExpParamsExt);
343+
var restoredExpParams =
344+
getContainedParameters(library, ((Reference) restoredExpParamsExt.getValue()).getReference());
345+
assertEquals(1, restoredExpParams.getParameter().size());
346+
assertEquals(
347+
"authored-value",
348+
((StringType) restoredExpParams.getParameter().get(0).getValue()).getValue());
349+
}
350+
351+
@Test
352+
void restoreInputExpansionParams_resets_to_empty_when_author_specified_none() {
353+
var library = new Library();
354+
library.setId("test-library");
355+
356+
// Author's captured input had zero parameters
357+
var inputExpParams = new Parameters();
358+
inputExpParams.setId("input-exp-params");
359+
library.addContained(inputExpParams);
360+
library.addExtension(
361+
new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params")));
362+
363+
// exp-params has processing-time entries which should be overwritten
364+
var expParams = new Parameters();
365+
expParams.setId("exp-params");
366+
expParams.addParameter().setName("system-version").setValue(new StringType("http://example.com|1.0.0"));
367+
library.addContained(expParams);
368+
library.addExtension(new Extension(Constants.CQF_EXPANSION_PARAMETERS, new Reference("#exp-params")));
369+
370+
var adapter = new AdapterFactory().createLibrary(library);
371+
org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter);
372+
373+
// assert input-exp-params contained element and extension have been removed
374+
assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS));
375+
assertNull(getContainedParameters(library, "#input-exp-params"));
376+
377+
// assert empty params from input-exp-params have been copied to exp-params
378+
var restoredExpParamsExt = library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS);
379+
assertNotNull(restoredExpParamsExt);
380+
var restoredExpParams =
381+
getContainedParameters(library, ((Reference) restoredExpParamsExt.getValue()).getReference());
382+
assertTrue(restoredExpParams.getParameter().isEmpty());
383+
}
384+
385+
@Test
386+
void restoreInputExpansionParams_removes_dangling_extension_when_contained_resource_missing() {
387+
var library = new Library();
388+
library.setId("test-library");
389+
// Extension references a contained resource that was never actually added
390+
library.addExtension(
391+
new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params")));
392+
393+
var adapter = new AdapterFactory().createLibrary(library);
394+
org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter);
395+
396+
// assert dangling extension was removed
397+
assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS));
398+
// Nothing to restore from, so exp-params should never have been created
399+
assertNull(library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS));
400+
assertTrue(library.getContained().isEmpty());
401+
}
402+
403+
@Test
404+
void restoreInputExpansionParams_is_noop_when_no_extension_present() {
405+
var library = new Library();
406+
library.setId("test-library");
407+
408+
var adapter = new AdapterFactory().createLibrary(library);
409+
org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter);
410+
411+
assertTrue(library.getExtension().isEmpty());
412+
assertTrue(library.getContained().isEmpty());
413+
}
242414
}

0 commit comments

Comments
 (0)