diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/BaseNpmPackageLoaderInMemory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/BaseNpmPackageLoaderInMemory.java new file mode 100644 index 0000000000..ed4892ffae --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/BaseNpmPackageLoaderInMemory.java @@ -0,0 +1,166 @@ +package org.opencds.cqf.fhir.utility.npm; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.StringJoiner; +import java.util.stream.Collectors; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.utilities.npm.NpmPackage; + +public abstract class BaseNpmPackageLoaderInMemory implements NpmPackageLoader { + + public static final String FAILED_TO_LOAD_RESOURCE_TEMPLATE = "Failed to load resource: %s"; + + private final Set npmPackages; + private final NpmNamespaceManager npmNamespaceManager; + + @Override + public Optional loadNpmResource(IPrimitiveType resourceUrl) { + return npmPackages.stream() + .filter(npmPackage -> doesPackageMatch(resourceUrl, npmPackage)) + .map(npmPackage -> getResource(npmPackage, resourceUrl)) + .findFirst(); + } + + private IBaseResource getResource(NpmPackage npmPackage, IPrimitiveType resourceUrl) { + try { + return tryGetResource(npmPackage, resourceUrl); + } catch (IOException exception) { + throw new InternalErrorException( + FAILED_TO_LOAD_RESOURCE_TEMPLATE.formatted(resourceUrl.getValue()), exception); + } + } + + private IBaseResource tryGetResource(NpmPackage npmPackage, IPrimitiveType resourceUrl) throws IOException { + + final FhirContext fhirContext = getFhirContext(npmPackage); + final String resourceUrlString = resourceUrl.getValue(); + + final String[] split = resourceUrlString.split("\\|"); + + try (InputStream libraryInputStream = npmPackage.loadByCanonical(split[0])) { + return fhirContext.newJsonParser().parseResource(libraryInputStream); + } + } + + private boolean doesPackageMatch(IPrimitiveType resourceUrl, NpmPackage npmPackage) { + try { + return npmPackage.hasCanonical(resourceUrl.getValue()); + } catch (IOException exception) { + throw new InternalErrorException(FAILED_TO_LOAD_RESOURCE_TEMPLATE, exception); + } + } + + @Override + public NpmNamespaceManager getNamespaceManager() { + return npmNamespaceManager; + } + + @Nonnull + protected static Set buildNpmPackagesFromAbsolutePath(List tgzPaths) { + return tgzPaths.stream() + .map(BaseNpmPackageLoaderInMemory::getNpmPackageFromAbsolutePaths) + .collect(Collectors.toUnmodifiableSet()); + } + + @Nonnull + protected static Set buildNpmPackageFromClasspath(Class clazz, List tgzPaths) { + return tgzPaths.stream() + .map(path -> getNpmPackageFromClasspath(clazz, path)) + .collect(Collectors.toUnmodifiableSet()); + } + + @Nonnull + private static NpmPackage getNpmPackageFromAbsolutePaths(Path tgzPath) { + try (final InputStream npmStream = Files.newInputStream(tgzPath)) { + return NpmPackage.fromPackage(npmStream); + } catch (IOException exception) { + throw new InvalidRequestException(FAILED_TO_LOAD_RESOURCE_TEMPLATE.formatted(tgzPath), exception); + } + } + + @Nonnull + private static NpmPackage getNpmPackageFromClasspath(Class clazz, Path tgzClasspathPath) { + try (final InputStream simpleAlphaStream = clazz.getResourceAsStream(tgzClasspathPath.toString())) { + if (simpleAlphaStream == null) { + throw new InvalidRequestException(FAILED_TO_LOAD_RESOURCE_TEMPLATE.formatted(tgzClasspathPath)); + } + + return NpmPackage.fromPackage(simpleAlphaStream); + } catch (IOException exception) { + throw new InvalidRequestException(FAILED_TO_LOAD_RESOURCE_TEMPLATE.formatted(tgzClasspathPath), exception); + } + } + + protected BaseNpmPackageLoaderInMemory( + Set npmPackages, @Nullable NpmNamespaceManager npmNamespaceManager) { + + if (npmNamespaceManager == null) { + var namespaceInfos = npmPackages.stream() + .map(npmPackage -> new NamespaceInfo(npmPackage.name(), npmPackage.canonical())) + .toList(); + + this.npmNamespaceManager = new NpmNamespaceManagerFromList(namespaceInfos); + } else { + this.npmNamespaceManager = npmNamespaceManager; + } + + this.npmPackages = npmPackages; + } + + private FhirContext getFhirContext(NpmPackage npmPackage) { + return FhirContext.forCached(FhirVersionEnum.forVersionString(npmPackage.fhirVersion())); + } + + /** + * Meant to test various scenarios involving missing of faulty NamespaceInfo data. + */ + public static class NpmNamespaceManagerFromList implements NpmNamespaceManager { + + private final List namespaceInfos; + + public NpmNamespaceManagerFromList(List namespaceInfos) { + this.namespaceInfos = List.copyOf(namespaceInfos); + } + + @Override + public List getAllNamespaceInfos() { + return namespaceInfos; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + NpmNamespaceManagerFromList that = (NpmNamespaceManagerFromList) o; + return Objects.equals(namespaceInfos, that.namespaceInfos); + } + + @Override + public int hashCode() { + return Objects.hashCode(namespaceInfos); + } + + @Override + public String toString() { + return new StringJoiner(", ", NpmNamespaceManagerFromList.class.getSimpleName() + "[", "]") + .add("namespaceInfos=" + namespaceInfos) + .toString(); + } + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutor.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutor.java new file mode 100644 index 0000000000..b7197d46aa --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutor.java @@ -0,0 +1,24 @@ +package org.opencds.cqf.fhir.utility.npm; + +import java.util.Optional; + +/** + * This class is meant to be used from Spring configuration classes, in the case of any missing + * NpmPackageLoader bean definitions, which Spring will inject as empty Optionals. + *

+ * Helps implement a migration from the old world of FHIR/Repository based resources for Libraries, + * Measures and eventually other clinical intelligence resources (such as PlanDefinitions or + * ValueSets), and the new world where they're derived from NPM packages. + * If Spring config is missing an instance of {@link NpmPackageLoader}, then * return the default + * instance. + */ +public class NpmConfigDependencySubstitutor { + + private NpmConfigDependencySubstitutor() { + // static utility class + } + + public static NpmPackageLoader substituteNpmPackageLoaderIfEmpty(Optional optNpmPackageLoader) { + return NpmPackageLoader.getDefaultIfEmpty(optNpmPackageLoader.orElse(null)); + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmNamespaceManager.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmNamespaceManager.java new file mode 100644 index 0000000000..c983a2d076 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmNamespaceManager.java @@ -0,0 +1,16 @@ +package org.opencds.cqf.fhir.utility.npm; + +import java.util.List; +import org.hl7.cql.model.NamespaceInfo; + +/** + * Load all {@link NamespaceInfo}s capturing package ID to URL mappings associated with the NPM + * packages maintained for clinical-reasoning NPM package users to be used to resolve cross-package + * Library/CQL dependencies. See {@link NpmPackageLoader}. + */ +public interface NpmNamespaceManager { + + NpmNamespaceManager DEFAULT = List::of; + + List getAllNamespaceInfos(); +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoader.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoader.java new file mode 100644 index 0000000000..87dadd7b0f --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoader.java @@ -0,0 +1,308 @@ +package org.opencds.cqf.fhir.utility.npm; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Optional; +import org.cqframework.cql.cql2elm.LibraryManager; +import org.cqframework.cql.cql2elm.LibrarySourceProvider; +import org.hl7.cql.model.ModelIdentifier; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.cql.model.NamespaceManager; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IResourceAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FHIR version agnostic Interface for loading NPM resources including Measures, Libraries and + * potentially other qualifying resources. + *

+ * This javadoc documents the entire NPM package feature in the clinical-reasoning project. Please + * read below: + *

+ * A downstream app from clinical-reasoning will be able to maintain Measures and Libraries loaded + * from NPM packages. Such Measures and Libraries will, for those clients implementing this + * feature, no longer be maintained in {@link IRepository} storage, unlike all other FHIR resources, + * such as Patients. + *

+ * Downstream apps are responsible for loading and retrieving such packages from implementations + * of the below interface. Additionally, they must map all package IDs to package URLs via a + * List of {@link NamespaceInfo}s. This is done via the + * {@link #initNamespaceMappings(LibraryManager)}, as due to how CQL libraries are loaded, it + * won't work automatically. + *

+ * When CQL runs and calls a custom {@link LibrarySourceProvider}, it will query all NPM packages + * accessible by the backing implementation. + *

+ * The above should also work with multiple layers of includes across packages. + *

+ * This workflow is meant to be triggered by a new Measure operation provider: + * $evaluate-measure-by-url, which takes a canonical measure URL instead of a measure ID like + * $evaluate-measure. + *

+ * Example: Package with ID X and URL ... contains Measure ABC + * is associated with Library 123, which contains CQL that includes Library 456 from NPM Package + * with ID Y and URL ..., which contains both the Library and + * its CQL. When resolve the CQL include pointing to Package ID Y, the CQL engine must be able + * to read the namespace info and resolve ID Y to URL .... This + * can only be accomplished via an explicit mapping. + *

+ * Note that, depending on the implementation, there is the real possibility of Measures + * corresponding to the same canonical URL among multiple NPM packages. As such, clients who + * unintentionally add Measures with the same URL in at least two different packages may see the + * Measure they're not expecting during an $evaluate-measure-by-url, and may file production + * issues accordingly. + */ +public interface NpmPackageLoader { + Logger logger = LoggerFactory.getLogger(NpmPackageLoader.class); + + String LIBRARY_URL_TEMPLATE = "%s/Library/%s"; + String LIBRARY = "Library"; + String MEASURE = "Measure"; + + // effectively a no-op implementation + NpmPackageLoader R4_DEFAULT = new NpmPackageLoader() { + + @Override + public Optional loadNpmResource(IPrimitiveType canonicalUrl) { + return Optional.empty(); + } + + @Override + public NpmNamespaceManager getNamespaceManager() { + return NpmNamespaceManager.DEFAULT; + } + + @Override + public FhirContext getFhirContext() { + return FhirContext.forR4Cached(); + } + }; + + /** + * Query the NPM package repo for the resource corresponding to the provided resource class and + * canonical URL corresponding to the NPM package repo. + * + * @param resourceClass The expected class of the resource to be returned, which will + * be checked via instanceof before being cast and returned. + * @param canonicalResourceUrl The resource URL provided by the caller, corresponding to a + * resource contained within one of the stored NPM packages. + * @return The resource (any {@link IBaseResource}) corresponding to the URL. + */ + default Optional loadNpmResource( + Class resourceClass, IPrimitiveType canonicalResourceUrl) { + + final Optional optResource = loadNpmResource(canonicalResourceUrl); + + if (optResource.isEmpty()) { + return Optional.empty(); + } + + final IBaseResource resource = optResource.get(); + + if (!resourceClass.isInstance(resource)) { + throw new IllegalArgumentException("Expected resource to be a %s, but was a %s" + .formatted(resourceClass.getSimpleName(), resource.fhirType())); + } + + return Optional.of(resourceClass.cast(resource)); + } + + default IAdapterFactory getAdapterFactory() { + return IAdapterFactory.forFhirVersion(getFhirContext().getVersion().getVersion()); + } + + /** + * Obtain the resource corresponding to the provided type-enclosed String URL from the NPM + * package repo. Implementors must figure out how to retrieve the resource by querying one + * or more NPM packages maintained by the application. + * + * @param canonicalResourceUrl The type-enclosed String canonical URL of the resource to load. + * @return The resource corresponding to the URL, if it exists. + */ + Optional loadNpmResource(IPrimitiveType canonicalResourceUrl); + + /** + * Implementors must commit to supporting a specific FHIR version, in order to ensure that + * the correct version of a given resource is used for both the FHIR canonical URL and the + * returned resource. + * + * @return The FhirContext corresponding to the FHIR version of the NPM packages maintained + * by the application. + */ + FhirContext getFhirContext(); + + /** + * It's up to implementors to maintain the NamespaceManager that maintains the NamespaceInfos. + */ + NpmNamespaceManager getNamespaceManager(); + + /** + * Hackish: Either the downstream app injected this or we default to a NO-OP implementation. + * + * @param npmPackageLoader The NpmPackageLoader, if injected by the downstream app, + * otherwise null. + * @return Either the downstream app's NpmPackageLoader a no-op implementation. + */ + static NpmPackageLoader getDefaultIfEmpty(@Nullable NpmPackageLoader npmPackageLoader) { + return Optional.ofNullable(npmPackageLoader).orElse(NpmPackageLoader.R4_DEFAULT); + } + + /** + * Ensure the passed Library gets initialized with the NPM namespace mappings belonging + * to this instance of NpmPackageLoader. + * + * @param libraryManager from the CQL Engine being used for an evaluation + */ + default void initNamespaceMappings(LibraryManager libraryManager) { + final List allNamespaceInfos = getAllNamespaceInfos(); + final NamespaceManager namespaceManager = libraryManager.getNamespaceManager(); + + for (NamespaceInfo namespaceInfo : allNamespaceInfos) { + // if we do this more than one time it won't error out subsequent times + namespaceManager.ensureNamespaceRegistered(namespaceInfo); + } + } + + /** + * @return All NamespaceInfos to map package IDs to package URLs for all NPM Packages maintained + * for clinical-reasoning NPM package to be used to resolve cross-package Library/CQL + * dependencies. + */ + default List getAllNamespaceInfos() { + return getNamespaceManager().getAllNamespaceInfos(); + } + + default Optional findMatchingLibrary(VersionedIdentifier versionedIdentifier) { + return findLibraryFromUnrelatedNpmPackage(versionedIdentifier); + } + + default Optional findMatchingLibrary(ModelIdentifier modelIdentifier) { + return findLibraryFromUnrelatedNpmPackage(modelIdentifier); + } + + default Optional findLibraryFromUnrelatedNpmPackage(VersionedIdentifier versionedIdentifier) { + return loadLibraryByUrl(getUrl(versionedIdentifier)); + } + + default Optional findLibraryFromUnrelatedNpmPackage(ModelIdentifier modelIdentifier) { + return loadLibraryByUrl(getUrl(modelIdentifier)); + } + + /** + * @param libraryUrl The Library URL converted from a given + * withing one of the stored NPM packages. + * @return The Measure corresponding to the URL. + */ + default Optional loadLibraryByUrl(String libraryUrl) { + + return toLibraryAdapter( + loadNpmResource(getLibraryClass(), toPrimitiveType(libraryUrl)).orElse(null)); + } + + default Optional toLibraryAdapter(IBaseResource resource) { + if (resource == null) { + return Optional.empty(); + } + switch (getFhirContext().getVersion().getVersion()) { + case R4 -> { + if (!(resource instanceof org.hl7.fhir.r4.model.Library r4Library)) { + throw new IllegalArgumentException( + "Expected resource to be a Library, but was a " + resource.fhirType()); + } + return Optional.of(new org.opencds.cqf.fhir.utility.adapter.r4.LibraryAdapter(r4Library)); + } + case R5 -> { + if (!(resource instanceof org.hl7.fhir.r5.model.Library r5Library)) { + throw new IllegalArgumentException( + "Expected resource to be a Library, but was a " + resource.fhirType()); + } + return Optional.of(new org.opencds.cqf.fhir.utility.adapter.r5.LibraryAdapter(r5Library)); + } + default -> throw new IllegalStateException( + "Unsupported FHIR version: " + getFhirContext().getVersion().getVersion()); + } + } + + default IPrimitiveType toPrimitiveType(String libraryUrl) { + switch (getFhirContext().getVersion().getVersion()) { + case R4 -> { + return new org.hl7.fhir.r4.model.StringType(libraryUrl); + } + case R5 -> { + return new org.hl7.fhir.r5.model.StringType(libraryUrl); + } + default -> throw new IllegalStateException( + "Unsupported FHIR version: " + getFhirContext().getVersion().getVersion()); + } + } + + default Class getLibraryClass() { + switch (getFhirContext().getVersion().getVersion()) { + case R4 -> { + return org.hl7.fhir.r4.model.Library.class; + } + case R5 -> { + return org.hl7.fhir.r5.model.Library.class; + } + default -> throw new IllegalStateException( + "Unsupported FHIR version: " + getFhirContext().getVersion().getVersion()); + } + } + + default IResourceAdapter toResourceAdapter(IBaseResource resource) { + if (resource == null) { + return null; + } + if (FhirVersionEnum.R4 == getFhirContext().getVersion().getVersion()) { + switch (resource.fhirType()) { + case LIBRARY -> { + return new org.opencds.cqf.fhir.utility.adapter.r4.LibraryAdapter( + (org.hl7.fhir.r4.model.Library) resource); + } + case MEASURE -> { + return new org.opencds.cqf.fhir.utility.adapter.r4.MeasureAdapter( + (org.hl7.fhir.r4.model.Measure) resource); + } + default -> throw new IllegalArgumentException( + "Expected resource to be a Library or Measure, but was a " + resource.fhirType()); + } + } + + if (FhirVersionEnum.R5 == getFhirContext().getVersion().getVersion()) { + switch (resource.fhirType()) { + case LIBRARY -> { + return new org.opencds.cqf.fhir.utility.adapter.r5.LibraryAdapter( + (org.hl7.fhir.r5.model.Library) resource); + } + case MEASURE -> { + return new org.opencds.cqf.fhir.utility.adapter.r5.MeasureAdapter( + (org.hl7.fhir.r5.model.Measure) resource); + } + default -> throw new IllegalArgumentException( + "Expected resource to be a Library or Measure, but was a " + resource.fhirType()); + } + } + + throw new InvalidRequestException("Unsupported FHIR version: %s" + .formatted(getFhirContext().getVersion().getVersion().toString())); + } + + private String getUrl(VersionedIdentifier versionedIdentifier) { + // We need this case because the CQL engine will do the right thing and populate the system + // in the cross-package target case + return LIBRARY_URL_TEMPLATE.formatted(versionedIdentifier.getSystem(), versionedIdentifier.getId()); + } + + private String getUrl(ModelIdentifier modelIdentifier) { + return LIBRARY_URL_TEMPLATE.formatted(modelIdentifier.getSystem(), modelIdentifier.getId()); + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/r4/R4NpmPackageLoaderInMemory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/r4/R4NpmPackageLoaderInMemory.java new file mode 100644 index 0000000000..bea2e32903 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/r4/R4NpmPackageLoaderInMemory.java @@ -0,0 +1,65 @@ +package org.opencds.cqf.fhir.utility.npm.r4; + +import ca.uhn.fhir.context.FhirContext; +import jakarta.annotation.Nullable; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.opencds.cqf.fhir.utility.npm.BaseNpmPackageLoaderInMemory; +import org.opencds.cqf.fhir.utility.npm.NpmNamespaceManager; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; + +/** + * Simplistic implementation of {@link NpmPackageLoader} that loads NpmPackages from the classpath + * and stores {@link NpmPackage}s in a Set. This class is recommended for testing + * and NOT for production. + *

+ * Optionally uses a custom {@link NpmNamespaceManager} but can also resolve all NamespaceInfos + * by extracting them from all loaded packages at construction time. + *

+ * This is for R4 only. + */ +public class R4NpmPackageLoaderInMemory extends BaseNpmPackageLoaderInMemory implements NpmPackageLoader { + + public static R4NpmPackageLoaderInMemory fromNpmPackageAbsolutePath(List tgzPaths) { + return fromNpmPackageAbsolutePath(null, tgzPaths); + } + + public static R4NpmPackageLoaderInMemory fromNpmPackageAbsolutePath( + NpmNamespaceManager npmNamespaceManager, List tgzPaths) { + final Set npmPackages = buildNpmPackagesFromAbsolutePath(tgzPaths); + + return new R4NpmPackageLoaderInMemory(npmPackages, npmNamespaceManager); + } + + public static R4NpmPackageLoaderInMemory fromNpmPackageClasspath(Class clazz, Path... tgzPaths) { + return fromNpmPackageClasspath(null, clazz, tgzPaths); + } + + public static R4NpmPackageLoaderInMemory fromNpmPackageClasspath( + @Nullable NpmNamespaceManager npmNamespaceManager, Class clazz, Path... tgzPaths) { + return fromNpmPackageClasspath(npmNamespaceManager, clazz, Arrays.asList(tgzPaths)); + } + + public static R4NpmPackageLoaderInMemory fromNpmPackageClasspath(Class clazz, List tgzPaths) { + return fromNpmPackageClasspath(null, clazz, tgzPaths); + } + + public static R4NpmPackageLoaderInMemory fromNpmPackageClasspath( + @Nullable NpmNamespaceManager npmNamespaceManager, Class clazz, List tgzPaths) { + final Set npmPackages = buildNpmPackageFromClasspath(clazz, tgzPaths); + + return new R4NpmPackageLoaderInMemory(npmPackages, npmNamespaceManager); + } + + public R4NpmPackageLoaderInMemory(Set npmPackages, @Nullable NpmNamespaceManager npmNamespaceManager) { + super(npmPackages, npmNamespaceManager); + } + + @Override + public FhirContext getFhirContext() { + return FhirContext.forR4Cached(); + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/r5/R5NpmPackageLoaderInMemory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/r5/R5NpmPackageLoaderInMemory.java new file mode 100644 index 0000000000..801e67eb94 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/r5/R5NpmPackageLoaderInMemory.java @@ -0,0 +1,65 @@ +package org.opencds.cqf.fhir.utility.npm.r5; + +import ca.uhn.fhir.context.FhirContext; +import jakarta.annotation.Nullable; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.opencds.cqf.fhir.utility.npm.BaseNpmPackageLoaderInMemory; +import org.opencds.cqf.fhir.utility.npm.NpmNamespaceManager; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; + +/** + * Simplistic implementation of {@link NpmPackageLoader} that loads NpmPackages from the classpath + * and stores {@link NpmPackage}s in a Set. This class is recommended for testing + * and NOT for production. + *

+ * Optionally uses a custom {@link NpmNamespaceManager} but can also resolve all NamespaceInfos + * by extracting them from all loaded packages at construction time. + *

+ * This is for R5 only. + */ +public class R5NpmPackageLoaderInMemory extends BaseNpmPackageLoaderInMemory implements NpmPackageLoader { + + public static R5NpmPackageLoaderInMemory fromNpmPackageAbsolutePath(List tgzPaths) { + return fromNpmPackageAbsolutePath(null, tgzPaths); + } + + public static R5NpmPackageLoaderInMemory fromNpmPackageAbsolutePath( + NpmNamespaceManager npmNamespaceManager, List tgzPaths) { + final Set npmPackages = buildNpmPackagesFromAbsolutePath(tgzPaths); + + return new R5NpmPackageLoaderInMemory(npmPackages, npmNamespaceManager); + } + + public static R5NpmPackageLoaderInMemory fromNpmPackageClasspath(Class clazz, Path... tgzPaths) { + return fromNpmPackageClasspath(null, clazz, tgzPaths); + } + + public static R5NpmPackageLoaderInMemory fromNpmPackageClasspath( + @Nullable NpmNamespaceManager npmNamespaceManager, Class clazz, Path... tgzPaths) { + return fromNpmPackageClasspath(npmNamespaceManager, clazz, Arrays.asList(tgzPaths)); + } + + public static R5NpmPackageLoaderInMemory fromNpmPackageClasspath(Class clazz, List tgzPaths) { + return fromNpmPackageClasspath(null, clazz, tgzPaths); + } + + public static R5NpmPackageLoaderInMemory fromNpmPackageClasspath( + @Nullable NpmNamespaceManager npmNamespaceManager, Class clazz, List tgzPaths) { + final Set npmPackages = buildNpmPackageFromClasspath(clazz, tgzPaths); + + return new R5NpmPackageLoaderInMemory(npmPackages, npmNamespaceManager); + } + + public R5NpmPackageLoaderInMemory(Set npmPackages, @Nullable NpmNamespaceManager npmNamespaceManager) { + super(npmPackages, npmNamespaceManager); + } + + @Override + public FhirContext getFhirContext() { + return FhirContext.forR5Cached(); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/BaseNpmPackageLoaderTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/BaseNpmPackageLoaderTest.java new file mode 100644 index 0000000000..c57af47cd5 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/BaseNpmPackageLoaderTest.java @@ -0,0 +1,124 @@ +package org.opencds.cqf.fhir.utility.npm; + +import jakarta.annotation.Nonnull; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +public abstract class BaseNpmPackageLoaderTest { + + protected static final String DOT_TGZ = ".tgz"; + + protected static final String SIMPLE_ALPHA_LOWER = "simplealpha"; + protected static final String SIMPLE_ALPHA_MIXED = "SimpleAlpha"; + protected static final String SIMPLE_BRAVO_LOWER = "simplebravo"; + protected static final String SIMPLE_BRAVO_MIXED = "SimpleBravo"; + protected static final String WITH_DERIVED_LIBRARY_LOWER = "withderivedlibrary"; + protected static final String WITH_DERIVED_LIBRARY_MIXED = "WithDerivedLibrary"; + protected static final String DERIVED_LIBRARY_ID = "DerivedLibrary"; + protected static final String DERIVED_LIBRARY = DERIVED_LIBRARY_ID; + + protected static final String DERIVED_LAYER_1_A = "DerivedLayer1a"; + protected static final String DERIVED_LAYER_1_B = "DerivedLayer1b"; + protected static final String DERIVED_LAYER_2_A = "DerivedLayer2a"; + protected static final String DERIVED_LAYER_2_B = "DerivedLayer2b"; + protected static final String CROSS_PACKAGE_SOURCE = "crosspackagesource"; + protected static final String CROSS_PACKAGE_SOURCE_ID = "CrossPackageSource"; + protected static final String CROSS_PACKAGE_TARGET = "crosspackagetarget"; + protected static final String CROSS_PACKAGE_TARGET_ID = "CrossPackageTarget"; + + protected static final String NAMESPACE_PREFIX = "opencds."; + + protected static final String WITH_TWO_LAYERS_DERIVED_LIBRARIES = "withtwolayersderivedlibraries"; + protected static final String WITH_TWO_LAYERS_DERIVED_LIBRARIES_UPPER = "WithTwoLayersDerivedLibraries"; + + protected static final String SIMPLE_ALPHA_NAMESPACE = NAMESPACE_PREFIX + SIMPLE_ALPHA_LOWER; + protected static final String SIMPLE_BRAVO_NAMESPACE = NAMESPACE_PREFIX + SIMPLE_BRAVO_LOWER; + protected static final String WITH_DERIVED_NAMESPACE = NAMESPACE_PREFIX + WITH_DERIVED_LIBRARY_LOWER; + protected static final String WITH_TWO_LAYERS_NAMESPACE = NAMESPACE_PREFIX + WITH_TWO_LAYERS_DERIVED_LIBRARIES; + protected static final String CROSS_PACKAGE_SOURCE_NAMESPACE = NAMESPACE_PREFIX + CROSS_PACKAGE_SOURCE; + protected static final String CROSS_PACKAGE_TARGET_NAMESPACE = NAMESPACE_PREFIX + CROSS_PACKAGE_TARGET; + + protected static final String SIMPLE_ALPHA_TGZ = SIMPLE_ALPHA_LOWER + DOT_TGZ; + protected static final String SIMPLE_BRAVO_TGZ = SIMPLE_BRAVO_LOWER + DOT_TGZ; + protected static final Path WITH_DERIVED_LIBRARY_TGZ = Paths.get(WITH_DERIVED_LIBRARY_LOWER + DOT_TGZ); + protected static final Path WITH_TWO_LAYERS_DERIVED_LIBRARIES_TGZ = + Paths.get(WITH_TWO_LAYERS_DERIVED_LIBRARIES + DOT_TGZ); + protected static final Path CROSS_PACKAGE_SOURCE_TGZ = Paths.get(CROSS_PACKAGE_SOURCE + DOT_TGZ); + protected static final Path CROSS_PACKAGE_TARGET_TGZ = Paths.get(CROSS_PACKAGE_TARGET + DOT_TGZ); + + protected static final String SLASH_MEASURE_SLASH = "/Measure/"; + protected static final String SLASH_LIBRARY_SLASH = "/Library/"; + + private static final String PIPE = "|"; + protected static final String VERSION_0_1 = "0.1"; + protected static final String VERSION_0_2 = "0.2"; + protected static final String VERSION_0_3 = "0.3"; + protected static final String VERSION_0_4 = "0.4"; + protected static final String VERSION_0_5 = "0.5"; + + protected static final String SIMPLE_ALPHA_NAMESPACE_URL = "http://simplealpha.npm.opencds.org"; + protected static final String SIMPLE_BRAVO_NAMESPACE_URL = "http://simplebravo.npm.opencds.org"; + protected static final String WITH_DERIVED_URL = "http://withderivedlibrary.npm.opencds.org"; + protected static final String WITH_TWO_LAYERS_DERIVED_URL = "http://withtwolayersderivedlibraries.npm.opencds.org"; + protected static final String CROSS_PACKAGE_SOURCE_URL = "http://crosspackagesource.npm.opencds.org"; + protected static final String CROSS_PACKAGE_TARGET_URL = "http://crosspackagetarget.npm.opencds.org"; + + protected static final String MEASURE_URL_ALPHA = + SIMPLE_ALPHA_NAMESPACE_URL + SLASH_MEASURE_SLASH + SIMPLE_ALPHA_MIXED; + protected static final String MEASURE_URL_BRAVO = + SIMPLE_BRAVO_NAMESPACE_URL + SLASH_MEASURE_SLASH + SIMPLE_BRAVO_MIXED; + protected static final String MEASURE_URL_WITH_DERIVED_LIBRARY = + WITH_DERIVED_URL + SLASH_MEASURE_SLASH + WITH_DERIVED_LIBRARY_MIXED; + protected static final String MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES = + WITH_TWO_LAYERS_DERIVED_URL + SLASH_MEASURE_SLASH + WITH_TWO_LAYERS_DERIVED_LIBRARIES_UPPER; + + protected static final String MEASURE_URL_CROSS_PACKAGE_SOURCE = + CROSS_PACKAGE_SOURCE_URL + SLASH_MEASURE_SLASH + CROSS_PACKAGE_SOURCE_ID; + + protected static final String MEASURE_URL_CROSS_PACKAGE_TARGET = + CROSS_PACKAGE_TARGET_URL + SLASH_MEASURE_SLASH + CROSS_PACKAGE_TARGET_ID; + + protected static final String LIBRARY_URL_ALPHA_NO_VERSION = + SIMPLE_ALPHA_NAMESPACE_URL + SLASH_LIBRARY_SLASH + SIMPLE_ALPHA_MIXED; + protected static final String LIBRARY_URL_ALPHA_WITH_VERSION = LIBRARY_URL_ALPHA_NO_VERSION + PIPE + VERSION_0_1; + protected static final String LIBRARY_URL_BRAVO_NO_VERSION = + SIMPLE_BRAVO_NAMESPACE_URL + SLASH_LIBRARY_SLASH + SIMPLE_BRAVO_MIXED; + protected static final String LIBRARY_URL_BRAVO_WITH_VERSION = LIBRARY_URL_BRAVO_NO_VERSION + PIPE + VERSION_0_1; + + protected static final String LIBRARY_URL_WITH_DERIVED_LIBRARY_NO_VERSION = + WITH_DERIVED_URL + SLASH_LIBRARY_SLASH + WITH_DERIVED_LIBRARY_MIXED; + protected static final String LIBRARY_URL_WITH_DERIVED_LIBRARY_WITH_VERSION = + WITH_DERIVED_URL + SLASH_LIBRARY_SLASH + WITH_DERIVED_LIBRARY_MIXED + PIPE + VERSION_0_4; + protected static final String LIBRARY_URL_DERIVED_LIBRARY = + WITH_DERIVED_URL + SLASH_LIBRARY_SLASH + DERIVED_LIBRARY; + + protected static final String LIBRARY_URL_CROSS_PACKAGE_SOURCE_NO_VERSION = + CROSS_PACKAGE_SOURCE_URL + SLASH_LIBRARY_SLASH + CROSS_PACKAGE_SOURCE_ID; + protected static final String LIBRARY_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION = + LIBRARY_URL_CROSS_PACKAGE_SOURCE_NO_VERSION + PIPE + VERSION_0_2; + protected static final String LIBRARY_URL_CROSS_PACKAGE_TARGET_NO_VERSION = + CROSS_PACKAGE_TARGET_URL + SLASH_LIBRARY_SLASH + CROSS_PACKAGE_TARGET_ID; + protected static final String LIBRARY_URL_CROSS_PACKAGE_TARGET_WITH_VERSION = + LIBRARY_URL_CROSS_PACKAGE_TARGET_NO_VERSION + PIPE + VERSION_0_3; + + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_NO_VERSION = + WITH_TWO_LAYERS_DERIVED_URL + SLASH_LIBRARY_SLASH + WITH_TWO_LAYERS_DERIVED_LIBRARIES_UPPER; + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION = + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_NO_VERSION + PIPE + VERSION_0_5; + + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A = + WITH_TWO_LAYERS_DERIVED_URL + SLASH_LIBRARY_SLASH + DERIVED_LAYER_1_A; + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B = + WITH_TWO_LAYERS_DERIVED_URL + SLASH_LIBRARY_SLASH + DERIVED_LAYER_1_B; + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A = + WITH_TWO_LAYERS_DERIVED_URL + SLASH_LIBRARY_SLASH + DERIVED_LAYER_2_A; + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B = + WITH_TWO_LAYERS_DERIVED_URL + SLASH_LIBRARY_SLASH + DERIVED_LAYER_2_B; + + @Nonnull + protected abstract BaseNpmPackageLoaderInMemory setup(Path... npmPackagePaths); + + @Nonnull + protected abstract BaseNpmPackageLoaderInMemory setup(List npmPackagePaths); +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutorTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutorTest.java new file mode 100644 index 0000000000..60e1016558 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutorTest.java @@ -0,0 +1,29 @@ +package org.opencds.cqf.fhir.utility.npm; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class NpmConfigDependencySubstitutorTest { + + @Test + void present() { + var npmPackageLoader = mock(NpmPackageLoader.class); + + var npmPackageLoaderFromSubstitutor = + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(Optional.of(npmPackageLoader)); + + assertNotEquals(NpmPackageLoader.R4_DEFAULT, npmPackageLoaderFromSubstitutor); + assertEquals(npmPackageLoader, npmPackageLoaderFromSubstitutor); + } + + @Test + void empty() { + var npmPackageLoaderFromSubstitutor = + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(Optional.empty()); + + assertEquals(NpmPackageLoader.R4_DEFAULT, npmPackageLoaderFromSubstitutor); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r4/NpmPackageLoaderR4Test.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r4/NpmPackageLoaderR4Test.java new file mode 100644 index 0000000000..f036cda55b --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r4/NpmPackageLoaderR4Test.java @@ -0,0 +1,612 @@ +package org.opencds.cqf.fhir.utility.npm.r4; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.context.FhirVersionEnum; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.r4.model.Attachment; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Measure; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.r4.LibraryAdapter; +import org.opencds.cqf.fhir.utility.npm.BaseNpmPackageLoaderInMemory; +import org.opencds.cqf.fhir.utility.npm.BaseNpmPackageLoaderTest; + +@SuppressWarnings("squid:S2699") +class NpmPackageLoaderR4Test extends BaseNpmPackageLoaderTest { + + protected FhirVersionEnum fhirVersion = FhirVersionEnum.R4; + + private static final String EXPECTED_CQL_ALPHA = + """ + library opencds.simplealpha.SimpleAlpha + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists ("Encounter Finished") + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + private static final String EXPECTED_CQL_BRAVO = + """ + library opencds.simplealpha.SimpleBravo + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2024-01-01T00:00:00.0-06:00, @2025-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists ("Encounter Planned") + + define "Encounter Planned": + [Encounter] E + where E.status = 'planned' + """; + + private static final String EXPECTED_CQL_WITH_DERIVED = + """ + library opencds.withderivedlibrary WithDerivedLibrary version '0.4' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + include DerivedLibrary version '0.4' + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (DerivedLibrary."Encounter Finished") + """; + + private static final String EXPECTED_CQL_DERIVED = + """ + library opencds.withderivedlibrary.DerivedLibrary version '0.4' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + private static final String EXPECTED_CQL_DERIVED_TWO_LAYERS = + """ + library opencds.withtwolayersderivedlibraries.WithTwoLayersDerivedLibraries version '0.5' + + using FHIR version '4.0.1' + + include DerivedLayer1a version '0.5' + include DerivedLayer1b version '0.5' + + parameter "Measurement Period" Interval + default Interval[@2022-01-01T00:00:00.0-06:00, @2023-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + DerivedLayer1a."Initial Population" + + define "Denominator": + DerivedLayer1b."Denominator" + + define "Numerator": + DerivedLayer1b."Numerator" + """; + + private static final String EXPECTED_CQL_DERIVED_1_A = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer1a version '0.5' + + using FHIR version '4.0.1' + + include DerivedLayer2a version '0.5' + include DerivedLayer2b version '0.5' + + context Patient + + define "Initial Population": + DerivedLayer2a."Initial Population" + """; + + private static final String EXPECTED_CQL_DERIVED_1_B = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer1b version '0.5' + + using FHIR version '4.0.1' + + include DerivedLayer2a version '0.5' + include DerivedLayer2b version '0.5' + + context Patient + + define "Denominator": + DerivedLayer2a."Denominator" + + define "Numerator": + DerivedLayer2b."Numerator" + """; + + private static final String EXPECTED_CQL_DERIVED_2_A = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer2a version '0.5' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + context Patient + + define "Initial Population": + exists ("Encounter Finished") + + define "Denominator": + exists ("Encounter Planned") + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + + define "Encounter Planned": + [Encounter] E + where E.status = 'planned' + """; + + private static final String EXPECTED_CQL_DERIVED_2_B = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer2b version '0.5' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Numerator": + exists ("Encounter Triaged") + + define "Encounter Triaged": + [Encounter] E + where E.status = 'triaged' + """; + + private static final String EXPECTED_CQL_CROSS_SOURCE = + """ + library opencds.crosspackagesource.CrossPackageSource version '0.2' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + include opencds.crosspackagetarget.CrossPackageTarget version '0.3' called CrossPackageTarget + + parameter "Measurement Period" Interval + default Interval[@2020-01-01T00:00:00.0-06:00, @2021-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (CrossPackageTarget."Encounter Finished") + """; + + private static final String EXPECTED_CQL_CROSS_TARGET = + """ + library opencds.crosspackagetarget.CrossPackageTarget version '0.3' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + @Test + void simpleAlpha() { + final BaseNpmPackageLoaderInMemory loader = setup(Path.of(SIMPLE_ALPHA_TGZ)); + + final Optional optMeasure = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_ALPHA)); + + verifyMeasure(optMeasure.orElse(null), MEASURE_URL_ALPHA, VERSION_0_1, LIBRARY_URL_ALPHA_WITH_VERSION); + + final Optional optLibrary = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_ALPHA_WITH_VERSION)); + + verifyLibrary(optLibrary.orElse(null), LIBRARY_URL_ALPHA_NO_VERSION, VERSION_0_1, EXPECTED_CQL_ALPHA); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(1, allNamespaceInfos.size()); + + final NamespaceInfo namespaceInfo = allNamespaceInfos.get(0); + + assertEquals(SIMPLE_ALPHA_NAMESPACE, namespaceInfo.getName()); + assertEquals(SIMPLE_ALPHA_NAMESPACE_URL, namespaceInfo.getUri()); + } + + @Test + void simpleBravo() { + final BaseNpmPackageLoaderInMemory loader = setup(Path.of(SIMPLE_BRAVO_TGZ)); + + final Optional optMeasure = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_BRAVO)); + + verifyMeasure(optMeasure.orElse(null), MEASURE_URL_BRAVO, VERSION_0_1, LIBRARY_URL_BRAVO_WITH_VERSION); + + final Optional optLibrary = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_BRAVO_WITH_VERSION)); + + verifyLibrary(optLibrary.orElse(null), LIBRARY_URL_BRAVO_NO_VERSION, VERSION_0_1, EXPECTED_CQL_BRAVO); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(1, allNamespaceInfos.size()); + + final NamespaceInfo namespaceInfo = allNamespaceInfos.get(0); + + assertEquals(SIMPLE_BRAVO_NAMESPACE, namespaceInfo.getName()); + assertEquals(SIMPLE_BRAVO_NAMESPACE_URL, namespaceInfo.getUri()); + } + + @Test + void multiplePackages() { + final BaseNpmPackageLoaderInMemory loader = setup( + Stream.of(SIMPLE_ALPHA_TGZ, SIMPLE_BRAVO_TGZ).map(Paths::get).toList()); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(2, allNamespaceInfos.size()); + + assertTrue(allNamespaceInfos.contains(new NamespaceInfo(SIMPLE_ALPHA_NAMESPACE, SIMPLE_ALPHA_NAMESPACE_URL))); + assertTrue(allNamespaceInfos.contains(new NamespaceInfo(SIMPLE_BRAVO_NAMESPACE, SIMPLE_BRAVO_NAMESPACE_URL))); + + final Optional optMeasureAlpha = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_ALPHA)); + + verifyMeasure(optMeasureAlpha.orElse(null), MEASURE_URL_ALPHA, VERSION_0_1, LIBRARY_URL_ALPHA_WITH_VERSION); + + final Optional optLibraryAlpha = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_ALPHA_WITH_VERSION)); + + verifyLibrary(optLibraryAlpha.orElse(null), LIBRARY_URL_ALPHA_NO_VERSION, VERSION_0_1, EXPECTED_CQL_ALPHA); + + final Optional optMeasureBravo = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_ALPHA)); + + verifyMeasure(optMeasureBravo.orElse(null), MEASURE_URL_ALPHA, VERSION_0_1, LIBRARY_URL_ALPHA_WITH_VERSION); + + final Optional optLibraryBravo = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_BRAVO_WITH_VERSION)); + + verifyLibrary(optLibraryBravo.orElse(null), LIBRARY_URL_BRAVO_NO_VERSION, VERSION_0_1, EXPECTED_CQL_BRAVO); + } + + @Test + void derivedLibrary() { + + final BaseNpmPackageLoaderInMemory loader = setup(WITH_DERIVED_LIBRARY_TGZ); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(1, allNamespaceInfos.size()); + + assertTrue(allNamespaceInfos.contains(new NamespaceInfo(WITH_DERIVED_NAMESPACE, WITH_DERIVED_URL))); + + final Optional optMeasureWithDerived = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_WITH_DERIVED_LIBRARY)); + + verifyMeasure( + optMeasureWithDerived.orElse(null), + MEASURE_URL_WITH_DERIVED_LIBRARY, + VERSION_0_4, + LIBRARY_URL_WITH_DERIVED_LIBRARY_WITH_VERSION); + + final Optional optLibraryWithDerived = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_WITH_DERIVED_LIBRARY_WITH_VERSION)); + + verifyLibrary( + optLibraryWithDerived.orElse(null), + LIBRARY_URL_WITH_DERIVED_LIBRARY_NO_VERSION, + VERSION_0_4, + EXPECTED_CQL_WITH_DERIVED); + + final Optional optLibraryDerived = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_DERIVED_LIBRARY)); + + verifyLibrary(optLibraryDerived.orElse(null), LIBRARY_URL_DERIVED_LIBRARY, VERSION_0_4, EXPECTED_CQL_DERIVED); + } + + @Test + void derivedLibraryTwoLayers() { + + final BaseNpmPackageLoaderInMemory loader = setup(WITH_TWO_LAYERS_DERIVED_LIBRARIES_TGZ); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(1, allNamespaceInfos.size()); + + assertTrue( + allNamespaceInfos.contains(new NamespaceInfo(WITH_TWO_LAYERS_NAMESPACE, WITH_TWO_LAYERS_DERIVED_URL))); + + final Optional optMeasureWithTwoDerived = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES)); + + verifyMeasure( + optMeasureWithTwoDerived.orElse(null), + MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES, + VERSION_0_5, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION); + + final Optional optLibraryWithTwoDerived = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION)); + + verifyLibrary( + optLibraryWithTwoDerived.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_NO_VERSION, + VERSION_0_5, + EXPECTED_CQL_DERIVED_TWO_LAYERS); + + final Optional optLibraryWithTwoDerived1a = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A)); + + verifyLibrary( + optLibraryWithTwoDerived1a.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A, + VERSION_0_5, + EXPECTED_CQL_DERIVED_1_A); + + final Optional optLibraryWithTwoDerived1b = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B)); + + verifyLibrary( + optLibraryWithTwoDerived1b.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B, + VERSION_0_5, + EXPECTED_CQL_DERIVED_1_B); + + final Optional optLibraryWithTwoDerived2a = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A)); + + verifyLibrary( + optLibraryWithTwoDerived2a.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A, + VERSION_0_5, + EXPECTED_CQL_DERIVED_2_A); + + final Optional optLibraryWithTwoDerived2b = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B)); + + verifyLibrary( + optLibraryWithTwoDerived2b.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B, + VERSION_0_5, + EXPECTED_CQL_DERIVED_2_B); + + final ILibraryAdapter libraryAdapter1a = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_1_A) + .withVersion(VERSION_0_5) + .withSystem(WITH_TWO_LAYERS_DERIVED_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapter1a, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A, + VERSION_0_5, + EXPECTED_CQL_DERIVED_1_A); + + final ILibraryAdapter libraryAdapter1b = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_1_B) + .withVersion(VERSION_0_5) + .withSystem(WITH_TWO_LAYERS_DERIVED_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapter1b, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B, + VERSION_0_5, + EXPECTED_CQL_DERIVED_1_B); + + final ILibraryAdapter libraryAdapter2a = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_2_A) + .withVersion(VERSION_0_5) + .withSystem(WITH_TWO_LAYERS_DERIVED_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapter2a, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A, + VERSION_0_5, + EXPECTED_CQL_DERIVED_2_A); + + final ILibraryAdapter libraryAdapter2b = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_2_B) + .withVersion(VERSION_0_5) + .withSystem(WITH_TWO_LAYERS_DERIVED_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapter2b, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B, + VERSION_0_5, + EXPECTED_CQL_DERIVED_2_B); + } + + @Test + void crossPackage() { + + final BaseNpmPackageLoaderInMemory loader = setup(CROSS_PACKAGE_SOURCE_TGZ, CROSS_PACKAGE_TARGET_TGZ); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(2, allNamespaceInfos.size()); + + assertTrue(allNamespaceInfos.contains( + new NamespaceInfo(CROSS_PACKAGE_SOURCE_NAMESPACE, CROSS_PACKAGE_SOURCE_URL))); + assertTrue(allNamespaceInfos.contains( + new NamespaceInfo(CROSS_PACKAGE_TARGET_NAMESPACE, CROSS_PACKAGE_TARGET_URL))); + + final Optional optMeasureCrossSource = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_CROSS_PACKAGE_SOURCE)); + + verifyMeasure( + optMeasureCrossSource.orElse(null), + MEASURE_URL_CROSS_PACKAGE_SOURCE, + VERSION_0_2, + LIBRARY_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION); + + final Optional optLibraryCrossSource = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION)); + + verifyLibrary( + optLibraryCrossSource.orElse(null), + LIBRARY_URL_CROSS_PACKAGE_SOURCE_NO_VERSION, + VERSION_0_2, + EXPECTED_CQL_CROSS_SOURCE); + + final Optional optMeasureCrossTarget = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_CROSS_PACKAGE_TARGET)); + + verifyMeasure( + optMeasureCrossTarget.orElse(null), + MEASURE_URL_CROSS_PACKAGE_TARGET, + VERSION_0_3, + LIBRARY_URL_CROSS_PACKAGE_TARGET_WITH_VERSION); + + final Optional optLibraryCrossTarget = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_CROSS_PACKAGE_TARGET_WITH_VERSION)); + + verifyLibrary( + optLibraryCrossTarget.orElse(null), + LIBRARY_URL_CROSS_PACKAGE_TARGET_NO_VERSION, + VERSION_0_3, + EXPECTED_CQL_CROSS_TARGET); + + final ILibraryAdapter libraryAdapterSource = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(CROSS_PACKAGE_SOURCE_ID) + .withVersion(VERSION_0_2) + .withSystem(CROSS_PACKAGE_SOURCE_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapterSource, + LIBRARY_URL_CROSS_PACKAGE_SOURCE_NO_VERSION, + VERSION_0_2, + EXPECTED_CQL_CROSS_SOURCE); + + final ILibraryAdapter libraryAdapterTarget = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(CROSS_PACKAGE_TARGET_ID) + .withVersion(VERSION_0_3) + .withSystem(CROSS_PACKAGE_TARGET_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapterTarget, + LIBRARY_URL_CROSS_PACKAGE_TARGET_NO_VERSION, + VERSION_0_3, + EXPECTED_CQL_CROSS_TARGET); + } + + protected void verifyMeasure( + @Nullable Measure measure, String measureUrl, String version, String expectedLibraryUrl) { + + assertNotNull(measure, "Could not find measure with url: %s".formatted(measureUrl)); + + assertEquals(measureUrl, measure.getUrl()); + + assertEquals(version, measure.getVersion()); + + final List libraryUrls = measure.getLibrary(); + assertEquals(1, libraryUrls.size()); + final CanonicalType libraryUrl = libraryUrls.get(0); + assertEquals(expectedLibraryUrl, libraryUrl.asStringValue()); + } + + private void verifyLibrary( + @Nullable ILibraryAdapter libraryAdapter, String expectedLibraryUrl, String version, String expectedCql) { + + assertNotNull(libraryAdapter, "Could not find library with url: %s".formatted(expectedLibraryUrl)); + + assertInstanceOf(LibraryAdapter.class, libraryAdapter); + + verifyLibrary(((LibraryAdapter) libraryAdapter).get(), expectedLibraryUrl, version, expectedCql); + } + + private void verifyLibrary( + @Nullable Library library, String expectedLibraryUrl, String version, String expectedCql) { + + assertNotNull(library, "Could not find library with url: %s".formatted(expectedLibraryUrl)); + + assertEquals(expectedLibraryUrl, library.getUrl()); + + assertEquals(version, library.getVersion()); + + final List attachments = library.getContent(); + assertEquals(1, attachments.size()); + final Attachment attachment = attachments.get(0); + assertEquals("text/cql", attachment.getContentType()); + final byte[] attachmentData = attachment.getData(); + final String cql = new String(attachmentData, StandardCharsets.UTF_8); + + assertEquals(expectedCql, cql); + } + + @Nonnull + @Override + protected BaseNpmPackageLoaderInMemory setup(Path... npmPackagePaths) { + return R4NpmPackageLoaderInMemory.fromNpmPackageClasspath(getClass(), npmPackagePaths); + } + + @Nonnull + @Override + protected BaseNpmPackageLoaderInMemory setup(List npmPackagePaths) { + return R4NpmPackageLoaderInMemory.fromNpmPackageClasspath(getClass(), npmPackagePaths); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r5/NpmPackageLoaderR5Test.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r5/NpmPackageLoaderR5Test.java new file mode 100644 index 0000000000..0bc7dc15cd --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r5/NpmPackageLoaderR5Test.java @@ -0,0 +1,610 @@ +package org.opencds.cqf.fhir.utility.npm.r5; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.context.FhirVersionEnum; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.r5.model.Attachment; +import org.hl7.fhir.r5.model.CanonicalType; +import org.hl7.fhir.r5.model.Library; +import org.hl7.fhir.r5.model.Measure; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.r5.LibraryAdapter; +import org.opencds.cqf.fhir.utility.npm.BaseNpmPackageLoaderInMemory; +import org.opencds.cqf.fhir.utility.npm.BaseNpmPackageLoaderTest; + +@SuppressWarnings("squid:S2699") +class NpmPackageLoaderR5Test extends BaseNpmPackageLoaderTest { + + protected FhirVersionEnum fhirVersion = FhirVersionEnum.R5; + + private static final String EXPECTED_CQL_ALPHA = + """ + library opencds.simplealpha.SimpleAlpha + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists ("Encounter Finished") + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + private static final String EXPECTED_CQL_BRAVO = + """ + library opencds.simplealpha.SimpleBravo + + using FHIR version '5.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2024-01-01T00:00:00.0-06:00, @2025-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists ("Encounter Planned") + + define "Encounter Planned": + [Encounter] E + where E.status = 'planned' + """; + private static final String EXPECTED_CQL_WITH_DERIVED = + """ + library opencds.withderivedlibrary WithDerivedLibrary version '0.4' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + include DerivedLibrary version '0.4' + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (DerivedLibrary."Encounter Finished") + """; + private static final String EXPECTED_CQL_DERIVED = + """ + library opencds.withderivedlibrary.DerivedLibrary version '0.4' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + private static final String EXPECTED_CQL_DERIVED_TWO_LAYERS = + """ + library opencds.withtwolayersderivedlibraries.WithTwoLayersDerivedLibraries version '0.5' + + using FHIR version '5.0.1' + + include DerivedLayer1a version '0.5' + include DerivedLayer1b version '0.5' + + parameter "Measurement Period" Interval + default Interval[@2022-01-01T00:00:00.0-06:00, @2023-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + DerivedLayer1a."Initial Population" + + define "Denominator": + DerivedLayer1b."Denominator" + + define "Numerator": + DerivedLayer1b."Numerator" + """; + + private static final String EXPECTED_CQL_DERIVED_1_A = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer1a version '0.5' + + using FHIR version '5.0.1' + + include DerivedLayer2a version '0.5' + include DerivedLayer2b version '0.5' + + context Patient + + define "Initial Population": + DerivedLayer2a."Initial Population" + """; + + private static final String EXPECTED_CQL_DERIVED_1_B = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer1b version '0.5' + + using FHIR version '5.0.1' + + include DerivedLayer2a version '0.5' + include DerivedLayer2b version '0.5' + + context Patient + + define "Denominator": + DerivedLayer2a."Denominator" + + define "Numerator": + DerivedLayer2b."Numerator" + """; + + private static final String EXPECTED_CQL_DERIVED_2_A = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer2a version '0.5' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + context Patient + + define "Initial Population": + exists ("Encounter Finished") + + define "Denominator": + exists ("Encounter Planned") + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + + define "Encounter Planned": + [Encounter] E + where E.status = 'planned' + """; + + private static final String EXPECTED_CQL_DERIVED_2_B = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer2b version '0.5' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Numerator": + exists ("Encounter Triaged") + + define "Encounter Triaged": + [Encounter] E + where E.status = 'triaged' + """; + + private static final String EXPECTED_CQL_CROSS_SOURCE = + """ + library opencds.crosspackagesource.CrossPackageSource version '0.2' + + using FHIR version '5.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + include opencds.crosspackagetarget.CrossPackageTarget version '0.3' called CrossPackageTarget + + parameter "Measurement Period" Interval + default Interval[@2020-01-01T00:00:00.0-06:00, @2021-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (CrossPackageTarget."Encounter Finished") + """; + + private static final String EXPECTED_CQL_CROSS_TARGET = + """ + library opencds.crosspackagetarget.CrossPackageTarget version '0.3' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + @Test + void simpleAlpha() { + final BaseNpmPackageLoaderInMemory loader = setup(Path.of(SIMPLE_ALPHA_TGZ)); + + final Optional optMeasure = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_ALPHA)); + + verifyMeasure(optMeasure.orElse(null), MEASURE_URL_ALPHA, VERSION_0_1, LIBRARY_URL_ALPHA_WITH_VERSION); + + final Optional optLibrary = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_ALPHA_WITH_VERSION)); + + verifyLibrary(optLibrary.orElse(null), LIBRARY_URL_ALPHA_NO_VERSION, VERSION_0_1, EXPECTED_CQL_ALPHA); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(1, allNamespaceInfos.size()); + + final NamespaceInfo namespaceInfo = allNamespaceInfos.get(0); + + assertEquals(SIMPLE_ALPHA_NAMESPACE, namespaceInfo.getName()); + assertEquals(SIMPLE_ALPHA_NAMESPACE_URL, namespaceInfo.getUri()); + } + + @Test + void simpleBravo() { + final BaseNpmPackageLoaderInMemory loader = setup(Path.of(SIMPLE_BRAVO_TGZ)); + + final Optional optMeasure = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_BRAVO)); + + verifyMeasure(optMeasure.orElse(null), MEASURE_URL_BRAVO, VERSION_0_1, LIBRARY_URL_BRAVO_WITH_VERSION); + + final Optional optLibrary = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_BRAVO_WITH_VERSION)); + + verifyLibrary(optLibrary.orElse(null), LIBRARY_URL_BRAVO_NO_VERSION, VERSION_0_1, EXPECTED_CQL_BRAVO); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(1, allNamespaceInfos.size()); + + final NamespaceInfo namespaceInfo = allNamespaceInfos.get(0); + + assertEquals(SIMPLE_BRAVO_NAMESPACE, namespaceInfo.getName()); + assertEquals(SIMPLE_BRAVO_NAMESPACE_URL, namespaceInfo.getUri()); + } + + @Test + void multiplePackages() { + final BaseNpmPackageLoaderInMemory loader = R5NpmPackageLoaderInMemory.fromNpmPackageClasspath( + getClass(), + Stream.of(SIMPLE_ALPHA_TGZ, SIMPLE_BRAVO_TGZ).map(Paths::get).toList()); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(2, allNamespaceInfos.size()); + + assertTrue(allNamespaceInfos.contains(new NamespaceInfo(SIMPLE_ALPHA_NAMESPACE, SIMPLE_ALPHA_NAMESPACE_URL))); + assertTrue(allNamespaceInfos.contains(new NamespaceInfo(SIMPLE_BRAVO_NAMESPACE, SIMPLE_BRAVO_NAMESPACE_URL))); + + final Optional optMeasureAlpha = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_ALPHA)); + + verifyMeasure(optMeasureAlpha.orElse(null), MEASURE_URL_ALPHA, VERSION_0_1, LIBRARY_URL_ALPHA_WITH_VERSION); + + final Optional optLibraryAlpha = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_ALPHA_WITH_VERSION)); + + verifyLibrary(optLibraryAlpha.orElse(null), LIBRARY_URL_ALPHA_NO_VERSION, VERSION_0_1, EXPECTED_CQL_ALPHA); + + final Optional optMeasureBravo = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_ALPHA)); + + verifyMeasure(optMeasureBravo.orElse(null), MEASURE_URL_ALPHA, VERSION_0_1, LIBRARY_URL_ALPHA_WITH_VERSION); + + final Optional optLibraryBravo = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_BRAVO_WITH_VERSION)); + + verifyLibrary(optLibraryBravo.orElse(null), LIBRARY_URL_BRAVO_NO_VERSION, VERSION_0_1, EXPECTED_CQL_BRAVO); + } + + @Test + void derivedLibrary() { + + final BaseNpmPackageLoaderInMemory loader = setup(WITH_DERIVED_LIBRARY_TGZ); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(1, allNamespaceInfos.size()); + + assertTrue(allNamespaceInfos.contains(new NamespaceInfo(WITH_DERIVED_NAMESPACE, WITH_DERIVED_URL))); + + final Optional optMeasureWithDerived = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_WITH_DERIVED_LIBRARY)); + + verifyMeasure( + optMeasureWithDerived.orElse(null), + MEASURE_URL_WITH_DERIVED_LIBRARY, + VERSION_0_4, + LIBRARY_URL_WITH_DERIVED_LIBRARY_WITH_VERSION); + + final Optional optLibraryWithDerived = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_WITH_DERIVED_LIBRARY_WITH_VERSION)); + + verifyLibrary( + optLibraryWithDerived.orElse(null), + LIBRARY_URL_WITH_DERIVED_LIBRARY_NO_VERSION, + VERSION_0_4, + EXPECTED_CQL_WITH_DERIVED); + + final Optional optLibraryDerived = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_DERIVED_LIBRARY)); + + verifyLibrary(optLibraryDerived.orElse(null), LIBRARY_URL_DERIVED_LIBRARY, VERSION_0_4, EXPECTED_CQL_DERIVED); + } + + @Test + void derivedLibraryTwoLayers() { + + final BaseNpmPackageLoaderInMemory loader = setup(WITH_TWO_LAYERS_DERIVED_LIBRARIES_TGZ); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(1, allNamespaceInfos.size()); + + assertTrue( + allNamespaceInfos.contains(new NamespaceInfo(WITH_TWO_LAYERS_NAMESPACE, WITH_TWO_LAYERS_DERIVED_URL))); + + final Optional optMeasureWithTwoDerived = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES)); + + verifyMeasure( + optMeasureWithTwoDerived.orElse(null), + MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES, + VERSION_0_5, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION); + + final Optional optLibraryWithTwoDerived = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION)); + + verifyLibrary( + optLibraryWithTwoDerived.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_NO_VERSION, + VERSION_0_5, + EXPECTED_CQL_DERIVED_TWO_LAYERS); + + final Optional optLibraryWithTwoDerived1a = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A)); + + verifyLibrary( + optLibraryWithTwoDerived1a.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A, + VERSION_0_5, + EXPECTED_CQL_DERIVED_1_A); + + final Optional optLibraryWithTwoDerived1b = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B)); + + verifyLibrary( + optLibraryWithTwoDerived1b.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B, + VERSION_0_5, + EXPECTED_CQL_DERIVED_1_B); + + final Optional optLibraryWithTwoDerived2a = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A)); + + verifyLibrary( + optLibraryWithTwoDerived2a.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A, + VERSION_0_5, + EXPECTED_CQL_DERIVED_2_A); + + final Optional optLibraryWithTwoDerived2b = loader.loadNpmResource( + Library.class, new CanonicalType(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B)); + + verifyLibrary( + optLibraryWithTwoDerived2b.orElse(null), + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B, + VERSION_0_5, + EXPECTED_CQL_DERIVED_2_B); + + final ILibraryAdapter libraryAdapter1a = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_1_A) + .withVersion(VERSION_0_5) + .withSystem(WITH_TWO_LAYERS_DERIVED_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapter1a, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A, + VERSION_0_5, + EXPECTED_CQL_DERIVED_1_A); + + final ILibraryAdapter libraryAdapter1b = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_1_B) + .withVersion(VERSION_0_5) + .withSystem(WITH_TWO_LAYERS_DERIVED_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapter1b, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B, + VERSION_0_5, + EXPECTED_CQL_DERIVED_1_B); + + final ILibraryAdapter libraryAdapter2a = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_2_A) + .withVersion(VERSION_0_5) + .withSystem(WITH_TWO_LAYERS_DERIVED_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapter2a, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A, + VERSION_0_5, + EXPECTED_CQL_DERIVED_2_A); + + final ILibraryAdapter libraryAdapter2b = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_2_B) + .withVersion(VERSION_0_5) + .withSystem(WITH_TWO_LAYERS_DERIVED_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapter2b, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B, + VERSION_0_5, + EXPECTED_CQL_DERIVED_2_B); + } + + @Test + void crossPackage() { + + final BaseNpmPackageLoaderInMemory loader = setup(CROSS_PACKAGE_SOURCE_TGZ, CROSS_PACKAGE_TARGET_TGZ); + + final List allNamespaceInfos = + loader.getNamespaceManager().getAllNamespaceInfos(); + + assertEquals(2, allNamespaceInfos.size()); + + assertTrue(allNamespaceInfos.contains( + new NamespaceInfo(CROSS_PACKAGE_SOURCE_NAMESPACE, CROSS_PACKAGE_SOURCE_URL))); + assertTrue(allNamespaceInfos.contains( + new NamespaceInfo(CROSS_PACKAGE_TARGET_NAMESPACE, CROSS_PACKAGE_TARGET_URL))); + + final Optional optMeasureCrossSource = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_CROSS_PACKAGE_SOURCE)); + + verifyMeasure( + optMeasureCrossSource.orElse(null), + MEASURE_URL_CROSS_PACKAGE_SOURCE, + VERSION_0_2, + LIBRARY_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION); + + final Optional optLibraryCrossSource = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION)); + + verifyLibrary( + optLibraryCrossSource.orElse(null), + LIBRARY_URL_CROSS_PACKAGE_SOURCE_NO_VERSION, + VERSION_0_2, + EXPECTED_CQL_CROSS_SOURCE); + + final Optional optMeasureCrossTarget = + loader.loadNpmResource(Measure.class, new CanonicalType(MEASURE_URL_CROSS_PACKAGE_TARGET)); + + verifyMeasure( + optMeasureCrossTarget.orElse(null), + MEASURE_URL_CROSS_PACKAGE_TARGET, + VERSION_0_3, + LIBRARY_URL_CROSS_PACKAGE_TARGET_WITH_VERSION); + + final Optional optLibraryCrossTarget = + loader.loadNpmResource(Library.class, new CanonicalType(LIBRARY_URL_CROSS_PACKAGE_TARGET_WITH_VERSION)); + + verifyLibrary( + optLibraryCrossTarget.orElse(null), + LIBRARY_URL_CROSS_PACKAGE_TARGET_NO_VERSION, + VERSION_0_3, + EXPECTED_CQL_CROSS_TARGET); + + final ILibraryAdapter libraryAdapterSource = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(CROSS_PACKAGE_SOURCE_ID) + .withVersion(VERSION_0_2) + .withSystem(CROSS_PACKAGE_SOURCE_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapterSource, + LIBRARY_URL_CROSS_PACKAGE_SOURCE_NO_VERSION, + VERSION_0_2, + EXPECTED_CQL_CROSS_SOURCE); + + final ILibraryAdapter libraryAdapterTarget = loader.findMatchingLibrary(new VersionedIdentifier() + .withId(CROSS_PACKAGE_TARGET_ID) + .withVersion(VERSION_0_3) + .withSystem(CROSS_PACKAGE_TARGET_URL)) + .orElse(null); + + verifyLibrary( + libraryAdapterTarget, + LIBRARY_URL_CROSS_PACKAGE_TARGET_NO_VERSION, + VERSION_0_3, + EXPECTED_CQL_CROSS_TARGET); + } + + protected void verifyMeasure( + @Nullable Measure measure, String measureUrl, String version, String expectedLibraryUrl) { + + assertNotNull(measure, "Could not find measure with url: %s".formatted(measureUrl)); + + assertEquals(measureUrl, measure.getUrl()); + + assertEquals(version, measure.getVersion()); + + final List libraryUrls = measure.getLibrary(); + assertEquals(1, libraryUrls.size()); + final CanonicalType libraryUrl = libraryUrls.get(0); + assertEquals(expectedLibraryUrl, libraryUrl.asStringValue()); + } + + private void verifyLibrary( + @Nullable ILibraryAdapter libraryAdapter, String expectedLibraryUrl, String version, String expectedCql) { + + assertNotNull(libraryAdapter, "Could not find library with url: %s".formatted(expectedLibraryUrl)); + + assertInstanceOf(LibraryAdapter.class, libraryAdapter); + + verifyLibrary(((LibraryAdapter) libraryAdapter).get(), expectedLibraryUrl, version, expectedCql); + } + + private void verifyLibrary( + @Nullable Library library, String expectedLibraryUrl, String version, String expectedCql) { + + assertNotNull(library, "Could not find library with url: %s".formatted(expectedLibraryUrl)); + + assertEquals(expectedLibraryUrl, library.getUrl()); + + assertEquals(version, library.getVersion()); + + final List attachments = library.getContent(); + assertEquals(1, attachments.size()); + final Attachment attachment = attachments.get(0); + assertEquals("text/cql", attachment.getContentType()); + final byte[] attachmentData = attachment.getData(); + final String cql = new String(attachmentData, StandardCharsets.UTF_8); + + assertEquals(expectedCql, cql); + } + + @Nonnull + @Override + protected R5NpmPackageLoaderInMemory setup(Path... tgzPaths) { + return R5NpmPackageLoaderInMemory.fromNpmPackageClasspath(getClass(), tgzPaths); + } + + @Nonnull + @Override + protected R5NpmPackageLoaderInMemory setup(List tgzPaths) { + return R5NpmPackageLoaderInMemory.fromNpmPackageClasspath(getClass(), tgzPaths); + } +} diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagesource.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagesource.tgz new file mode 100644 index 0000000000..79954af46a Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagesource.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagetarget.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagetarget.tgz new file mode 100644 index 0000000000..a5627babcf Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagetarget.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplealpha.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplealpha.tgz new file mode 100644 index 0000000000..6fdbe3d7ba Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplealpha.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplebravo.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplebravo.tgz new file mode 100644 index 0000000000..2ea1d03cfc Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplebravo.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withderivedlibrary.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withderivedlibrary.tgz new file mode 100644 index 0000000000..7a6dd561ef Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withderivedlibrary.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withtwolayersderivedlibraries.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withtwolayersderivedlibraries.tgz new file mode 100644 index 0000000000..8d91473cc4 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withtwolayersderivedlibraries.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagesource.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagesource.tgz new file mode 100644 index 0000000000..595519d9de Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagesource.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagetarget.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagetarget.tgz new file mode 100644 index 0000000000..d1904430e8 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagetarget.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplealpha.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplealpha.tgz new file mode 100644 index 0000000000..a31cb42b49 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplealpha.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplebravo.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplebravo.tgz new file mode 100644 index 0000000000..ca650e7fb5 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplebravo.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withderivedlibrary.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withderivedlibrary.tgz new file mode 100644 index 0000000000..201a3c71b1 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withderivedlibrary.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withtwolayersderivedlibraries.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withtwolayersderivedlibraries.tgz new file mode 100644 index 0000000000..fd1580aa6c Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withtwolayersderivedlibraries.tgz differ