diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/INpmRepository.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/INpmRepository.java new file mode 100644 index 0000000000..e6d82ded9c --- /dev/null +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/INpmRepository.java @@ -0,0 +1,28 @@ +package org.opencds.cqf.fhir.cql.npm; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.hl7.fhir.instance.model.api.IBaseResource; + +/** + * A "repository" backed by fhir's NPM storage. + */ +public interface INpmRepository { + + /** + * Resolve a resource by a class and url. + * - + * The url provided is optional (can be null), can contain a version (http://example.com|1.2.3), + * or not (http://example.com). + * - + * If a version is included, it must be an exact match. If it's omitted, all like-urls will be matched. + * Ie, http://example.com matches http://example.com|1.2.3 and http://example.com|2.3.4. + * - + * If no url is provided, all resources of the provided type are returned. + * + * @param clazz - the class of the resource desired (required; must not be null) + * @param url - url of the resource desired (can be null) + * @return list of resources that match the condition + */ + List resolveByUrl(@Nonnull Class clazz, String url); +} diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmBackedRepository.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmBackedRepository.java new file mode 100644 index 0000000000..501d1d8df3 --- /dev/null +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmBackedRepository.java @@ -0,0 +1,255 @@ +package org.opencds.cqf.fhir.cql.npm; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.isEmpty; + +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import jakarta.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Paths; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.cqframework.fhir.npm.NpmProcessor; +import org.cqframework.fhir.utilities.IGContext; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.jetbrains.annotations.NotNull; +import org.opencds.cqf.fhir.cql.EvaluationSettings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NpmBackedRepository implements INpmRepository { + + /** + * Internal wrapper class to hold resource and provide access to + * canonical url field (if available). + */ + private class WrappedResource { + + /** + * The resource being wrapped + */ + private final IBaseResource resource; + + /** + * The canonical url with version (if available); + * eg: http://example.com/resource|1.2.3 + */ + private String canonicalWithVersion; + + public WrappedResource(IBaseResource resource) { + this.resource = resource; + + RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resource); + + Optional urlFieldOp = def.getChildren().stream() + .filter(f -> f.getElementName().equals("url")) + .findFirst(); + + if (urlFieldOp.isPresent()) { + BaseRuntimeChildDefinition urlField = urlFieldOp.get(); + Optional valueOp = urlField.getAccessor().getFirstValueOrNull(resource); + valueOp.ifPresent(v -> { + if (v instanceof IPrimitiveType pt) { + canonicalWithVersion = pt.getValueAsString(); + } + }); + } + } + + public IBaseResource getResource() { + return resource; + } + + public boolean hasCanonicalUrl() { + return !isEmpty(canonicalWithVersion); + } + + public String getCanonicalUrl(boolean withVersion) { + String url = canonicalWithVersion; + if (!withVersion && hasVersion(url)) { + return getUrlWithoutVersion(url); + } + return url; + } + } + + private static final Logger log = LoggerFactory.getLogger(NpmBackedRepository.class); + + private final FhirContext fhirContext; + private final EvaluationSettings settings; + private NpmProcessor npmProcessor; + + // cache these because the NpmPackage holds the unparsed files + // and parsing each time is work + private final Multimap resourceType2Resource = HashMultimap.create(); + private final Multimap canonicalUrl2Resource = HashMultimap.create(); + + public NpmBackedRepository(FhirContext context, EvaluationSettings settings) { + this.fhirContext = context; + this.settings = settings; + } + + public void loadIg(String folder, String pkgName) { + ensureInitialized(); + + try { + NpmPackage pkg = NpmPackage.fromFolder(Paths.get(folder, pkgName).toString(), false); + pkg.loadAllFiles(); + npmProcessor.getPackageManager().getNpmList().add(pkg); + } catch (Exception ex) { + String msg = String.format("Could not load package %s in folder %s.", pkgName, folder); + log.error(msg, ex); + throw new RuntimeException(msg, ex); + } + } + + private void ensureInitialized() { + if (npmProcessor != null) { + return; + } + + npmProcessor = settings.getNpmProcessor(); + if (npmProcessor == null) { + // for some reason we require a 'base' sourceig... + // and this base *must be* R5 + org.hl7.fhir.r5.model.ImplementationGuide guide = new org.hl7.fhir.r5.model.ImplementationGuide(); + guide.addFhirVersion(getFhirVersionFromFhirVersion()); + guide.setName("default"); // does this matter? + IGContext igContext = new IGContext(); + igContext.setSourceIg(guide); + npmProcessor = new NpmProcessor(igContext); + settings.setNpmProcessor(npmProcessor); + } + } + + private org.hl7.fhir.r5.model.Enumerations.FHIRVersion getFhirVersionFromFhirVersion() { + FhirVersionEnum fv = fhirContext.getVersion().getVersion(); + switch (fv) { + case DSTU3 -> { + return Enumerations.FHIRVersion._3_0_0; + } + case R4 -> { + return org.hl7.fhir.r5.model.Enumerations.FHIRVersion._4_0_0; + } + case R5 -> { + return Enumerations.FHIRVersion._5_0_0; + } + default -> { + String msg = String.format("Unsupported FHIR version %s", fv.getFhirVersionString()); + log.error(msg); + throw new InvalidParameterException(msg); + } + } + } + + @Override + @SuppressWarnings("unchecked") + public List resolveByUrl(@Nonnull Class clazz, String url) { + requireNonNull(clazz, "clazz cannot be null"); + String type = clazz.getSimpleName(); + boolean hasUrl = !isEmpty(url); + + if (!resourceType2Resource.containsKey(type)) { + populateCaches(clazz, type); + } + + Collection resources = null; + if (hasUrl) { + String searchUrl = url; + boolean hasVersion = hasVersion(searchUrl); + if (hasVersion) { + searchUrl = getUrlWithoutVersion(url); + } + resources = canonicalUrl2Resource.get(searchUrl); + if (hasVersion) { + resources = resources.stream() + .filter(wr -> wr.getCanonicalUrl(true).equals(url)) + .collect(Collectors.toList()); + } + } else { + resources = resourceType2Resource.get(type); + } + + if (isListEmpty(resources)) { + return List.of(); + } + + return resources.stream().map(wr -> (T) wr.getResource()).toList(); + } + + private void populateCaches(@NotNull Class clazz, String type) { + var list = npmProcessor.getPackageManager().getNpmList(); + + for (var pkg : list) { + if (!pkg.getTypes().containsKey(type)) { + // if it doesn't have the provided resource type we can skip it + continue; + } + + List pkgResources = getResourcesFromPkg(pkg, clazz); + if (!isListEmpty(pkgResources)) { + for (T resource : pkgResources) { + WrappedResource wrappedResource = new WrappedResource(resource); + resourceType2Resource.put(type, wrappedResource); + if (wrappedResource.hasCanonicalUrl()) { + canonicalUrl2Resource.put(wrappedResource.getCanonicalUrl(false), wrappedResource); + } + } + } + } + } + + private List getResourcesFromPkg(NpmPackage pkg, Class clazz) { + IParser parser = fhirContext.newJsonParser(); + + List resources = new ArrayList<>(); + String type = clazz.getSimpleName(); + List files = pkg.getTypes().get(type); + for (String file : files) { + try (InputStream is = pkg.loadResource(file)) { + String resourceStr = new String(is.readAllBytes()); + + T resource = parser.parseResource(clazz, resourceStr); + resources.add(resource); + } catch (IOException | DataFormatException ex) { + String msg = String.format("Could not parse resource from package %s", pkg.url()); + log.error(msg, ex); + throw new RuntimeException(msg, ex); + } + } + + return resources; + } + + private boolean isListEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + private boolean hasVersion(String url) { + return url.contains("|"); + } + + private String getUrlWithoutVersion(String url) { + int barIndex = url.indexOf("|"); + if (barIndex != -1) { + return url.substring(0, barIndex); + } + return url; + } +} diff --git a/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/npm/INpmBackedRepositoryTest.java b/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/npm/INpmBackedRepositoryTest.java new file mode 100644 index 0000000000..697c23285f --- /dev/null +++ b/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/npm/INpmBackedRepositoryTest.java @@ -0,0 +1,442 @@ +package org.opencds.cqf.fhir.cql.npm; + +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.cqframework.fhir.npm.NpmPackageManager; +import org.cqframework.fhir.npm.NpmProcessor; +import org.cqframework.fhir.utilities.IGContext; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.opencds.cqf.fhir.cql.EvaluationSettings; + +public interface INpmBackedRepositoryTest { + FhirContext getFhirContext(); + + IBaseResource createLibraryResource(String name, String canonicalUrl); + + IBaseResource createMeasureResource(String canonicalUrl); + + Class getResourceClass(String resourceName); + + default String getCanonicalUrlFromResource(IBaseResource resource) { + IBase val = getFhirContext() + .getResourceDefinition(resource) + .getChildByName("url") + .getAccessor() + .getValues(resource) + .get(0); + if (val instanceof IPrimitiveType pt) { + return pt.getValueAsString(); + } + return null; + } + + @Test + default void resolveByUrl_multipleResourceBaseTest_works(@TempDir Path tempDir) throws IOException { + // setup + String name = "testIg"; + int count = 4; + String urlBase = "http://example.com/"; + + createPackage(tempDir, name, count, (Function) val -> { + if (val % 2 == 0) { + return createLibraryResource("Library" + val, String.format(urlBase + "%s/%s", "Library", val + "")); + } else { + return createMeasureResource(String.format(urlBase + "%s/%s", "Measure", val + "")); + } + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + repo.loadIg(tempDir.toString(), name); + + // test(s) + String libraryUrl = urlBase + "Library/0"; + for (String url : new String[] {libraryUrl, null}) { + List libraries = repo.resolveByUrl(getResourceClass("Library"), url); + + int expected = isEmpty(url) ? 2 : 1; + assertEquals(expected, libraries.size()); + for (IBaseResource resource : libraries) { + assertEquals("Library", resource.fhirType()); + } + } + + String measureUrl = urlBase + "Measure/1"; + for (String url : new String[] {measureUrl, null}) { + List measures = repo.resolveByUrl(getResourceClass("Measure"), url); + + int expected = isEmpty(url) ? 2 : 1; + assertEquals(expected, measures.size()); + for (IBaseResource resource : measures) { + assertEquals("Measure", resource.fhirType()); + } + } + } + + @Test + default void resolveByUrl_urlWithVersion_fetchesOnlyResourceWithExactUrl(@TempDir Path tempDir) throws IOException { + // setup + String name = "testIg"; + int count = 3; + String urlBase = "http://example.com/"; + + createPackage(tempDir, name, count, (Function) val -> { + return createLibraryResource( + "Library" + val, String.format(urlBase + "%s|%s", "Library", (val + 1) + ".0.0")); + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + + // test + repo.loadIg(tempDir.toString(), name); + String urlToSearch = urlBase + "Library|1.0.0"; + List resources = repo.resolveByUrl(getResourceClass("Library"), urlToSearch); + + // validate + assertEquals(1, resources.size()); + IBaseResource resource = resources.get(0); + assertEquals(urlToSearch, getCanonicalUrlFromResource(resource)); + } + + @Test + default void resolveByUrl_urlWithVersionNotInIG_returnsNothing(@TempDir Path tempDir) throws IOException { + // setup + String name = "testIg"; + int count = 3; + String urlBase = "http://example.com/"; + + createPackage(tempDir, name, count, (Function) val -> { + return createLibraryResource( + "Library" + val, String.format(urlBase + "%s|%s", "Library", (val + 1) + ".0.0")); + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + + // test + repo.loadIg(tempDir.toString(), name); + String urlToSearch = urlBase + "Library|1.2.3"; + List resources = repo.resolveByUrl(getResourceClass("Library"), urlToSearch); + + // validate + assertTrue(resources.isEmpty()); + } + + @Test + default void resolveByUrl_urlWithoutVersion_fetchesAllWithSimilarUrls(@TempDir Path tempDir) throws IOException { + // setup + String name = "testIg"; + int count = 3; + String urlBase = "http://example.com/"; + + createPackage(tempDir, name, count, (Function) val -> { + return createLibraryResource( + "Library" + val, String.format(urlBase + "%s|%s", "Library", (val + 1) + ".0.0")); + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + + // test + repo.loadIg(tempDir.toString(), name); + String urlToSearch = urlBase + "Library"; + List resources = repo.resolveByUrl(getResourceClass("Library"), urlToSearch); + + // verify + assertNotNull(resources); + assertEquals(3, resources.size()); + for (IBaseResource resource : resources) { + String url = getCanonicalUrlFromResource(resource); + assertTrue(url.startsWith(urlToSearch)); + } + } + + @Test + default void resolveByUrl_withUrl_returnsOnlyResourcesThatMatch(@TempDir Path tempDir) throws IOException { + // setup + String name = "testIg"; + int count = 3; + String urlBase = "http://example.com/"; + + createPackage(tempDir, name, count, (Function) val -> { + return createLibraryResource("Library" + val, String.format(urlBase + "%s/%s", "Library", val + "")); + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + + // test + repo.loadIg(tempDir.toString(), name); + String urlToSearch = urlBase + "Library/1"; + List libraries = repo.resolveByUrl(getResourceClass("Library"), urlToSearch); + + // verify + assertEquals(1, libraries.size()); + IBaseResource library = libraries.get(0); + assertEquals("Library", library.fhirType()); + String url = getCanonicalUrlFromResource(library); + assertEquals(urlToSearch, url); + } + + @Test + default void resolveByUrl_withoutUrl_returnsAllResourcesOfType(@TempDir Path tempDir) throws IOException { + // setup + String name = "testIg"; + int count = 3; + String urlBase = "http://example.com/"; + + createPackage(tempDir, name, count, (Function) val -> { + return createLibraryResource("Library" + val, String.format(urlBase + "%s/%s", "Library", val + "")); + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + + // test + repo.loadIg(tempDir.toString(), name); + List libraries = repo.resolveByUrl(getResourceClass("Library"), null); + + // verify + assertEquals(count, libraries.size()); + for (IBaseResource library : libraries) { + assertEquals("Library", library.fhirType()); + String url = getCanonicalUrlFromResource(library); + assertTrue(url.startsWith(urlBase), urlBase); + } + } + + @Test + default void resolveByUrl_noMatchingUrl_returnsNothing(@TempDir Path tempDir) throws IOException { + // setup + String name = "testIg"; + createPackage(tempDir, name, 1, (Function) val -> { + return createLibraryResource("Library" + val, "http://some.place.com/lib"); + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + + // test + repo.loadIg(tempDir.toString(), name); + List libraries = repo.resolveByUrl(getResourceClass("Library"), "http://example.com"); + + // verify + assertTrue(libraries.isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"http://some.place.com/"}) + @NullSource + default void resolveByUrl_incorrectResource_returnsNothing(String urlToSearch, @TempDir Path tempDir) + throws IOException { + // setup + String name = "testIg"; + createPackage(tempDir, name, 1, (Function) val -> { + return createLibraryResource("Library" + val, urlToSearch); + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + + // test + repo.loadIg(tempDir.toString(), name); + List questionnaires = repo.resolveByUrl(getResourceClass("Questionnaire"), urlToSearch); + + // verify + // should be no questionnaire resources at all (regardless of url) + assertTrue(questionnaires.isEmpty()); + } + + @Test + default void resolveByUrl_validUrlButResourceHasNoCanonical_returnsNothing(@TempDir Path tempDir) + throws IOException { + // setup + String name = "testIg"; + createPackage(tempDir, name, 1, (Function) val -> { + return createLibraryResource("Library" + val, null); + }); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), EvaluationSettings.getDefault()); + + // test + repo.loadIg(tempDir.toString(), name); + List libraries = repo.resolveByUrl(getResourceClass("Library"), "http://example.com"); + + // validate + assertTrue(libraries.isEmpty()); + } + + @Test + default void loadIg_nonExistentPackage_failsToLoad() { + // setup + EvaluationSettings settings = EvaluationSettings.getDefault(); + NpmProcessor processor = mock(NpmProcessor.class); + settings.setNpmProcessor(processor); + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), settings); + + // test + try { + // non-existent path + repo.loadIg("any", "thing"); + fail(); + } catch (Exception ex) { + assertTrue(ex.getMessage().contains("Could not load package"), ex.getMessage()); + } + } + + @Test + @SuppressWarnings("resource") + default void resolveByUrl_cannotReadFile_coverageTest() throws IOException { + // setup + Map> types = new HashMap<>(); + types.put("Library", List.of("someFile.json")); + + NpmPackage pkgMock = mock(NpmPackage.class); + NpmProcessor processor = mock(NpmProcessor.class); + org.hl7.fhir.r5.model.ImplementationGuide guide = new org.hl7.fhir.r5.model.ImplementationGuide(); + guide.setName("default"); + NpmPackageManager pkgMgr = new NpmPackageManager(guide); + + // when + when(processor.getPackageManager()).thenReturn(pkgMgr); + + try (MockedStatic npmPackageMock = mockStatic(NpmPackage.class)) { + npmPackageMock + .when(() -> NpmPackage.fromFolder(anyString(), anyBoolean())) + .thenReturn(pkgMock); + + EvaluationSettings settings = EvaluationSettings.getDefault(); + settings.setNpmProcessor(processor); + NpmBackedRepository repo = new NpmBackedRepository(getFhirContext(), settings); + + // when + when(pkgMock.getTypes()).thenReturn(types); + doThrow(new IOException("Exception")).when(pkgMock).loadResource(anyString()); + + // test + repo.loadIg("any", "thing"); + repo.resolveByUrl(getResourceClass("Library"), null); + fail(); + } catch (RuntimeException ex) { + assertTrue( + ex.getLocalizedMessage().contains("Could not parse resource from package"), + ex.getLocalizedMessage()); + } + } + + @Test + @SuppressWarnings("unchecked") + default void resolveByUrl_cannotParseResource_coverageTest(@TempDir Path tempDir) throws IOException { + // setup + String name = "testIg"; + createPackage(tempDir, name, 1, (Function) val -> { + return createLibraryResource("Library" + val, null); + }); + + FhirContext spy = Mockito.spy(getFhirContext()); + + // when + IParser parser = mock(IParser.class); + doReturn(parser).when(spy).newJsonParser(); + doThrow(new DataFormatException("exceptional")).when(parser).parseResource(any(Class.class), anyString()); + + // create the repo + NpmBackedRepository repo = new NpmBackedRepository(spy, EvaluationSettings.getDefault()); + + repo.loadIg(tempDir.toString(), name); + + // test + try { + repo.resolveByUrl(getResourceClass("Library"), null); + fail(); + } catch (Exception ex) { + assertTrue(ex.getLocalizedMessage().contains("Could not parse resource from package"), ex.getMessage()); + } + } + + // TODO - we might want to put this into a test util if we're going to use it over and over + private void createPackage( + Path tempDir, String pkgName, int resourceCount, Function resourceCreator) + throws IOException { + IParser parser = getFhirContext().newJsonParser(); + + // create the default guide... + // for some reason we require a 'base' sourceig... + // and this base *must be* R5 + org.hl7.fhir.r5.model.ImplementationGuide guide = new org.hl7.fhir.r5.model.ImplementationGuide(); + guide.addFhirVersion(org.hl7.fhir.r5.model.Enumerations.FHIRVersion._4_0_0); + guide.setName("default"); + IGContext igContext = new IGContext(); + igContext.setSourceIg(guide); + + // NpmManager expects: + // (tempdir)/something/package/package.json + // "version" = file://(tempdir) + + // create named file + Path igNamePath = Files.createDirectory(Paths.get(tempDir.toString(), pkgName)); + // create package path + Path packagePath = Files.createDirectory(Paths.get(igNamePath.toString(), "package")); + + for (int i = 0; i < resourceCount; i++) { + IBaseResource resource = resourceCreator.apply(i); + + // add to package + File f = new File(packagePath.toString(), String.format("resource%s.json", Integer.toString(i))); + Files.writeString(f.toPath().toAbsolutePath(), parser.encodeResourceToString(resource)); + } + + // create the package speck (package.json) + URI uri = packagePath.toUri(); + String pkgJson = String.format( + """ + { + "packageUrl": "%s", + "name": "%s", + "version": "1.0.0", + "installMode": "STORE_ONLY" + } + """, + uri, pkgName); + + File packagejson = new File(packagePath.toString(), "package.json"); + Files.writeString(packagejson.toPath(), pkgJson); + } +} diff --git a/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/npm/NpmBackedRepositoryTest.java b/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/npm/NpmBackedRepositoryTest.java new file mode 100644 index 0000000000..77c65c1018 --- /dev/null +++ b/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/npm/NpmBackedRepositoryTest.java @@ -0,0 +1,113 @@ +package org.opencds.cqf.fhir.cql.npm; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Measure; +import org.junit.jupiter.api.Nested; + +public class NpmBackedRepositoryTest { + + @Nested + public class R4Tests implements INpmBackedRepositoryTest { + private final FhirContext fhirContext = FhirContext.forR4Cached(); + + @Override + public FhirContext getFhirContext() { + return fhirContext; + } + + @Override + public IBaseResource createLibraryResource(String name, String canonicalUrl) { + Library library = new Library(); + library.setStatus(Enumerations.PublicationStatus.ACTIVE); + library.setUrl(canonicalUrl); + library.setName(name); + return library; + } + + @Override + public IBaseResource createMeasureResource(String canonicalUrl) { + Measure measure = new Measure(); + measure.setStatus(PublicationStatus.ACTIVE); + measure.setUrl(canonicalUrl); + return measure; + } + + @Override + @SuppressWarnings("unchecked") + public Class getResourceClass(String resourceName) { + return (Class) fhirContext.getResourceDefinition(resourceName).getImplementingClass(); + } + } + + @Nested + public class R5Tests implements INpmBackedRepositoryTest { + + private final FhirContext fhirContext = FhirContext.forR5Cached(); + + @Override + public FhirContext getFhirContext() { + return fhirContext; + } + + @Override + public IBaseResource createLibraryResource(String name, String canonicalUrl) { + var library = new org.hl7.fhir.r5.model.Library(); + library.setStatus(org.hl7.fhir.r5.model.Enumerations.PublicationStatus.ACTIVE); + library.setUrl(canonicalUrl); + library.setName(name); + return library; + } + + @Override + public IBaseResource createMeasureResource(String canonicalUrl) { + var measure = new org.hl7.fhir.r5.model.Measure(); + measure.setStatus(org.hl7.fhir.r5.model.Enumerations.PublicationStatus.ACTIVE); + measure.setUrl(canonicalUrl); + return measure; + } + + @Override + @SuppressWarnings("unchecked") + public Class getResourceClass(String resourceName) { + return (Class) fhirContext.getResourceDefinition(resourceName).getImplementingClass(); + } + } + + @Nested + public class Dstu3 implements INpmBackedRepositoryTest { + + private final FhirContext fhirContext = FhirContext.forDstu3Cached(); + + @Override + public FhirContext getFhirContext() { + return fhirContext; + } + + @Override + public IBaseResource createLibraryResource(String name, String canonicalUrl) { + var library = new org.hl7.fhir.dstu3.model.Library(); + library.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); + library.setUrl(canonicalUrl); + library.setName(name); + return library; + } + + @Override + public IBaseResource createMeasureResource(String canonicalUrl) { + var measure = new org.hl7.fhir.dstu3.model.Measure(); + measure.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); + measure.setUrl(canonicalUrl); + return measure; + } + + @Override + @SuppressWarnings("unchecked") + public Class getResourceClass(String resourceName) { + return (Class) fhirContext.getResourceDefinition(resourceName).getImplementingClass(); + } + } +}