diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/DraftVisitor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/DraftVisitor.java index 3524ac2857..4aba305c2c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/DraftVisitor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/DraftVisitor.java @@ -51,6 +51,15 @@ public IBase visit(IKnowledgeArtifactAdapter adapter, IBaseParameters draftParam adapter.setExtension(removeReleaseLabelAndDescription); // remove approval date adapter.setApprovalDate(null); + // reset expansion parameters to the author-specified input, in preparation for future releases + switch (repository.fhirContext().getVersion().getVersion()) { + case DSTU3 -> org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter); + case R4 -> org.opencds.cqf.fhir.cr.visitor.r4.DraftVisitor.restoreInputExpansionParams(adapter); + case R5 -> org.opencds.cqf.fhir.cr.visitor.r5.DraftVisitor.restoreInputExpansionParams(adapter); + default -> + throw new UnprocessableEntityException("Unsupported FHIR version: " + + repository.fhirContext().getVersion().getVersion()); + } // new draft version String draftVersion = version + "-draft"; String draftVersionUrl = Canonicals.getUrl(adapter.getUrl()) + "|" + draftVersion; diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/dstu3/DraftVisitor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/dstu3/DraftVisitor.java new file mode 100644 index 0000000000..fc21b23929 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/dstu3/DraftVisitor.java @@ -0,0 +1,46 @@ +package org.opencds.cqf.fhir.cr.visitor.dstu3; + +import java.util.ArrayList; +import org.hl7.fhir.dstu3.model.DomainResource; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.Reference; +import org.opencds.cqf.fhir.cr.visitor.r4.ReleaseVisitor; +import org.opencds.cqf.fhir.utility.Constants; +import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; + +public class DraftVisitor { + + private DraftVisitor() {} + + /** + * Reverses {@link ReleaseVisitor#captureInputExpansionParams}. Restores the author-specified + * input-exp-params back onto exp-params (overwriting any processing-time values), then removes the + * input-exp-params contained resource and the extension referencing it. No-op if the artifact does + * not have an input expansion parameters extension. + */ + public static void restoreInputExpansionParams(IKnowledgeArtifactAdapter rootAdapter) { + var rootResource = (DomainResource) rootAdapter.get(); + var inputExpansionParamsExtension = rootResource.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS); + if (inputExpansionParamsExtension == null) { + return; + } + + var reference = ((Reference) inputExpansionParamsExtension.getValue()).getReference(); + var inputExpansionParams = rootResource.getContained().stream() + .filter(contained -> reference.equals("#" + contained.getId())) + .filter(Parameters.class::isInstance) + .map(Parameters.class::cast) + .findFirst(); + + // exp-params is set if input-exp-params is actually found + inputExpansionParams.ifPresent(inputParams -> { + var expansionParams = + (Parameters) rootAdapter.getExpansionParameters().orElseThrow(); + expansionParams.setParameter(new ArrayList<>(inputParams.getParameter())); + }); + + // cleanup of input-exp-params + rootResource.getContained().removeIf(contained -> reference.equals("#" + contained.getId())); + rootResource.getExtension().removeIf(ext -> Constants.CQF_INPUT_EXPANSION_PARAMETERS.equals(ext.getUrl())); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/r4/DraftVisitor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/r4/DraftVisitor.java new file mode 100644 index 0000000000..d2102fa4e1 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/r4/DraftVisitor.java @@ -0,0 +1,45 @@ +package org.opencds.cqf.fhir.cr.visitor.r4; + +import java.util.ArrayList; +import org.hl7.fhir.r4.model.DomainResource; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Reference; +import org.opencds.cqf.fhir.utility.Constants; +import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; + +public class DraftVisitor { + + private DraftVisitor() {} + + /** + * Reverses {@link ReleaseVisitor#captureInputExpansionParams}. Restores the author-specified + * input-exp-params back onto exp-params (overwriting any processing-time values), then removes the + * input-exp-params contained resource and the extension referencing it. No-op if the artifact does + * not have an input expansion parameters extension. + */ + public static void restoreInputExpansionParams(IKnowledgeArtifactAdapter rootAdapter) { + var rootResource = (DomainResource) rootAdapter.get(); + var inputExpansionParamsExtension = rootResource.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS); + if (inputExpansionParamsExtension == null) { + return; + } + + var reference = ((Reference) inputExpansionParamsExtension.getValue()).getReference(); + var inputExpansionParams = rootResource.getContained().stream() + .filter(contained -> reference.equals("#" + contained.getId())) + .filter(Parameters.class::isInstance) + .map(Parameters.class::cast) + .findFirst(); + + // exp-params is set if input-exp-params is actually found + inputExpansionParams.ifPresent(inputParams -> { + var expansionParams = + (Parameters) rootAdapter.getExpansionParameters().orElseThrow(); + expansionParams.setParameter(new ArrayList<>(inputParams.getParameter())); + }); + + // cleanup of input-exp-params + rootResource.getContained().removeIf(contained -> reference.equals("#" + contained.getId())); + rootResource.getExtension().removeIf(ext -> Constants.CQF_INPUT_EXPANSION_PARAMETERS.equals(ext.getUrl())); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/r5/DraftVisitor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/r5/DraftVisitor.java new file mode 100644 index 0000000000..256983fcfc --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/r5/DraftVisitor.java @@ -0,0 +1,46 @@ +package org.opencds.cqf.fhir.cr.visitor.r5; + +import java.util.ArrayList; +import org.hl7.fhir.r5.model.DomainResource; +import org.hl7.fhir.r5.model.Parameters; +import org.hl7.fhir.r5.model.Reference; +import org.opencds.cqf.fhir.cr.visitor.r4.ReleaseVisitor; +import org.opencds.cqf.fhir.utility.Constants; +import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; + +public class DraftVisitor { + + private DraftVisitor() {} + + /** + * Reverses {@link ReleaseVisitor#captureInputExpansionParams}. Restores the author-specified + * input-exp-params back onto exp-params (overwriting any processing-time values), then removes the + * input-exp-params contained resource and the extension referencing it. No-op if the artifact does + * not have an input expansion parameters extension. + */ + public static void restoreInputExpansionParams(IKnowledgeArtifactAdapter rootAdapter) { + var rootResource = (DomainResource) rootAdapter.get(); + var inputExpansionParamsExtension = rootResource.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS); + if (inputExpansionParamsExtension == null) { + return; + } + + var reference = ((Reference) inputExpansionParamsExtension.getValue()).getReference(); + var inputExpansionParams = rootResource.getContained().stream() + .filter(contained -> reference.equals("#" + contained.getId())) + .filter(Parameters.class::isInstance) + .map(Parameters.class::cast) + .findFirst(); + + // exp-params is set if input-exp-params is actually found + inputExpansionParams.ifPresent(inputParams -> { + var expansionParams = + (Parameters) rootAdapter.getExpansionParameters().orElseThrow(); + expansionParams.setParameter(new ArrayList<>(inputParams.getParameter())); + }); + + // cleanup of input-exp-params + rootResource.getContained().removeIf(contained -> reference.equals("#" + contained.getId())); + rootResource.getExtension().removeIf(ext -> Constants.CQF_INPUT_EXPANSION_PARAMETERS.equals(ext.getUrl())); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/dstu3/DraftVisitorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/dstu3/DraftVisitorTests.java index 118e1ad303..ae91ba0e0a 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/dstu3/DraftVisitorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/dstu3/DraftVisitorTests.java @@ -19,19 +19,25 @@ import java.util.Optional; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.dstu3.model.CodeType; import org.hl7.fhir.dstu3.model.Enumerations; +import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.Library; import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Period; import org.hl7.fhir.dstu3.model.PlanDefinition; +import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.dstu3.model.RelatedArtifact; import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.dstu3.model.ValueSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.visitor.DraftVisitor; +import org.opencds.cqf.fhir.cr.visitor.ReleaseVisitor; import org.opencds.cqf.fhir.utility.Canonicals; +import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; import org.opencds.cqf.fhir.utility.adapter.dstu3.AdapterFactory; @@ -239,4 +245,170 @@ void draftOperation_version_format_test() { assertNull(versionException, "Non-semver version '" + version + "' should not throw for format"); } } + + private static Parameters getContainedParameters(Library library, String reference) { + return (Parameters) library.getContained().stream() + .filter(contained -> contained.getId().equals(reference.substring(1))) + .findFirst() + .orElse(null); + } + + @Test + void draft_should_restore_authored_expansion_params_after_release() { + var bundle = (Bundle) jsonParser.parseResource( + DraftVisitorTests.class.getResourceAsStream("Bundle-versioned-and-unversioned-dependency.json")); + repo.transaction(bundle); + var releaseVisitor = new ReleaseVisitor(repo); + var originalLibrary = repo.read(Library.class, new IdType("Library/SpecificationLibrary")) + .copy(); + var releaseLibraryAdapter = new AdapterFactory().createLibrary(originalLibrary.copy()); + var releaseParams = + parameters(part("version", new StringType("1.2.3")), part("versionBehavior", new CodeType("force"))); + var releaseReturnBundle = (Bundle) releaseLibraryAdapter.accept(releaseVisitor, releaseParams); + var maybeReleasedLib = releaseReturnBundle.getEntry().stream() + .filter(entry -> entry.getResponse().getLocation().contains("Library/SpecificationLibrary")) + .findFirst(); + assertTrue(maybeReleasedLib.isPresent()); + var releasedLibrary = repo.read( + Library.class, new IdType(maybeReleasedLib.get().getResponse().getLocation())); + + // Sanity check the release step actually captured the author's input separately from the + // processing timen exp-params + var releasedInputExt = releasedLibrary.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS); + assertNotNull(releasedInputExt); + var releasedInputParams = + getContainedParameters(releasedLibrary, ((Reference) releasedInputExt.getValue()).getReference()); + assertEquals(1, releasedInputParams.getParameter().size()); + var releasedExpExt = releasedLibrary.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(releasedExpExt); + var releasedExpParams = + getContainedParameters(releasedLibrary, ((Reference) releasedExpExt.getValue()).getReference()); + assertEquals(3, releasedExpParams.getParameter().size()); + + // Now draft the released library and confirm exp-params is reset to the 1 authored + // parameter, and input-exp-params (and its extension) are gone + var draftLibraryAdapter = new AdapterFactory().createLibrary(releasedLibrary.copy()); + var draftVisitor = new DraftVisitor(repo); + var draftParams = parameters(part("version", new StringType("1.2.4"))); + var draftReturnBundle = (Bundle) draftLibraryAdapter.accept(draftVisitor, draftParams); + var maybeDraftLib = draftReturnBundle.getEntry().stream() + .filter(entry -> entry.getResponse().getLocation().contains("Library")) + .map(entry -> + repo.read(Library.class, new IdType(entry.getResponse().getLocation()))) + .filter(lib -> originalLibrary.getUrl().equals(lib.getUrl())) + .findFirst(); + assertTrue(maybeDraftLib.isPresent()); + var draftLibrary = maybeDraftLib.get(); + + assertNull(draftLibrary.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(getContainedParameters(draftLibrary, "#input-exp-params")); + var draftExpExt = draftLibrary.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(draftExpExt); + var draftExpParams = getContainedParameters(draftLibrary, ((Reference) draftExpExt.getValue()).getReference()); + assertEquals(1, draftExpParams.getParameter().size()); + assertEquals( + "http://loinc.org|2.76", + ((UriType) draftExpParams.getParameter().get(0).getValue()).getValue()); + } + + @Test + void restoreInputExpansionParams_copies_authored_params_and_cleans_up() { + var library = new Library(); + library.setId("test-library"); + + var inputExpParams = new Parameters(); + inputExpParams.setId("input-exp-params"); + inputExpParams.addParameter().setName("authored-param").setValue(new StringType("authored-value")); + library.addContained(inputExpParams); + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + var expParams = new Parameters(); + expParams.setId("exp-params"); + expParams.addParameter().setName("system-version").setValue(new StringType("http://example.com|1.0.0")); + expParams.addParameter().setName("system-version").setValue(new StringType("http://example.com/other|2.0.0")); + library.addContained(expParams); + library.addExtension(new Extension(Constants.CQF_EXPANSION_PARAMETERS, new Reference("#exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert input-exp-params contained element and extension have been removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(getContainedParameters(library, "#input-exp-params")); + + // assert params from input-exp-params have been copied to exp-params + var restoredExpParamsExt = library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(restoredExpParamsExt); + var restoredExpParams = + getContainedParameters(library, ((Reference) restoredExpParamsExt.getValue()).getReference()); + assertEquals(1, restoredExpParams.getParameter().size()); + assertEquals( + "authored-value", + ((StringType) restoredExpParams.getParameter().get(0).getValue()).getValue()); + } + + @Test + void restoreInputExpansionParams_resets_to_empty_when_author_specified_none() { + var library = new Library(); + library.setId("test-library"); + + // Author's captured input had zero parameters + var inputExpParams = new Parameters(); + inputExpParams.setId("input-exp-params"); + library.addContained(inputExpParams); + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + // exp-params has processing-time entries which should be overwritten + var expParams = new Parameters(); + expParams.setId("exp-params"); + expParams.addParameter().setName("system-version").setValue(new StringType("http://example.com|1.0.0")); + library.addContained(expParams); + library.addExtension(new Extension(Constants.CQF_EXPANSION_PARAMETERS, new Reference("#exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert input-exp-params contained element and extension have been removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(getContainedParameters(library, "#input-exp-params")); + + // assert empty params from input-exp-params have been copied to exp-params + var restoredExpParamsExt = library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(restoredExpParamsExt); + var restoredExpParams = + getContainedParameters(library, ((Reference) restoredExpParamsExt.getValue()).getReference()); + assertTrue(restoredExpParams.getParameter().isEmpty()); + } + + @Test + void restoreInputExpansionParams_removes_dangling_extension_when_contained_resource_missing() { + var library = new Library(); + library.setId("test-library"); + // Extension references a contained resource that was never actually added + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert dangling extension was removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + // Nothing to restore from, so exp-params should never have been created + assertNull(library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS)); + assertTrue(library.getContained().isEmpty()); + } + + @Test + void restoreInputExpansionParams_is_noop_when_no_extension_present() { + var library = new Library(); + library.setId("test-library"); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.dstu3.DraftVisitor.restoreInputExpansionParams(adapter); + + assertTrue(library.getExtension().isEmpty()); + assertTrue(library.getContained().isEmpty()); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r4/DraftVisitorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r4/DraftVisitorTests.java index 00bac29ef2..86ba0519e5 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r4/DraftVisitorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r4/DraftVisitorTests.java @@ -19,19 +19,25 @@ import java.util.Optional; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Period; import org.hl7.fhir.r4.model.PlanDefinition; +import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.RelatedArtifact; import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.visitor.DraftVisitor; +import org.opencds.cqf.fhir.cr.visitor.ReleaseVisitor; import org.opencds.cqf.fhir.utility.Canonicals; +import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactVisitor; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; @@ -235,4 +241,164 @@ void draftOperation_version_format_test() { assertNull(versionException, "Non-semver version '" + version + "' should not throw for format"); } } + + @Test + void draft_should_restore_authored_expansion_params_after_release() { + var bundle = (Bundle) jsonParser.parseResource( + DraftVisitorTests.class.getResourceAsStream("Bundle-versioned-and-unversioned-dependency.json")); + repo.transaction(bundle); + var releaseVisitor = new ReleaseVisitor(repo); + var originalLibrary = repo.read(Library.class, new IdType("Library/SpecificationLibrary")) + .copy(); + var releaseLibraryAdapter = new AdapterFactory().createLibrary(originalLibrary.copy()); + var releaseParams = + parameters(part("version", new StringType("1.2.3")), part("versionBehavior", new CodeType("force"))); + var releaseReturnBundle = (Bundle) releaseLibraryAdapter.accept(releaseVisitor, releaseParams); + var maybeReleasedLib = releaseReturnBundle.getEntry().stream() + .filter(entry -> entry.getResponse().getLocation().contains("Library/SpecificationLibrary")) + .findFirst(); + assertTrue(maybeReleasedLib.isPresent()); + var releasedLibrary = repo.read( + Library.class, new IdType(maybeReleasedLib.get().getResponse().getLocation())); + + // Sanity check the release step actually captured the author's input separately from the + // processing timen exp-params + var releasedInputExt = releasedLibrary.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS); + assertNotNull(releasedInputExt); + var releasedInputParams = + (Parameters) releasedLibrary.getContained(((Reference) releasedInputExt.getValue()).getReference()); + assertEquals(1, releasedInputParams.getParameter().size()); + var releasedExpExt = releasedLibrary.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(releasedExpExt); + var releasedExpParams = + (Parameters) releasedLibrary.getContained(((Reference) releasedExpExt.getValue()).getReference()); + assertEquals(3, releasedExpParams.getParameter().size()); + + // Now draft the released library and confirm exp-params is reset to the 1 authored + // parameter, and input-exp-params (and its extension) are gone + var draftLibraryAdapter = new AdapterFactory().createLibrary(releasedLibrary.copy()); + IKnowledgeArtifactVisitor draftVisitor = new DraftVisitor(repo); + var draftParams = parameters(part("version", new StringType("1.2.4"))); + var draftReturnBundle = (Bundle) draftLibraryAdapter.accept(draftVisitor, draftParams); + var maybeDraftLib = draftReturnBundle.getEntry().stream() + .filter(entry -> entry.getResponse().getLocation().contains("Library")) + .map(entry -> + repo.read(Library.class, new IdType(entry.getResponse().getLocation()))) + .filter(lib -> originalLibrary.getUrl().equals(lib.getUrl())) + .findFirst(); + assertTrue(maybeDraftLib.isPresent()); + var draftLibrary = maybeDraftLib.get(); + + assertNull(draftLibrary.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(draftLibrary.getContained("#input-exp-params")); + var draftExpExt = draftLibrary.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(draftExpExt); + var draftExpParams = + (Parameters) draftLibrary.getContained(((Reference) draftExpExt.getValue()).getReference()); + assertEquals(1, draftExpParams.getParameter().size()); + assertEquals( + "http://loinc.org|2.76", + ((UriType) draftExpParams.getParameter().get(0).getValue()).getValue()); + } + + @Test + void restoreInputExpansionParams_copies_authored_params_and_cleans_up() { + var library = new Library(); + library.setId("test-library"); + + var inputExpParams = new Parameters(); + inputExpParams.setId("input-exp-params"); + inputExpParams.addParameter("authored-param", new StringType("authored-value")); + library.addContained(inputExpParams); + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + var expParams = new Parameters(); + expParams.setId("exp-params"); + expParams.addParameter("system-version", new StringType("http://example.com|1.0.0")); + expParams.addParameter("system-version", new StringType("http://example.com/other|2.0.0")); + library.addContained(expParams); + library.addExtension(new Extension(Constants.CQF_EXPANSION_PARAMETERS, new Reference("#exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.r4.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert input-exp-params contained element and extension have been removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(library.getContained("#input-exp-params")); + + // assert params from input-exp-params have been copied to exp-params + var restoredExpParamsExt = library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(restoredExpParamsExt); + var restoredExpParams = + (Parameters) library.getContained(((Reference) restoredExpParamsExt.getValue()).getReference()); + assertEquals(1, restoredExpParams.getParameter().size()); + assertEquals( + "authored-value", + ((StringType) restoredExpParams.getParameter().get(0).getValue()).getValue()); + } + + @Test + void restoreInputExpansionParams_resets_to_empty_when_author_specified_none() { + var library = new Library(); + library.setId("test-library"); + + // input-exp-params is present no params were authored + var inputExpParams = new Parameters(); + inputExpParams.setId("input-exp-params"); + library.addContained(inputExpParams); + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + // exp-params has processing-time entries which should be overwritten + var expParams = new Parameters(); + expParams.setId("exp-params"); + expParams.addParameter("system-version", new StringType("http://example.com|1.0.0")); + library.addContained(expParams); + library.addExtension(new Extension(Constants.CQF_EXPANSION_PARAMETERS, new Reference("#exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.r4.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert input-exp-params contained element and extension have been removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(library.getContained("#input-exp-params")); + + // assert empty params from input-exp-params have been copied to exp-params + var restoredExpParamsExt = library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(restoredExpParamsExt); + var restoredExpParams = + (Parameters) library.getContained(((Reference) restoredExpParamsExt.getValue()).getReference()); + assertTrue(restoredExpParams.getParameter().isEmpty()); + } + + @Test + void restoreInputExpansionParams_removes_dangling_extension_when_contained_resource_missing() { + var library = new Library(); + library.setId("test-library"); + // Extension references a contained resource that was never actually added + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.r4.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert dangling extension was removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + // Nothing to restore from, so exp-params should never have been created + assertNull(library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS)); + assertTrue(library.getContained().isEmpty()); + } + + @Test + void restoreInputExpansionParams_is_noop_when_no_extension_present() { + var library = new Library(); + library.setId("test-library"); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.r4.DraftVisitor.restoreInputExpansionParams(adapter); + + assertTrue(library.getExtension().isEmpty()); + assertTrue(library.getContained().isEmpty()); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r5/DraftVisitorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r5/DraftVisitorTests.java index f1346b773a..98bbec7655 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r5/DraftVisitorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r5/DraftVisitorTests.java @@ -19,19 +19,25 @@ import java.util.Optional; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r5.model.CodeType; import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.IdType; import org.hl7.fhir.r5.model.Library; import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.Period; import org.hl7.fhir.r5.model.PlanDefinition; +import org.hl7.fhir.r5.model.Reference; import org.hl7.fhir.r5.model.RelatedArtifact; import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.model.ValueSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.visitor.DraftVisitor; +import org.opencds.cqf.fhir.cr.visitor.ReleaseVisitor; import org.opencds.cqf.fhir.utility.Canonicals; +import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactVisitor; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; @@ -235,4 +241,164 @@ void draftOperation_version_format_test() { assertNull(versionException, "Non-semver version '" + version + "' should not throw for format"); } } + + @Test + void draft_should_restore_authored_expansion_params_after_release() { + var bundle = (Bundle) jsonParser.parseResource( + DraftVisitorTests.class.getResourceAsStream("Bundle-versioned-and-unversioned-dependency.json")); + repo.transaction(bundle); + var releaseVisitor = new ReleaseVisitor(repo); + var originalLibrary = repo.read(Library.class, new IdType("Library/SpecificationLibrary")) + .copy(); + var releaseLibraryAdapter = new AdapterFactory().createLibrary(originalLibrary.copy()); + var releaseParams = + parameters(part("version", new StringType("1.2.3")), part("versionBehavior", new CodeType("force"))); + var releaseReturnBundle = (Bundle) releaseLibraryAdapter.accept(releaseVisitor, releaseParams); + var maybeReleasedLib = releaseReturnBundle.getEntry().stream() + .filter(entry -> entry.getResponse().getLocation().contains("Library/SpecificationLibrary")) + .findFirst(); + assertTrue(maybeReleasedLib.isPresent()); + var releasedLibrary = repo.read( + Library.class, new IdType(maybeReleasedLib.get().getResponse().getLocation())); + + // Sanity check the release step actually captured the author's input separately from the + // processing timen exp-params + var releasedInputExt = releasedLibrary.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS); + assertNotNull(releasedInputExt); + var releasedInputParams = + (Parameters) releasedLibrary.getContained(((Reference) releasedInputExt.getValue()).getReference()); + assertEquals(1, releasedInputParams.getParameter().size()); + var releasedExpExt = releasedLibrary.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(releasedExpExt); + var releasedExpParams = + (Parameters) releasedLibrary.getContained(((Reference) releasedExpExt.getValue()).getReference()); + assertEquals(3, releasedExpParams.getParameter().size()); + + // Now draft the released library and confirm exp-params is reset to the 1 authored + // parameter, and input-exp-params (and its extension) are gone + var draftLibraryAdapter = new AdapterFactory().createLibrary(releasedLibrary.copy()); + IKnowledgeArtifactVisitor draftVisitor = new DraftVisitor(repo); + var draftParams = parameters(part("version", new StringType("1.2.4"))); + var draftReturnBundle = (Bundle) draftLibraryAdapter.accept(draftVisitor, draftParams); + var maybeDraftLib = draftReturnBundle.getEntry().stream() + .filter(entry -> entry.getResponse().getLocation().contains("Library")) + .map(entry -> + repo.read(Library.class, new IdType(entry.getResponse().getLocation()))) + .filter(lib -> originalLibrary.getUrl().equals(lib.getUrl())) + .findFirst(); + assertTrue(maybeDraftLib.isPresent()); + var draftLibrary = maybeDraftLib.get(); + + assertNull(draftLibrary.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(draftLibrary.getContained("#input-exp-params")); + var draftExpExt = draftLibrary.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(draftExpExt); + var draftExpParams = + (Parameters) draftLibrary.getContained(((Reference) draftExpExt.getValue()).getReference()); + assertEquals(1, draftExpParams.getParameter().size()); + assertEquals( + "http://loinc.org|2.76", + ((UriType) draftExpParams.getParameter().get(0).getValue()).getValue()); + } + + @Test + void restoreInputExpansionParams_copies_authored_params_and_cleans_up() { + var library = new Library(); + library.setId("test-library"); + + var inputExpParams = new Parameters(); + inputExpParams.setId("input-exp-params"); + inputExpParams.addParameter("authored-param", new StringType("authored-value")); + library.addContained(inputExpParams); + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + var expParams = new Parameters(); + expParams.setId("exp-params"); + expParams.addParameter("system-version", new StringType("http://example.com|1.0.0")); + expParams.addParameter("system-version", new StringType("http://example.com/other|2.0.0")); + library.addContained(expParams); + library.addExtension(new Extension(Constants.CQF_EXPANSION_PARAMETERS, new Reference("#exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.r5.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert input-exp-params contained element and extension have been removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(library.getContained("#input-exp-params")); + + // assert params from input-exp-params have been copied to exp-params + var restoredExpParamsExt = library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(restoredExpParamsExt); + var restoredExpParams = + (Parameters) library.getContained(((Reference) restoredExpParamsExt.getValue()).getReference()); + assertEquals(1, restoredExpParams.getParameter().size()); + assertEquals( + "authored-value", + ((StringType) restoredExpParams.getParameter().get(0).getValue()).getValue()); + } + + @Test + void restoreInputExpansionParams_resets_to_empty_when_author_specified_none() { + var library = new Library(); + library.setId("test-library"); + + // input-exp-params is present no params were authored + var inputExpParams = new Parameters(); + inputExpParams.setId("input-exp-params"); + library.addContained(inputExpParams); + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + // exp-params has processing-time entries which should be overwritten + var expParams = new Parameters(); + expParams.setId("exp-params"); + expParams.addParameter("system-version", new StringType("http://example.com|1.0.0")); + library.addContained(expParams); + library.addExtension(new Extension(Constants.CQF_EXPANSION_PARAMETERS, new Reference("#exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.r5.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert input-exp-params contained element and extension have been removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + assertNull(library.getContained("#input-exp-params")); + + // assert empty params from input-exp-params have been copied to exp-params + var restoredExpParamsExt = library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS); + assertNotNull(restoredExpParamsExt); + var restoredExpParams = + (Parameters) library.getContained(((Reference) restoredExpParamsExt.getValue()).getReference()); + assertTrue(restoredExpParams.getParameter().isEmpty()); + } + + @Test + void restoreInputExpansionParams_removes_dangling_extension_when_contained_resource_missing() { + var library = new Library(); + library.setId("test-library"); + // Extension references a contained resource that was never actually added + library.addExtension( + new Extension(Constants.CQF_INPUT_EXPANSION_PARAMETERS, new Reference("#input-exp-params"))); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.r5.DraftVisitor.restoreInputExpansionParams(adapter); + + // assert dangling extension was removed + assertNull(library.getExtensionByUrl(Constants.CQF_INPUT_EXPANSION_PARAMETERS)); + // Nothing to restore from, so exp-params should never have been created + assertNull(library.getExtensionByUrl(Constants.CQF_EXPANSION_PARAMETERS)); + assertTrue(library.getContained().isEmpty()); + } + + @Test + void restoreInputExpansionParams_is_noop_when_no_extension_present() { + var library = new Library(); + library.setId("test-library"); + + var adapter = new AdapterFactory().createLibrary(library); + org.opencds.cqf.fhir.cr.visitor.r5.DraftVisitor.restoreInputExpansionParams(adapter); + + assertTrue(library.getExtension().isEmpty()); + assertTrue(library.getContained().isEmpty()); + } }