From d234b601ea1d974f7c7d921c2dfb2d1b426ad8ce Mon Sep 17 00:00:00 2001 From: Gino Canessa Date: Thu, 18 Jul 2024 15:41:55 -0500 Subject: [PATCH 1/5] Solution file cleanup Add argo.run k8 configuration info. --- .deployments/k8-argo-run-ris.yaml | 145 ++++++++++++++++++++++++++++++ fhir-candle.sln | 24 ++++- 2 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 .deployments/k8-argo-run-ris.yaml diff --git a/.deployments/k8-argo-run-ris.yaml b/.deployments/k8-argo-run-ris.yaml new file mode 100644 index 0000000..792ec49 --- /dev/null +++ b/.deployments/k8-argo-run-ris.yaml @@ -0,0 +1,145 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ri-subscriptions + namespace: subscriptions-ri + labels: + app: ri-subscriptions +spec: + replicas: 1 + selector: + matchLabels: + app: ri-subscriptions + template: + metadata: + labels: + app: ri-subscriptions + spec: + containers: + - name: fhir-candle + image: ghcr.io/fhir/fhir-candle:latest + command: ["dotnet"] + args: ["fhir-candle.dll", "--reference-implementation", "subscriptions", "--load-package", "hl7.fhir.uv.subscriptions-backport#1.1.0", "--load-examples", "false", "--protect-source", "true", "-m", "200"] + envFrom: + - configMapRef: + name: special-config + env: + - name: Listen_Port + value: "5826" + - name: Public_Url + value: "https://subscriptions.argo.run" + - name: Zulip_Email + valueFrom: + secretKeyRef: + name: argonaut-secrets + key: Zulip_Email + - name: Zulip_Key + valueFrom: + secretKeyRef: + name: argonaut-secrets + key: Zulip_Key + - name: Zulip_Url + value: "https://chat.fhir.org" + - name: SMTP_Host + valueFrom: + secretKeyRef: + name: argonaut-secrets + key: SMTP_Host + - name: SMTP_Password + valueFrom: + secretKeyRef: + name: argonaut-secrets + key: SMTP_Password + - name: SMTP_Port + valueFrom: + secretKeyRef: + name: argonaut-secrets + key: SMTP_Port + - name: SMTP_User + valueFrom: + secretKeyRef: + name: argonaut-secrets + key: SMTP_User + ports: + - containerPort: 5826 +--- +apiVersion: v1 +kind: Service +metadata: + namespace: subscriptions-ri + name: ri-subscriptions +spec: + selector: + app: ri-subscriptions + ports: + - protocol: TCP + port: 80 + targetPort: 5826 +--- +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: subscriptions-ingress + namespace: subscriptions-ri + annotations: + cert-manager.io/cluster-issuer: letsencrypt-nginx +spec: + tls: + - hosts: + - subscriptions.argo.run + - cdex.ri.argo.run + - ecr.ri.argo.run + - vitals-server.ri.argo.run + - feature-cs-server.ri.argo.run + secretName: tls-secret + rules: + - host: subscriptions.argo.run + http: + paths: + - backend: + service: + name: ri-subscriptions + port: + number: 80 + path: / + pathType: Prefix + - host: cdex.ri.argo.run + http: + paths: + - backend: + service: + name: ri-cdex + port: + number: 80 + path: / + pathType: Prefix + - host: ecr.ri.argo.run + http: + paths: + - backend: + service: + name: ri-ecr + port: + number: 80 + path: / + pathType: Prefix + - host: vitals-server.ri.argo.run + http: + paths: + - backend: + service: + name: ri-vitals-server + port: + number: 80 + path: / + pathType: Prefix + - host: feature-cs-server.ri.argo.run + http: + paths: + - backend: + service: + name: ri-feature-cs-server + port: + number: 80 + path: / + pathType: Prefix diff --git a/fhir-candle.sln b/fhir-candle.sln index 8585f33..51139be 100644 --- a/fhir-candle.sln +++ b/fhir-candle.sln @@ -21,14 +21,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitignore = .gitignore - .github\workflows\argo-subscriptions.yml = .github\workflows\argo-subscriptions.yml - .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml CONTRIBUTING.MD = CONTRIBUTING.MD Dockerfile = Dockerfile fhir-candle.props = fhir-candle.props - .github\workflows\ghcr-docker.yml = .github\workflows\ghcr-docker.yml LICENSE = LICENSE - .github\workflows\nuget-tool.yml = .github\workflows\nuget-tool.yml README.md = README.md THIRDPARTYNOTICES.md = THIRDPARTYNOTICES.md EndProjectSection @@ -41,6 +37,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FhirCandle.Ui.R5", "src\Fhi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FhirCandle.Ui.Common", "src\FhirCandle.Ui.Common\FhirCandle.Ui.Common.csproj", "{2276D057-68A6-4639-A821-0C028D8449D0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".deployments", ".deployments", "{C08AFD85-C98F-4DEA-8915-AEE3CF5A7C15}" + ProjectSection(SolutionItems) = preProject + .deployments\k8-argo-run-ris.yaml = .deployments\k8-argo-run-ris.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1AAC7962-65B4-42CD-AD5F-3EE8BD3149D0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{22C11CB9-2408-4056-B41E-87DD07AECF94}" + ProjectSection(SolutionItems) = preProject + .github\workflows\argo-subscriptions.yml = .github\workflows\argo-subscriptions.yml + .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml + .github\workflows\ghcr-docker.yml = .github\workflows\ghcr-docker.yml + .github\workflows\nuget-tool.yml = .github\workflows\nuget-tool.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -91,6 +102,11 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C08AFD85-C98F-4DEA-8915-AEE3CF5A7C15} = {8B6BFF93-0260-40D3-BF26-175BC295D77B} + {1AAC7962-65B4-42CD-AD5F-3EE8BD3149D0} = {8B6BFF93-0260-40D3-BF26-175BC295D77B} + {22C11CB9-2408-4056-B41E-87DD07AECF94} = {1AAC7962-65B4-42CD-AD5F-3EE8BD3149D0} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2F9896CC-1A33-4A99-95F9-8D13DF291779} EndGlobalSection From b0c5e02849fecb556524820ad6d5542bf2d056af Mon Sep 17 00:00:00 2001 From: Gino Canessa Date: Wed, 28 Aug 2024 11:29:18 -0500 Subject: [PATCH 2/5] Pass of updating to Firely.Packages. Added multi-target (.Net 8 and NetStandard 2) for Common. Refactoring of FHIR Release tracking for sanity. Prep config objects for new functionality (remote load and reload) --- .deployments/k8-argo-run-ris.yaml | 2 +- .../FhirCandle.Ui.Common.csproj | 6 +- .../Models/IPackagePage.cs | 18 +- src/FhirCandle.Ui.R4/FhirCandle.Ui.R4.csproj | 6 +- .../FhirCandle.Ui.R4B.csproj | 6 +- src/FhirCandle.Ui.R5/FhirCandle.Ui.R5.csproj | 6 +- .../Client/CandleClientSettings.cs | 1 - .../Configuration/CandleConfig.cs | 1015 +++++++++ .../Configuration/ConfigOptionAttribute.cs | 25 + .../Configuration/ConfigurationOption.cs | 21 + src/FhirStore.Common/FhirCandle.Common.csproj | 17 +- .../Models/FhirNpmPackageDetails.cs | 8 +- .../Models/FhirRequestContext.cs | 6 +- .../Models/TenantConfiguration.cs | 118 +- .../Polyfill/CandleCommonPolyfill.cs | 186 ++ src/FhirStore.Common/Search/Common.cs | 12 + .../Serialization/SerializationCommon.cs | 4 + src/FhirStore.Common/Storage/IFhirStore.cs | 5 +- src/FhirStore.Common/Utils/FhirReleases.cs | 429 ++++ .../FhirStore.CommonVersioned.projitems | 2 +- .../Interactions/IFhirInteractionHook.cs | 2 +- .../Operations/IFhirOperation.cs | 6 +- .../Operations/OpFeatureQuery.cs | 14 +- .../Operations/OpIsFhir.cs | 16 +- .../Operations/OpSubscriptionEvents.cs | 20 +- .../Operations/OpSubscriptionHook.cs | 16 +- .../Operations/OpSubscriptionStatus.cs | 12 +- .../Search/EvalDateSearch.cs | 2 +- .../{Utils.cs => SerializationUtils.cs} | 2 +- .../Storage/ResourceStore.cs | 22 +- .../Storage/VersionedFhirStore.cs | 315 +-- src/FhirStore.R4/FhirCandle.R4.csproj | 2 +- .../InteractionHooks/CDexTaskProcess.cs | 4 +- .../Operations/OpPasClaimInquiry.cs | 6 +- .../Operations/OpPasClaimSubmit.cs | 6 +- src/FhirStore.R4B/FhirCandle.R4B.csproj | 2 +- src/FhirStore.R5/FhirCandle.R5.csproj | 2 +- src/fhir-candle.Tests/AuthTests.cs | 11 +- src/fhir-candle.Tests/FhirStoreTests.cs | 49 +- src/fhir-candle.Tests/FhirStoreTestsR4.cs | 5 +- src/fhir-candle.Tests/FhirStoreTestsR4B.cs | 5 +- src/fhir-candle.Tests/FhirStoreTestsR5.cs | 5 +- src/fhir-candle.Tests/R4BTests.cs | 11 +- src/fhir-candle.Tests/R4Tests.cs | 21 +- src/fhir-candle.Tests/R5Tests.cs | 9 +- .../fhir-candle.Tests.csproj | 6 +- src/fhir-candle/Layout/MainLayout.razor | 5 +- .../Models/RegistryPackageManifest.cs | 12 +- src/fhir-candle/Models/ServerConfiguration.cs | 94 - src/fhir-candle/Pages/Index.razor | 17 +- .../Pages/RI/subscriptions/Tour.razor | 16 +- .../Pages/Store/ResourceViewer.razor | 17 +- src/fhir-candle/Program.cs | 442 ++-- .../Properties/launchSettings.json | 2 +- .../Services/FhirPackageService.cs | 1993 ++++------------- src/fhir-candle/Services/FhirStoreManager.cs | 294 +-- .../Services/IFhirPackageService.cs | 37 +- .../Services}/IFhirStoreManager.cs | 3 +- .../Services/NotificationManager.cs | 15 +- src/fhir-candle/Services/SmartAuthManager.cs | 5 +- .../_ForPackages/AuthorJsonConverter.cs | 59 + .../_ForPackages/DiskPackageCache.cs | 63 + src/fhir-candle/_ForPackages/FhirCiClient.cs | 1089 +++++++++ src/fhir-candle/_ForPackages/JsonModels.cs | 426 ++++ .../_ForPackages/ManifestDateJsonConverter.cs | 50 + .../_ForPackages/VersionExtensions.cs | 65 + src/fhir-candle/_Imports.razor | 1 + src/fhir-candle/fhir-candle.csproj | 7 +- 68 files changed, 4621 insertions(+), 2555 deletions(-) rename src/{FhirStore.Common => FhirCandle.Ui.Common}/Models/IPackagePage.cs (85%) create mode 100644 src/FhirStore.Common/Configuration/CandleConfig.cs create mode 100644 src/FhirStore.Common/Configuration/ConfigOptionAttribute.cs create mode 100644 src/FhirStore.Common/Configuration/ConfigurationOption.cs create mode 100644 src/FhirStore.Common/Polyfill/CandleCommonPolyfill.cs create mode 100644 src/FhirStore.Common/Utils/FhirReleases.cs rename src/FhirStore.CommonVersioned/Serialization/{Utils.cs => SerializationUtils.cs} (99%) delete mode 100644 src/fhir-candle/Models/ServerConfiguration.cs rename src/{FhirStore.Common/Storage => fhir-candle/Services}/IFhirStoreManager.cs (95%) create mode 100644 src/fhir-candle/_ForPackages/AuthorJsonConverter.cs create mode 100644 src/fhir-candle/_ForPackages/DiskPackageCache.cs create mode 100644 src/fhir-candle/_ForPackages/FhirCiClient.cs create mode 100644 src/fhir-candle/_ForPackages/JsonModels.cs create mode 100644 src/fhir-candle/_ForPackages/ManifestDateJsonConverter.cs create mode 100644 src/fhir-candle/_ForPackages/VersionExtensions.cs diff --git a/.deployments/k8-argo-run-ris.yaml b/.deployments/k8-argo-run-ris.yaml index 792ec49..ac9920e 100644 --- a/.deployments/k8-argo-run-ris.yaml +++ b/.deployments/k8-argo-run-ris.yaml @@ -82,7 +82,7 @@ metadata: name: subscriptions-ingress namespace: subscriptions-ri annotations: - cert-manager.io/cluster-issuer: letsencrypt-nginx + cert-manager.io/cluster-issuer: letsencrypt-prod spec: tls: - hosts: diff --git a/src/FhirCandle.Ui.Common/FhirCandle.Ui.Common.csproj b/src/FhirCandle.Ui.Common/FhirCandle.Ui.Common.csproj index acfb903..673f853 100644 --- a/src/FhirCandle.Ui.Common/FhirCandle.Ui.Common.csproj +++ b/src/FhirCandle.Ui.Common/FhirCandle.Ui.Common.csproj @@ -8,11 +8,11 @@ - + - + - + diff --git a/src/FhirStore.Common/Models/IPackagePage.cs b/src/FhirCandle.Ui.Common/Models/IPackagePage.cs similarity index 85% rename from src/FhirStore.Common/Models/IPackagePage.cs rename to src/FhirCandle.Ui.Common/Models/IPackagePage.cs index 71355c3..87e51b8 100644 --- a/src/FhirStore.Common/Models/IPackagePage.cs +++ b/src/FhirCandle.Ui.Common/Models/IPackagePage.cs @@ -1,8 +1,14 @@ -// +// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + namespace FhirCandle.Models; /// Information about the package page. @@ -14,11 +20,11 @@ namespace FhirCandle.Models; /// The FHIR version numeric. /// The only show on endpoint. public record struct PackagePageInfo( - string ContentFor, - string PageName, - string Description, - string RoutePath, - string FhirVersionLiteral, + string ContentFor, + string PageName, + string Description, + string RoutePath, + string FhirVersionLiteral, string FhirVersionNumeric, string OnlyShowOnEndpoint); diff --git a/src/FhirCandle.Ui.R4/FhirCandle.Ui.R4.csproj b/src/FhirCandle.Ui.R4/FhirCandle.Ui.R4.csproj index e11b4d6..02e8207 100644 --- a/src/FhirCandle.Ui.R4/FhirCandle.Ui.R4.csproj +++ b/src/FhirCandle.Ui.R4/FhirCandle.Ui.R4.csproj @@ -15,10 +15,10 @@ - + - - + + diff --git a/src/FhirCandle.Ui.R4B/FhirCandle.Ui.R4B.csproj b/src/FhirCandle.Ui.R4B/FhirCandle.Ui.R4B.csproj index efeaf2e..6aa6a8f 100644 --- a/src/FhirCandle.Ui.R4B/FhirCandle.Ui.R4B.csproj +++ b/src/FhirCandle.Ui.R4B/FhirCandle.Ui.R4B.csproj @@ -15,10 +15,10 @@ - + - - + + diff --git a/src/FhirCandle.Ui.R5/FhirCandle.Ui.R5.csproj b/src/FhirCandle.Ui.R5/FhirCandle.Ui.R5.csproj index e59abde..ad7a586 100644 --- a/src/FhirCandle.Ui.R5/FhirCandle.Ui.R5.csproj +++ b/src/FhirCandle.Ui.R5/FhirCandle.Ui.R5.csproj @@ -15,10 +15,10 @@ - + - - + + diff --git a/src/FhirStore.Common/Client/CandleClientSettings.cs b/src/FhirStore.Common/Client/CandleClientSettings.cs index 4c69f83..557431d 100644 --- a/src/FhirStore.Common/Client/CandleClientSettings.cs +++ b/src/FhirStore.Common/Client/CandleClientSettings.cs @@ -4,7 +4,6 @@ // -using static System.Runtime.InteropServices.JavaScript.JSType; using System.Net; using FhirCandle.Extensions; diff --git a/src/FhirStore.Common/Configuration/CandleConfig.cs b/src/FhirStore.Common/Configuration/CandleConfig.cs new file mode 100644 index 0000000..3975435 --- /dev/null +++ b/src/FhirStore.Common/Configuration/CandleConfig.cs @@ -0,0 +1,1015 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.CommandLine.Parsing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#if NETSTANDARD2_0 +using FhirCandle.Polyfill; +#endif + +namespace FhirCandle.Configuration; + +/// Main configuration class for FHIR-Candle. +public class CandleConfig +{ + /// (Immutable) The default listen port. + private const int _defaultListenPort = 5826; + + /// Gets or sets URL of the public. + [ConfigOption( + ArgAliases = ["--url", "-u"], + EnvName = "Public_Url", + Description = "Public URL for the server")] + public string PublicUrl { get; set; } = string.Empty; + + /// Gets the public URL option. + private static ConfigurationOption PublicUrlParameter { get; } = new() + { + Name = "PublicUrl", + EnvVarName = "Public_Url", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option(["--url", "-u"], "Public URL for the server") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the listen port. + [ConfigOption( + ArgName = "--port", + EnvName = "Listen_Port", + Description = "TCP port to listen on")] + public int ListenPort { get; set; } = _defaultListenPort; + + /// Gets the listen port option. + private static ConfigurationOption ListenPortParameter { get; } = new() + { + Name = "ListenPort", + EnvVarName = "Listen_Port", + DefaultValue = _defaultListenPort, + CliOption = new System.CommandLine.Option("--port", "TCP port to listen on") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets a value indicating whether the browser should be opened. + [ConfigOption( + ArgAliases = ["--open-browser", "-o"], + EnvName = "Open_Browser", + Description = "Open the browser to the public URL once the server starts")] + public bool OpenBrowser { get; set; } = false; + + /// Gets the open browser option. + private static ConfigurationOption OpenBrowserParameter { get; } = new() + { + Name = "OpenBrowser", + EnvVarName = "Open_Browser", + DefaultValue = false, + CliOption = new System.CommandLine.Option(["--open-browser", "-o"], "Open the browser to the public URL once the server starts") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the maximum resources. + [ConfigOption( + ArgAliases = ["--max-resources", "-m"], + EnvName = "Max_Resources", + Description = "Maximum number of resources allowed per tenant")] + public int MaxResourceCount { get; set; } = 0; + + /// Gets the maximum resources option. + private static ConfigurationOption MaxResourceCountParameter { get; } = new() + { + Name = "MaxResources", + EnvVarName = "Max_Resources", + DefaultValue = 0, + CliOption = new System.CommandLine.Option(["--max-resources", "-m"], "Maximum number of resources allowed per tenant") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// + /// Gets or sets a value indicating whether the user interface is disabled. + /// + [ConfigOption( + ArgName = "--disable-ui", + EnvName = "Disable_Ui", + Description = "Set to disable the UI (run server headless)")] + public bool DisableUi { get; set; } = false; + + /// Gets the disable user interface option. + private static ConfigurationOption DisableUiParameter { get; } = new() + { + Name = "DisableUi", + EnvVarName = "Disable_Ui", + DefaultValue = false, + CliOption = new System.CommandLine.Option("--disable-ui", "Set to disable the UI (run server headless)") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the pathname of the FHIR package cache directory. + [ConfigOption( + ArgName = "--fhir-package-cache", + EnvName = "Fhir_Package_Cache", + Description = "Location of the FHIR package cache, for use with registries and IG packages. Not specified defaults to ~/.fhir.")] + public string? FhirCacheDirectory { get; set; } = null; + + /// Gets the FHIR package cache directory option. + private static ConfigurationOption FhirCacheDirectoryParameter { get; } = new() + { + Name = "FhirPackageCache", + EnvVarName = "Fhir_Package_Cache", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--fhir-package-cache", "Location of the FHIR package cache, for use with registries and IG packages. Not specified defaults to ~/.fhir.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + [ConfigOption( + ArgName = "--use-official-registries", + EnvName = "Use_Official_Registries", + Description = "Use official FHIR registries to resolve packages.")] + public bool UseOfficialRegistries { get; set; } = true; + + private static ConfigurationOption UseOfficialRegistriesParameter { get; } = new() + { + Name = "UseOfficialRegistries", + EnvVarName = "Use_Official_Registries", + DefaultValue = true, + CliOption = new System.CommandLine.Option("--use-official-registries", "Use official FHIR registries to resolve packages.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + [ConfigOption( + ArgName = "--additional-fhir-registry-urls", + EnvName = "Additional_FHIR_Registry_Urls", + ArgArity = "0..*", + Description = "Additional FHIR registry URLs to use.")] + public string[] AdditionalFhirRegistryUrls { get; set; } = Array.Empty(); + + private static ConfigurationOption AdditionalFhirRegistryUrlsParameter { get; } = new() + { + Name = "AdditionalFhirRegistryUrls", + EnvVarName = "Additional_FHIR_Registry_Urls", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option("--additional-fhir-registry-urls", "Additional FHIR registry URLs to use.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + [ConfigOption( + ArgName = "--additional-npm-registry-urls", + EnvName = "Additional_NPM_Registry_Urls", + ArgArity = "0..*", + Description = "Additional NPM registry URLs to use.")] + public string[] AdditionalNpmRegistryUrls { get; set; } = Array.Empty(); + + private static ConfigurationOption AdditionalNpmRegistryUrlsParameter { get; } = new() + { + Name = "AdditionalNpmRegistryUrls", + EnvVarName = "Additional_NPM_Registry_Urls", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option("--additional-npm-registry-urls", "Additional NPM registry URLs to use.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + /// Gets or sets the FHIR packages. + [ConfigOption( + ArgAliases = ["--load-package", "-p"], + EnvName = "Fhir_Package", + Description = "FHIR package to load on startup, specified by directive. Can be specified multiple times.")] + public string[] PublishedPackages { get; set; } = []; + + /// Gets the FHIR packages option. + private static ConfigurationOption PublishedPackagesParameter { get; } = new() + { + Name = "FhirPackages", + EnvVarName = "Fhir_Package", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option(["--load-package", "-p"], "FHIR package to load on startup, specified by directive. Can be specified multiple times.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + /// Gets or sets the FHIR ci packages. + [ConfigOption( + ArgName = "--ci-package", + EnvName = "Fhir_Ci_Package", + Description = "FHIR package to load on startup, specified by directive. Can be specified multiple times.")] + public string[] CiPackages { get; set; } = []; + + /// Gets the FHIR ci packages option. + private static ConfigurationOption CiPackagesParameter { get; } = new() + { + Name = "FhirCiPackages", + EnvVarName = "Fhir_Ci_Package", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option("--ci-package", "FHIR package to load on startup, specified by directive. Can be specified multiple times.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + /// Gets or sets a value indicating whether the examples should be loaded. + [ConfigOption( + ArgName = "--load-examples", + EnvName = "Load_Examples", + Description = "If package loading should include example instances")] + public bool LoadPackageExamples { get; set; } = false; + + /// Gets the load examples option. + private static ConfigurationOption LoadPackageExamplesParameter { get; } = new() + { + Name = "LoadExamples", + EnvVarName = "Load_Examples", + DefaultValue = false, + CliOption = new System.CommandLine.Option("--load-examples", "If package loading should include example instances") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the reference implementation. + [ConfigOption( + ArgName = "--reference-implementation", + EnvName = "Reference_Implementation", + Description = "If running as the Reference Implementation, the package directive or literal.")] + public string? ReferenceImplementation { get; set; } = null; + + /// Gets the reference implementation option. + private static ConfigurationOption ReferenceImplementationParameter { get; } = new() + { + Name = "ReferenceImplementation", + EnvVarName = "Reference_Implementation", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--reference-implementation", "If running as the Reference Implementation, the package directive or literal.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the pathname of the FHIR source directory. + [ConfigOption( + ArgName = "--fhir-source", + EnvName = "Fhir_Source_Directory", + Description = "FHIR Contents to load, either in this directory or by subdirectories named per tenant.")] + public string? SourceDirectory { get; set; } = null; + + /// Gets the FHIR source directory option. + private static ConfigurationOption SourceDirectoryParameter { get; } = new() + { + Name = "FhirSourceDirectory", + EnvVarName = "Fhir_Source_Directory", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--fhir-source", "FHIR Contents to load, either in this directory or by subdirectories named per tenant.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets a value indicating whether the protect loaded content. + [ConfigOption( + ArgAliases = ["--protect-source", "--protect-loaded-content"], + EnvName = "Protect_Source", + Description = "If set, loaded content will be protected from modification.")] + public bool ProtectLoadedContent { get; set; } = false; + + /// Gets the protect loaded content option. + private static ConfigurationOption ProtectLoadedContentParameter { get; } = new() + { + Name = "ProtectLoadedContent", + EnvVarName = "Protect_Source", + DefaultValue = false, + CliOption = new System.CommandLine.Option(["--protect-source", "--protect-loaded-content"], "If set, loaded content will be protected from modification.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// The fourth tenants r. + [ConfigOption( + ArgName = "--r4", + EnvName = "Tenants_R4", + Description = "FHIR R4 Tenants to create. Can be specified multiple times.")] + public string[] TenantsR4 = []; + + /// Gets the tenants r 4 option. + private static ConfigurationOption TenantsR4Parameter { get; } = new() + { + Name = "TenantsR4", + EnvVarName = "Tenants_R4", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option("--r4", "FHIR R4 Tenants to create. Can be specified multiple times.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + /// The tenants r 4 b. + [ConfigOption( + ArgName = "--r4b", + EnvName = "Tenants_R4B", + Description = "FHIR R4B Tenants to create. Can be specified multiple times.")] + public string[] TenantsR4B = []; + + /// Gets the tenants r 4 b option. + private static ConfigurationOption TenantsR4BParameter { get; } = new() + { + Name = "TenantsR4B", + EnvVarName = "Tenants_R4B", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option("--r4b", "FHIR R4B Tenants to create. Can be specified multiple times.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + /// The fifth tenants r. + [ConfigOption( + ArgName = "--r5", + EnvName = "Tenants_R5", + Description = "FHIR R5 Tenants to create. Can be specified multiple times.")] + public string[] TenantsR5 = []; + + /// Gets the tenants r 5 option. + private static ConfigurationOption TenantsR5Parameter { get; } = new() + { + Name = "TenantsR5", + EnvVarName = "Tenants_R5", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option("--r5", "FHIR R5 Tenants to create. Can be specified multiple times.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + /// The smart required tenants. + [ConfigOption( + ArgName = "--smart-required", + EnvName = "Smart_Required_Tenants", + Description = "Tenants that require SMART on FHIR support.")] + public string[] SmartRequiredTenants = []; + + /// Gets the smart required tenants option. + private static ConfigurationOption SmartRequiredTenantsParameter { get; } = new() + { + Name = "SmartRequiredTenants", + EnvVarName = "Smart_Required_Tenants", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option("--smart-required", "Tenants that require SMART on FHIR support.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + /// The smart optional tenants. + [ConfigOption( + ArgName = "--smart-optional", + EnvName = "Smart_Optional_Tenants", + Description = "Tenants that support SMART on FHIR but do not require it.")] + public string[] SmartOptionalTenants = []; + + /// Gets the smart optional tenants option. + private static ConfigurationOption SmartOptionalTenantsParameter { get; } = new() + { + Name = "SmartOptionalTenants", + EnvVarName = "Smart_Optional_Tenants", + DefaultValue = Array.Empty(), + CliOption = new System.CommandLine.Option("--smart-optional", "Tenants that support SMART on FHIR but do not require it.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrMore, + IsRequired = false, + }, + }; + + /// + /// Gets or sets a value indicating whether the create existing identifier is enabled. + /// + [ConfigOption( + ArgName = "--create-existing-id", + EnvName = "Create_Existing_Id", + Description = "Allow Create interactions (POST) to specify an ID.")] + public bool AllowExistingId { get; set; } = true; + + /// Gets the enable create existing identifier option. + private static ConfigurationOption AllowExistingIdParameter { get; } = new() + { + Name = "CreateExistingId", + EnvVarName = "Create_Existing_Id", + DefaultValue = true, + CliOption = new System.CommandLine.Option("--create-existing-id", "Allow Create interactions (POST) to specify an ID.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// + /// Gets or sets a value indicating whether the create as update is enabled. + /// + [ConfigOption( + ArgName = "--create-as-update", + EnvName = "Create_As_Update", + Description = "Allow Update interactions (PUT) to create new resources.")] + public bool AllowCreateAsUpdate { get; set; } = true; + + /// Gets the enable create as update option. + private static ConfigurationOption AllowCreateAsUpdateParameter { get; } = new() + { + Name = "CreateAsUpdate", + EnvVarName = "Create_As_Update", + DefaultValue = true, + CliOption = new System.CommandLine.Option("--create-as-update", "Allow Update interactions (PUT) to create new resources.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the maximum subscription minutes. + [ConfigOption( + ArgName = "--max-subscription-minutes", + EnvName = "Max_Subscription_Minutes", + Description = "Maximum number of minutes a subscription can be active.")] + public int MaxSubscriptionExpirationMinutes { get; set; } = 0; + + /// Gets the maximum subscription minutes option. + private static ConfigurationOption MaxSubscriptionExpirationMinutesParameter { get; } = new() + { + Name = "MaxSubscriptionMinutes", + EnvVarName = "Max_Subscription_Minutes", + DefaultValue = 0, + CliOption = new System.CommandLine.Option("--max-subscription-minutes", "Maximum number of minutes a subscription can be active.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + + /// Gets or sets the zulip email. + [ConfigOption( + ArgName = "--zulip-email", + EnvName = "Zulip_Email", + Description = "Zulip bot email address to use for Zulip notifications.")] + public string? ZulipEmail { get; set; } = null; + + /// Gets the zulip email option. + private static ConfigurationOption ZulipEmailParameter { get; } = new() + { + Name = "ZulipEmail", + EnvVarName = "Zulip_Email", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--zulip-email", "Zulip bot email address to use for Zulip notifications.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the zulip key. + [ConfigOption( + ArgName = "--zulip-key", + EnvName = "Zulip_Key", + Description = "Zulip bot API key to use for Zulip notifications.")] + public string? ZulipKey { get; set; } = null; + + /// Gets the zulip key option. + private static ConfigurationOption ZulipKeyParameter { get; } = new() + { + Name = "ZulipKey", + EnvVarName = "Zulip_Key", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--zulip-key", "Zulip bot API key to use for Zulip notifications.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets URL of the zulip. + [ConfigOption( + ArgName = "--zulip-url", + EnvName = "Zulip_Url", + Description = "Zulip server URL to use for Zulip notifications.")] + public string? ZulipUrl { get; set; } = null; + + /// Gets the zulip URL option. + private static ConfigurationOption ZulipUrlParameter { get; } = new() + { + Name = "ZulipUrl", + EnvVarName = "Zulip_Url", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--zulip-url", "Zulip server URL to use for Zulip notifications.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the SMTP host. + [ConfigOption( + ArgName = "--smtp-host", + EnvName = "SMTP_Host", + Description = "SMTP host to use for email notifications.")] + public string? SmtpHost { get; set; } = null; + + /// Gets the SMTP host option. + private static ConfigurationOption SmtpHostParameter { get; } = new() + { + Name = "SmtpHost", + EnvVarName = "SMTP_Host", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--smtp-host", "SMTP host to use for email notifications.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the SMTP port. + [ConfigOption( + ArgName = "--smtp-port", + EnvName = "SMTP_Port", + Description = "SMTP port to use for email notifications.")] + public int SmtpPort { get; set; } = 465; + + /// Gets the SMTP port option. + private static ConfigurationOption SmtpPortParameter { get; } = new() + { + Name = "SmtpPort", + EnvVarName = "SMTP_Port", + DefaultValue = 465, + CliOption = new System.CommandLine.Option("--smtp-port", "SMTP port to use for email notifications.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the SMTP user. + [ConfigOption( + ArgName = "--smtp-user", + EnvName = "SMTP_User", + Description = "SMTP user to use for email notifications.")] + public string? SmtpUser { get; set; } = null; + + /// Gets the SMTP user option. + private static ConfigurationOption SmtpUserParameter { get; } = new() + { + Name = "SmtpUser", + EnvVarName = "SMTP_User", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--smtp-user", "SMTP user to use for email notifications.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets the SMTP password. + [ConfigOption( + ArgName = "--smtp-password", + EnvName = "SMTP_Password", + Description = "SMTP password to use for email notifications.")] + public string? SmtpPassword { get; set; } = null; + + /// Gets the SMTP password option. + private static ConfigurationOption SmtpPasswordParameter { get; } = new() + { + Name = "SmtpPassword", + EnvVarName = "SMTP_Password", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--smtp-password", "SMTP password to use for email notifications.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// Gets or sets URL of the FHIR path lab. + [ConfigOption( + ArgName = "--fhirpath-lab-url", + EnvName = "FhirPath_Lab_Url", + Description = "FHIRPath Lab URL to use for external FHIRPath tests.")] + public string? FhirPathLabUrl { get; set; } = null; + + /// Gets the FHIR path lab URL option. + private static ConfigurationOption FhirPathLabUrlParameter { get; } = new() + { + Name = "FhirPathLabUrl", + EnvVarName = "FhirPath_Lab_Url", + DefaultValue = string.Empty, + CliOption = new System.CommandLine.Option("--fhirpath-lab-url", "FHIRPath Lab URL to use for external FHIRPath tests.") + { + Arity = System.CommandLine.ArgumentArity.ZeroOrOne, + IsRequired = false, + }, + }; + + /// (Immutable) Options for controlling the operation. + private static readonly ConfigurationOption[] _options = + [ + PublicUrlParameter, + ListenPortParameter, + OpenBrowserParameter, + MaxResourceCountParameter, + DisableUiParameter, + FhirCacheDirectoryParameter, + UseOfficialRegistriesParameter, + AdditionalFhirRegistryUrlsParameter, + AdditionalNpmRegistryUrlsParameter, + PublishedPackagesParameter, + CiPackagesParameter, + LoadPackageExamplesParameter, + ReferenceImplementationParameter, + SourceDirectoryParameter, + ProtectLoadedContentParameter, + TenantsR4Parameter, + TenantsR4BParameter, + TenantsR5Parameter, + SmartRequiredTenantsParameter, + SmartOptionalTenantsParameter, + AllowExistingIdParameter, + AllowCreateAsUpdateParameter, + MaxSubscriptionExpirationMinutesParameter, + ZulipEmailParameter, + ZulipKeyParameter, + ZulipUrlParameter, + SmtpHostParameter, + SmtpPortParameter, + SmtpUserParameter, + SmtpPasswordParameter, + FhirPathLabUrlParameter, + ]; + + /// Parses the given parse result. + /// The parse result. + public virtual void Parse(System.CommandLine.Parsing.ParseResult parseResult) + { + foreach (ConfigurationOption opt in _options) + { + switch (opt.Name) + { + case "PublicUrl": + PublicUrl = GetOpt(parseResult, opt.CliOption, PublicUrl); + break; + case "ListenPort": + ListenPort = GetOpt(parseResult, opt.CliOption, ListenPort); + break; + case "OpenBrowser": + OpenBrowser = GetOpt(parseResult, opt.CliOption, OpenBrowser); + break; + case "MaxResources": + MaxResourceCount = GetOpt(parseResult, opt.CliOption, MaxResourceCount); + break; + case "DisableUi": + DisableUi = GetOpt(parseResult, opt.CliOption, DisableUi); + break; + case "FhirPackageCacheDirectory": + { + string? dir = GetOpt(parseResult, opt.CliOption, FhirCacheDirectory); + FhirCacheDirectory = string.IsNullOrEmpty(dir) ? null : dir; + } + break; + case "UseOfficialRegistries": + UseOfficialRegistries = GetOpt(parseResult, opt.CliOption, UseOfficialRegistries); + break; + case "AdditionalFhirRegistryUrls": + AdditionalFhirRegistryUrls = GetOptArray(parseResult, opt.CliOption, AdditionalFhirRegistryUrls); + break; + case "AdditionalNpmRegistryUrls": + AdditionalNpmRegistryUrls = GetOptArray(parseResult, opt.CliOption, AdditionalNpmRegistryUrls); + break; + case "FhirPackages": + PublishedPackages = GetOptArray(parseResult, opt.CliOption, PublishedPackages); + break; + case "FhirCiPackages": + CiPackages = GetOptArray(parseResult, opt.CliOption, CiPackages); + break; + case "LoadExamples": + LoadPackageExamples = GetOpt(parseResult, opt.CliOption, LoadPackageExamples); + break; + case "ReferenceImplementation": + ReferenceImplementation = GetOpt(parseResult, opt.CliOption, ReferenceImplementation); + break; + case "FhirSourceDirectory": + { + string? dir = GetOpt(parseResult, opt.CliOption, SourceDirectory); + SourceDirectory = string.IsNullOrEmpty(dir) ? null : dir; + } + break; + case "ProtectLoadedContent": + ProtectLoadedContent = GetOpt(parseResult, opt.CliOption, ProtectLoadedContent); + break; + case "TenantsR4": + TenantsR4 = GetOptArray(parseResult, opt.CliOption, TenantsR4); + break; + case "TenantsR4B": + TenantsR4B = GetOptArray(parseResult, opt.CliOption, TenantsR4B); + break; + case "TenantsR5": + TenantsR5 = GetOptArray(parseResult, opt.CliOption, TenantsR5); + break; + case "SmartRequiredTenants": + SmartRequiredTenants = GetOptArray(parseResult, opt.CliOption, SmartRequiredTenants); + break; + case "SmartOptionalTenants": + SmartOptionalTenants = GetOptArray(parseResult, opt.CliOption, SmartOptionalTenants); + break; + case "CreateExistingId": + AllowExistingId = GetOpt(parseResult, opt.CliOption, AllowExistingId); + break; + case "CreateAsUpdate": + AllowCreateAsUpdate = GetOpt(parseResult, opt.CliOption, AllowCreateAsUpdate); + break; + case "MaxSubscriptionMinutes": + MaxSubscriptionExpirationMinutes = GetOpt(parseResult, opt.CliOption, MaxSubscriptionExpirationMinutes); + break; + case "ZulipEmail": + ZulipEmail = GetOpt(parseResult, opt.CliOption, ZulipEmail); + break; + case "ZulipKey": + ZulipKey = GetOpt(parseResult, opt.CliOption, ZulipKey); + break; + case "ZulipUrl": + ZulipUrl = GetOpt(parseResult, opt.CliOption, ZulipUrl); + break; + case "SmtpHost": + SmtpHost = GetOpt(parseResult, opt.CliOption, SmtpHost); + break; + case "SmtpPort": + SmtpPort = GetOpt(parseResult, opt.CliOption, SmtpPort); + break; + case "SmtpUser": + SmtpUser = GetOpt(parseResult, opt.CliOption, SmtpUser); + break; + case "SmtpPassword": + SmtpPassword = GetOpt(parseResult, opt.CliOption, SmtpPassword); + break; + case "FhirPathLabUrl": + FhirPathLabUrl = GetOpt(parseResult, opt.CliOption, FhirPathLabUrl); + break; + } + } + } + + /// Gets the array of configuration options. + /// An array of configuration option. + public virtual ConfigurationOption[] GetOptions() => _options; + + /// Gets an option. + /// Generic type parameter. + /// The parse result. + /// The option. + /// The default value. + /// The option. + internal T GetOpt( + System.CommandLine.Parsing.ParseResult parseResult, + System.CommandLine.Option opt, + T defaultValue) + { + if (!parseResult.HasOption(opt)) + { + return defaultValue; + } + + object? parsed = parseResult.GetValueForOption(opt); + + if ((parsed != null) && + (parsed is T typed)) + { + return typed; + } + + return defaultValue; + } + + /// Gets option array. + /// Thrown when an exception error condition occurs. + /// Generic type parameter. + /// The parse result. + /// The option. + /// The default value. + /// An array of t. + internal T[] GetOptArray( + System.CommandLine.Parsing.ParseResult parseResult, + System.CommandLine.Option opt, + T[] defaultValue) + { + if (!parseResult.HasOption(opt)) + { + return defaultValue; + } + + object? parsed = parseResult.GetValueForOption(opt); + + if (parsed == null) + { + return defaultValue; + } + + List values = []; + + if (parsed is T[] array) + { + return array; + } + else if (parsed is IEnumerator genericEnumerator) + { + // use the enumerator to add values to the array + while (genericEnumerator.MoveNext()) + { + if (genericEnumerator.Current is T tValue) + { + values.Add(tValue); + } + else + { + throw new Exception("Should not be here!"); + } + } + } + else if (parsed is IEnumerator enumerator) + { + // use the enumerator to add values to the array + while (enumerator.MoveNext()) + { + values.Add(enumerator.Current); + } + } + else + { + throw new Exception("Should not be here!"); + } + + // if no values were added, return the default - parser cannot tell the difference between no values and default values + if (values.Count == 0) + { + return defaultValue; + } + + return [.. values]; + } + + /// Gets option hash. + /// Thrown when an exception error condition occurs. + /// Generic type parameter. + /// The parse result. + /// The option. + /// The default value. + /// The option hash. + internal HashSet GetOptHash( + System.CommandLine.Parsing.ParseResult parseResult, + System.CommandLine.Option opt, + HashSet defaultValue) + { + if (!parseResult.HasOption(opt)) + { + return defaultValue; + } + + object? parsed = parseResult.GetValueForOption(opt); + + if (parsed == null) + { + return defaultValue; + } + + HashSet values = []; + + if (parsed is IEnumerator typed) + { + // use the enumerator to add values to the array + while (typed.MoveNext()) + { + values.Add(typed.Current); + } + } + else + { + throw new Exception("Should not be here!"); + } + + // if no values were added, return the default - parser cannot tell the difference between no values and default values + if (values.Count == 0) + { + return defaultValue; + } + + return values; + } + + /// Searches for the first relative dir. + /// Thrown when the requested directory is not + /// present. + /// The start dir. + /// Pathname of the directory. + /// (Optional) True to throw if not found. + /// The found relative dir. + internal string FindRelativeDir( + string startDir, + string dirName, + bool throwIfNotFound = true) + { + string currentDir; + + if (string.IsNullOrEmpty(startDir)) + { + if (dirName.StartsWith('~')) + { + currentDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + + if (dirName.Length > 1) + { + dirName = dirName[2..]; + } + else + { + dirName = string.Empty; + } + } + else + { + currentDir = Path.GetDirectoryName(AppContext.BaseDirectory) ?? string.Empty; + } + } + else if (startDir.StartsWith('~')) + { + // check if the path was only the user dir or the user dir plus a separator + if ((startDir.Length == 1) || (startDir.Length == 2)) + { + currentDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + } + else + { + // skip the separator + currentDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), startDir[2..]); + } + } + else + { + currentDir = startDir; + } + + string testDir = Path.Combine(currentDir, dirName); + + while (!Directory.Exists(testDir)) + { + currentDir = Path.GetFullPath(Path.Combine(currentDir, "..")); + + if (currentDir == Path.GetPathRoot(currentDir)) + { + if (throwIfNotFound) + { + throw new DirectoryNotFoundException($"Could not find directory {dirName}!"); + } + + return string.Empty; + } + + testDir = Path.Combine(currentDir, dirName); + } + + return Path.GetFullPath(testDir); + } +} diff --git a/src/FhirStore.Common/Configuration/ConfigOptionAttribute.cs b/src/FhirStore.Common/Configuration/ConfigOptionAttribute.cs new file mode 100644 index 0000000..6bbd2b3 --- /dev/null +++ b/src/FhirStore.Common/Configuration/ConfigOptionAttribute.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// + +namespace FhirCandle.Configuration; + +/// Attribute for configuration option. +public class ConfigOptionAttribute : Attribute +{ + /// Gets or sets the name of the environment variable name. + public string EnvName { get; set; } = string.Empty; + + /// Gets or sets the name of the argument. + public string ArgName { get; set; } = string.Empty; + + /// Gets or sets the argument aliases. + public string[] ArgAliases { get; set; } = []; + + /// Gets or sets the description. + public string Description { get; set; } = string.Empty; + + /// Gets or sets the arity, specified as a FHIR Cardinality string. + public string ArgArity { get; set; } = "0..1"; +} diff --git a/src/FhirStore.Common/Configuration/ConfigurationOption.cs b/src/FhirStore.Common/Configuration/ConfigurationOption.cs new file mode 100644 index 0000000..77350ea --- /dev/null +++ b/src/FhirStore.Common/Configuration/ConfigurationOption.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// + +namespace FhirCandle.Configuration; + +public record class ConfigurationOption +{ + /// Gets or initializes the name. + public required string Name { get; init; } + + /// Gets or sets the name of the environment variable name. + public string EnvVarName { get; init; } = string.Empty; + + /// Gets or sets the default value. + public required object DefaultValue { get; init; } + + /// Gets or initializes the CLI option. + public required System.CommandLine.Option CliOption { get; init; } +} diff --git a/src/FhirStore.Common/FhirCandle.Common.csproj b/src/FhirStore.Common/FhirCandle.Common.csproj index 00a122c..ec10fe1 100644 --- a/src/FhirStore.Common/FhirCandle.Common.csproj +++ b/src/FhirStore.Common/FhirCandle.Common.csproj @@ -1,11 +1,15 @@  - - net8.0 - FhirCandle - - + + 12.0 + netstandard2.0;net8.0 + enable + enable + FhirCandle + + + @@ -19,7 +23,8 @@ - + + \ No newline at end of file diff --git a/src/FhirStore.Common/Models/FhirNpmPackageDetails.cs b/src/FhirStore.Common/Models/FhirNpmPackageDetails.cs index 24136e9..b5fced4 100644 --- a/src/FhirStore.Common/Models/FhirNpmPackageDetails.cs +++ b/src/FhirStore.Common/Models/FhirNpmPackageDetails.cs @@ -7,6 +7,10 @@ using System.Text.Json.Serialization; using System.Text.Json; +#if NETSTANDARD2_0 +using FhirCandle.Polyfill; +#endif + namespace FhirCandle.Models; /// Information about the FHIR npm package. @@ -218,7 +222,7 @@ public static FhirNpmPackageDetails Parse(string contents) throw new Exception("Invalid NPM Package Manifest"); } - if (!string.IsNullOrEmpty(details.FhirVersion)) + if (!string.IsNullOrEmpty(details!.FhirVersion)) { if (details.FhirVersion.StartsWith('[')) { @@ -350,7 +354,7 @@ private static IEnumerable EnumerableStringFromNode(JsonNode? node, stri continue; } - val.Add(item); + val.Add(item!); } } break; diff --git a/src/FhirStore.Common/Models/FhirRequestContext.cs b/src/FhirStore.Common/Models/FhirRequestContext.cs index 0f9140b..ff09e27 100644 --- a/src/FhirStore.Common/Models/FhirRequestContext.cs +++ b/src/FhirStore.Common/Models/FhirRequestContext.cs @@ -8,6 +8,10 @@ using System.Diagnostics.CodeAnalysis; using static FhirCandle.Storage.Common; +#if NETSTANDARD2_0 +using FhirCandle.Polyfill; +#endif + namespace FhirCandle.Models; /// A FHIR request context. @@ -283,7 +287,7 @@ internal bool TryParseRequest() default: // assume there are query parameters that contain '?' requestUrlPath = pathAndQuery[0]; - requestUrlQuery = string.Join('?', pathAndQuery[1..]); + requestUrlQuery = string.Join("?", pathAndQuery[1..]); break; } diff --git a/src/FhirStore.Common/Models/TenantConfiguration.cs b/src/FhirStore.Common/Models/TenantConfiguration.cs index 4bfa96f..ff087fb 100644 --- a/src/FhirStore.Common/Models/TenantConfiguration.cs +++ b/src/FhirStore.Common/Models/TenantConfiguration.cs @@ -4,79 +4,115 @@ // using FhirCandle.Extensions; +using FhirCandle.Utils; namespace FhirCandle.Models; -/// A provider configuration. +/// +/// A provider configuration. +/// public class TenantConfiguration { - /// Values that represent supported FHIR versions. - public enum SupportedFhirVersions : int - { - [FhirLiteral("R4")] - R4, - - [FhirLiteral("R4B")] - R4B, + /// + /// Gets or sets the supported FHIR versions. + /// + public static readonly List SupportedFhirVersions = [ + FhirReleases.FhirSequenceCodes.R4, + FhirReleases.FhirSequenceCodes.R4B, + FhirReleases.FhirSequenceCodes.R5 + ]; - [FhirLiteral("R5")] - R5, + /// + /// Information about the FHIR package. + /// + public readonly record struct FhirPackageInfo + { + /// + /// Gets the identifier. + /// + public string Id { get; init; } + + /// + /// Gets the version. + /// + public string Version { get; init; } + + /// + /// Gets the registry. + /// + public string Registry { get; init; } } - /// Information about the FHIR package. - /// The identifier. - /// The version. - /// The registry. - public readonly record struct FhirPackageInfo( - string Id, - string Version, - string Registry); - - /// Gets or sets the version. - public required SupportedFhirVersions FhirVersion { get; set; } = SupportedFhirVersions.R5; + /// + /// Gets or sets the FHIR version. + /// + public required FhirReleases.FhirSequenceCodes FhirVersion { get; set; } - /// Gets or sets the supported resources. - public IEnumerable SupportedResources { get; set; } = Array.Empty(); + /// + /// Gets or sets the supported resources. + /// + public IEnumerable SupportedResources { get; set; } = []; - /// Gets or sets the supported MIME formats. - public IEnumerable SupportedFormats { get; set; } = new string[] - { + /// + /// Gets or sets the supported MIME formats. + /// + public IEnumerable SupportedFormats { get; set; } = [ "application/fhir+json", - "application/fhir+xml", - }; + "application/fhir+xml" + ]; - /// Gets or sets route controller name. + /// + /// Gets or sets the route controller name. + /// public required string ControllerName { get; set; } = string.Empty; /// - /// Gets or sets the absolute base url of this store. + /// Gets or sets the absolute base URL of this store. /// public required string BaseUrl { get; set; } = string.Empty; - /// Gets or sets the FHIR packages. - public Dictionary FhirPackages { get; } = new(); + /// + /// Gets the FHIR packages. + /// + public Dictionary FhirPackages { get; } = []; - /// Gets or sets the pathname of the load directory. + /// + /// Gets or sets the load directory path. + /// public System.IO.DirectoryInfo? LoadDirectory { get; set; } = null; - /// Gets or sets the protect loaded content. + /// + /// Gets or sets a value indicating whether to protect loaded content. + /// public bool ProtectLoadedContent { get; set; } = false; - /// Gets or sets the number of maximum resources. + /// + /// Gets or sets the maximum resource count. + /// public int MaxResourceCount { get; set; } = 0; - /// Gets or sets the max allowed subscription expiration minutes. + /// + /// Gets or sets the maximum allowed subscription expiration minutes. + /// public int MaxSubscriptionExpirationMinutes { get; set; } = 30; - /// Gets or sets a value indicating whether the smart required. + /// + /// Gets or sets a value indicating whether SMART is required. + /// public bool SmartRequired { get; set; } = false; - /// Gets or sets a value indicating whether smart is allowed. + /// + /// Gets or sets a value indicating whether SMART is allowed. + /// public bool SmartAllowed { get; set; } = false; - /// Gets or sets a value indicating whether we allow existing identifier. + /// + /// Gets or sets a value indicating whether to allow existing identifier. + /// public bool AllowExistingId { get; set; } = true; - /// Gets or sets a value indicating whether we allow create as update. + /// + /// Gets or sets a value indicating whether to allow create as update. + /// public bool AllowCreateAsUpdate { get; set; } = true; } diff --git a/src/FhirStore.Common/Polyfill/CandleCommonPolyfill.cs b/src/FhirStore.Common/Polyfill/CandleCommonPolyfill.cs new file mode 100644 index 0000000..e725323 --- /dev/null +++ b/src/FhirStore.Common/Polyfill/CandleCommonPolyfill.cs @@ -0,0 +1,186 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +/* + * NOTE: This file uses `internal` access modifiers to avoid exporting the polyfill types to the assembly consumers. + * Each project internally that wants to use this should add the file as a link to ensure consistency. + */ + +#if NETSTANDARD2_0 +// some functionality must be specified in CompilerServices to Polyfill without errors +namespace System.Runtime.CompilerServices +{ + internal static class RuntimeHelpers + { + // For a value of type System.Range to be used in an array element access expression, the following member must be present: + public static T[] GetSubArray(T[] array, System.Range range) + { + if (array == null) + { + throw new ArgumentNullException(); + } + + (int offset, int length) = range.GetOffsetAndLength(array.Length); + + if (default(T)! != null || typeof(T[]) == array.GetType()) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757) + { + if (length == 0) + { + return Array.Empty(); + } + + var dest = new T[length]; + Array.Copy(array, offset, dest, 0, length); + return dest; + } + else + { + // The array is actually a U[] where U:T. + T[] dest = (T[])Array.CreateInstance(array.GetType().GetElementType()!, length); + Array.Copy(array, offset, dest, 0, length); + return dest; + } + } + } +} +#endif + +namespace FhirCandle.Polyfill +{ + internal static class LiftedExtensions + { + /// Indicates whether a character is categorized as an ASCII letter. + /// The character to evaluate. + /// true if is an ASCII letter; otherwise, false. + /// + /// This determines whether the character is in the range 'A' through 'Z', inclusive, + /// or 'a' through 'z', inclusive. + /// + public static bool IsAsciiLetter(this char c) + { + return (uint)((c | 0x20) - 'a') <= 'z' - 'a'; + } + } + +#if NETSTANDARD2_0 + + internal static class KeyValuePairExtensions + { + public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + { + key = kvp.Key; + value = kvp.Value; + } + } + + internal static class EnumerableExtensions + { + public static IOrderedEnumerable Order(this IEnumerable val) + { + return val.OrderBy(v => v); + } + + public static IOrderedEnumerable Order(this IEnumerable val, IComparer comparer) + { + return val.OrderBy(v => v, comparer); + } + + public static IOrderedEnumerable OrderDescending(this IEnumerable val) + { + return val.OrderByDescending(v => v); + } + } + + internal class ImmutableHashSet + where T : notnull + { + internal static HashSet Create(IEnumerable items) + { + return new HashSet(items); + } + } + + internal class FrozenDictionary : Dictionary + where TKey : notnull + { + public FrozenDictionary(IEqualityComparer? comparer = null) : base(comparer) { } + } + + internal static class FrozenDictionaryExtensions + { + public static FrozenDictionary ToFrozenDictionary(this Dictionary dictionary, IEqualityComparer? comparer = null) + where TKey : notnull => new FrozenDictionary(comparer); + } + + internal static class StringExtensions + { + public static bool StartsWith(this string str, char value) + { + return string.IsNullOrEmpty(str) + ? false + : str[0] == value; + } + + public static bool EndsWith(this string str, char value) + { + return string.IsNullOrEmpty(str) + ? false + : str[^1] == value; + } + + public static bool Contains(this string str, string value, StringComparison _) + { + return str.Contains(value); + } + + public static string Replace(this string str, string search, string replace, StringComparison _) + { + return str.Replace(search, replace); + } + + public static string AsSpan(this string value, int start) => value.Substring(start); + } + + internal static class StringArrayExtensions + { + public static string[] Split(this string str, char sep1, StringSplitOptions options) + { + return str.Split(new[] { sep1 }, options); + } + + public static string[] Split(this string str, char sep1, char sep2, StringSplitOptions options) + { + return str.Split(new[] { sep1, sep2 }, options); + } + } + + internal static class CollectionExtensions + { + public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) + { + if (dictionary.ContainsKey(key)) + { + return false; + } + + dictionary.Add(key, value); + return true; + } + + public static TValue? GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) => + dictionary.GetValueOrDefault(key, default!); + + public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) + { + return dictionary.TryGetValue(key, out TValue? value) ? value : defaultValue; + } + } +#endif +} diff --git a/src/FhirStore.Common/Search/Common.cs b/src/FhirStore.Common/Search/Common.cs index d23d5df..08a6978 100644 --- a/src/FhirStore.Common/Search/Common.cs +++ b/src/FhirStore.Common/Search/Common.cs @@ -3,7 +3,11 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // +#if NET8_0_OR_GREATER using System.Collections.Immutable; +#elif NETSTANDARD2_0 +using FhirCandle.Polyfill; +#endif namespace FhirCandle.Search; @@ -11,7 +15,11 @@ namespace FhirCandle.Search; public static class Common { /// (Immutable) Options for controlling the HTTP. +#if NET8_0_OR_GREATER public static readonly ImmutableHashSet HttpParameters = ImmutableHashSet.Create(new string[] +#elif NETSTANDARD2_0 + public static readonly HashSet HttpParameters = ImmutableHashSet.Create(new string[] +#endif { /// Override the HTTP content negotiation. "_format", @@ -27,7 +35,11 @@ public static class Common }); /// (Immutable) Options for controlling the search result. +#if NET8_0_OR_GREATER public static readonly ImmutableHashSet SearchResultParameters = ImmutableHashSet.Create(new string[] +#elif NETSTANDARD2_0 + public static readonly HashSet SearchResultParameters = ImmutableHashSet.Create(new string[] +#endif { /// Request different types of handling for contained resources. "_contained", diff --git a/src/FhirStore.Common/Serialization/SerializationCommon.cs b/src/FhirStore.Common/Serialization/SerializationCommon.cs index 2b4bdb2..5c3422d 100644 --- a/src/FhirStore.Common/Serialization/SerializationCommon.cs +++ b/src/FhirStore.Common/Serialization/SerializationCommon.cs @@ -22,7 +22,11 @@ public static string SerializeObject( string format = "application/json", bool pretty = false) { +#if NET8_0_OR_GREATER string[] formatComponents = format.Split(';', StringSplitOptions.TrimEntries); +#else + string[] formatComponents = format.Split(';').Select(s => s.Trim()).ToArray(); +#endif System.Text.Encoding encoding = System.Text.Encoding.UTF8; switch (formatComponents[0]) diff --git a/src/FhirStore.Common/Storage/IFhirStore.cs b/src/FhirStore.Common/Storage/IFhirStore.cs index 7e1964b..f8e98b8 100644 --- a/src/FhirStore.Common/Storage/IFhirStore.cs +++ b/src/FhirStore.Common/Storage/IFhirStore.cs @@ -64,7 +64,10 @@ void LoadPackage( bool includeExamples); /// Gets a list of names of the loaded packages. - HashSet LoadedPackages { get; } + HashSet LoadedPackageDirectives { get; } + + /// Gets a list of identifiers of the loaded packages. + HashSet LoadedPackageIds { get; } /// Gets the loaded supplements. HashSet LoadedSupplements { get; } diff --git a/src/FhirStore.Common/Utils/FhirReleases.cs b/src/FhirStore.Common/Utils/FhirReleases.cs new file mode 100644 index 0000000..c3f664d --- /dev/null +++ b/src/FhirStore.Common/Utils/FhirReleases.cs @@ -0,0 +1,429 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// + +using System; +using System.Diagnostics.CodeAnalysis; + +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#elif NETSTANDARD2_0 +using FhirCandle.Polyfill; +#endif + +namespace FhirCandle.Utils; + +/// FHIR release information and utilities. +public static class FhirReleases +{ + /// Values that represent FHIR major releases. + public enum FhirSequenceCodes : int + { + /// Unknown FHIR Version. + Unknown = 0, + + /// FHIR DSTU2. + DSTU2 = 1, + + /// FHIR STU3. + STU3 = 2, + + /// FHIR R4. + R4 = 3, + + /// FHIR R4B. + R4B = 4, + + /// FHIR R5. + R5 = 5, + + /// FHIR R5. + R6 = 6, + } + + /// Information about the published release. + public readonly record struct PublishedReleaseInformation( + FhirSequenceCodes Sequence, + DateTime PublicationDate, + bool IsSequenceOfficial, + string Version, + string Description, + string? BallotPrefix = null) + { + public override string ToString() => Description; + }; + + /// The FHIR releases. + internal static readonly FrozenDictionary FhirPublishedVersions = new Dictionary() + { + { "1.0.2", new (FhirSequenceCodes.DSTU2, new DateTime(2015, 10, 24), true, "1.0.2", "DSTU2 Release with 1 technical errata") }, + { "3.0.2", new (FhirSequenceCodes.STU3, new DateTime(2019, 10, 24), true, "3.0.2", "STU3 Release with 2 technical errata") }, + { "3.2.0", new (FhirSequenceCodes.R4, new DateTime(2018, 04, 02), false, "3.2.0", "R4 Draft for comment / First Candidate Normative Content", "2018Jan") }, + { "3.3.0", new (FhirSequenceCodes.R4, new DateTime(2018, 05, 02), false, "3.3.0", "R4 Ballot #1 : Mixed Normative/Trial use (First Normative ballot)", "2018May") }, + { "3.5.0", new (FhirSequenceCodes.R4, new DateTime(2018, 08, 21), false, "3.5.0", "R4 Ballot #2 : Mixed Normative/Trial use (Second Normative ballot + Baltimore Connectathon)", "2018Sep") }, + { "3.5a.0", new (FhirSequenceCodes.R4, new DateTime(2018, 11, 09), false, "3.5a.0", "Special R4 Ballot #3 : Normative Packages for Terminology / Conformance + Observation", "2018Dec") }, + { "4.0.1", new (FhirSequenceCodes.R4, new DateTime(2019, 10, 30), true, "4.0.1", "R4 Release with 1 technical errata") }, + { "4.1.0", new (FhirSequenceCodes.R4B, new DateTime(2021, 03, 11), false, "4.1.0", "R4B Ballot #1", "2021Mar") }, + { "4.3.0-snapshot1", new (FhirSequenceCodes.R4B, new DateTime(2021, 12, 20), false, "4.3.0-snapshot1", "R4B January 2022 Connectathon") }, + { "4.3.0", new (FhirSequenceCodes.R4B, new DateTime(2022, 05, 28), true, "4.3.0", "R4B Release") }, + { "4.2.0", new (FhirSequenceCodes.R5, new DateTime(2019, 12, 31), false, "4.2.0", "R5 Preview #1", "2020Feb") }, + { "4.4.0", new (FhirSequenceCodes.R5, new DateTime(2020, 05, 04), false, "4.4.0", "R5 Preview #2", "2020May") }, + { "4.5.0", new (FhirSequenceCodes.R5, new DateTime(2020, 08, 20), false, "4.5.0", "R5 Preview #3", "2020Sep") }, + { "4.6.0", new (FhirSequenceCodes.R5, new DateTime(2021, 04, 15), false, "4.6.0", "R5 Draft Ballot", "2021May") }, + { "5.0.0-snapshot1", new (FhirSequenceCodes.R5, new DateTime(2021, 12, 19), false, "5.0.0-snapshot1", "R5 January 2022 Connectathon") }, + { "5.0.0-ballot", new (FhirSequenceCodes.R5, new DateTime(2022, 09, 10), false, "5.0.0-ballot", "R5 Ballot #1") }, + { "5.0.0-snapshot3", new (FhirSequenceCodes.R5, new DateTime(2022, 12, 14), false, "5.0.0-snapshot3", "R5 Connectathon 32 Base") }, + { "5.0.0-draft-final", new (FhirSequenceCodes.R5, new DateTime(2023, 03, 01), false, "5.0.0-draft-final", "R5 Final QA") }, + { "5.0.0", new (FhirSequenceCodes.R5, new DateTime(2023, 03, 26), true, "5.0.0", "R5 Release") }, + { "6.0.0-ballot1", new (FhirSequenceCodes.R6, new DateTime(2023, 12, 19), false, "6.0.0-ballot1", "R6 Ballot 1st Draft") }, + { "6.0.0-ballot2", new (FhirSequenceCodes.R6, new DateTime(2024, 08, 13), false, "6.0.0-ballot2", "R6 Ballot 2nd Draft") }, + + }.ToFrozenDictionary(); + + /// (Immutable) The FHIR sequence map. + private static readonly FrozenDictionary _fhirSequenceMap = new Dictionary() + { + // unknown mapping (for performance) + { "", FhirSequenceCodes.Unknown }, + { "Unknown", FhirSequenceCodes.Unknown }, + + // DSTU2 + { "DSTU2", FhirSequenceCodes.DSTU2 }, + { "R2", FhirSequenceCodes.DSTU2 }, + { "2", FhirSequenceCodes.DSTU2 }, + { "0.4", FhirSequenceCodes.DSTU2 }, + { "0.4.0", FhirSequenceCodes.DSTU2 }, + { "0.5", FhirSequenceCodes.DSTU2 }, + { "0.5.0", FhirSequenceCodes.DSTU2 }, + { "1.0", FhirSequenceCodes.DSTU2 }, + { "1.0.0", FhirSequenceCodes.DSTU2 }, + { "1.0.1", FhirSequenceCodes.DSTU2 }, + { "1.0.2", FhirSequenceCodes.DSTU2 }, + { "2.0", FhirSequenceCodes.DSTU2 }, + { "hl7.fhir.r2", FhirSequenceCodes.DSTU2 }, + { "hl7.fhir.r2.core", FhirSequenceCodes.DSTU2 }, + + // STU3 + { "STU3", FhirSequenceCodes.STU3 }, + { "R3", FhirSequenceCodes.STU3 }, + { "3", FhirSequenceCodes.STU3 }, + { "1.1", FhirSequenceCodes.STU3 }, + { "1.1.0", FhirSequenceCodes.STU3 }, + { "1.2", FhirSequenceCodes.STU3 }, + { "1.2.0", FhirSequenceCodes.STU3 }, + { "1.4", FhirSequenceCodes.STU3 }, + { "1.4.0", FhirSequenceCodes.STU3 }, + { "1.6", FhirSequenceCodes.STU3 }, + { "1.6.0", FhirSequenceCodes.STU3 }, + { "1.8", FhirSequenceCodes.STU3 }, + { "1.8.0", FhirSequenceCodes.STU3 }, + { "3.0", FhirSequenceCodes.STU3 }, + { "3.0.0", FhirSequenceCodes.STU3 }, + { "3.0.1", FhirSequenceCodes.STU3 }, + { "3.0.2", FhirSequenceCodes.STU3 }, + { "hl7.fhir.r3", FhirSequenceCodes.STU3 }, + { "hl7.fhir.r3.core", FhirSequenceCodes.STU3 }, + + // R4 + { "R4", FhirSequenceCodes.R4 }, + { "4", FhirSequenceCodes.R4 }, + { "3.2", FhirSequenceCodes.R4 }, + { "3.2.0", FhirSequenceCodes.R4 }, + { "3.3", FhirSequenceCodes.R4 }, + { "3.3.0", FhirSequenceCodes.R4 }, + { "3.5", FhirSequenceCodes.R4 }, + { "3.5.0", FhirSequenceCodes.R4 }, + { "3.5a", FhirSequenceCodes.R4 }, + { "3.5a.0", FhirSequenceCodes.R4 }, + { "4.0", FhirSequenceCodes.R4 }, + { "4.0.0", FhirSequenceCodes.R4 }, + { "4.0.1", FhirSequenceCodes.R4 }, + { "hl7.fhir.r4", FhirSequenceCodes.R4 }, + { "hl7.fhir.r4.core", FhirSequenceCodes.R4 }, + + // R4B + { "R4B", FhirSequenceCodes.R4B }, + { "4B", FhirSequenceCodes.R4B }, + { "4.1", FhirSequenceCodes.R4B }, + { "4.1.0", FhirSequenceCodes.R4B }, + { "4.3", FhirSequenceCodes.R4B }, + { "4.3.0", FhirSequenceCodes.R4B }, + { "4.3.0-snapshot1", FhirSequenceCodes.R4B }, + { "hl7.fhir.r4b", FhirSequenceCodes.R4B }, + { "hl7.fhir.r4b.core", FhirSequenceCodes.R4B }, + + // R5 + { "R5", FhirSequenceCodes.R5 }, + { "5", FhirSequenceCodes.R5 }, + { "4.2", FhirSequenceCodes.R5 }, + { "4.2.0", FhirSequenceCodes.R5 }, + { "4.4", FhirSequenceCodes.R5 }, + { "4.4.0", FhirSequenceCodes.R5 }, + { "4.5", FhirSequenceCodes.R5 }, + { "4.5.0", FhirSequenceCodes.R5 }, + { "4.6", FhirSequenceCodes.R5 }, + { "4.6.0", FhirSequenceCodes.R5 }, + { "5.0", FhirSequenceCodes.R5 }, + { "5.0.0", FhirSequenceCodes.R5 }, + { "5.0.0-cibuild", FhirSequenceCodes.R5 }, + { "5.0.0-snapshot1", FhirSequenceCodes.R5 }, + { "5.0.0-ballot", FhirSequenceCodes.R5 }, + { "5.0.0-snapshot3", FhirSequenceCodes.R5 }, + { "5.0.0-draft-final", FhirSequenceCodes.R5 }, + { "hl7.fhir.r5", FhirSequenceCodes.R5 }, + { "hl7.fhir.r5.core", FhirSequenceCodes.R5 }, + + // R6 + { "R6", FhirSequenceCodes.R6 }, + { "6", FhirSequenceCodes.R6 }, + { "6.0", FhirSequenceCodes.R6 }, + { "6.0.0", FhirSequenceCodes.R6 }, + { "6.0.0-ballot1", FhirSequenceCodes.R6 }, + { "6.0.0-ballot2", FhirSequenceCodes.R6 }, + { "6.0.0-cibuild", FhirSequenceCodes.R6 }, + { "hl7.fhir.r6", FhirSequenceCodes.R6 }, + { "hl7.fhir.r6.core", FhirSequenceCodes.R6 }, + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + + /// Check if a FHIR release version is unavailable (e.g., a superseded patch). + /// The version string. + /// True if it succeeds, false if it fails. + public static bool VersionIsUnavailable(string version) => version switch + { + "1.0.0" => true, + "1.0.1" => true, + "3.0.0" => true, + "3.0.1" => true, + "4.0.0" => true, + _ => false, + }; + + public static string GetCurrentPatch(string version) => FhirVersionToSequence(version).ToLongVersion(); + + /// Attempts to get sequence the FhirSequenceCodes from the given string. + /// The literal. + /// [out] The FHIR sequence. + /// True if it succeeds, false if it fails. + public static bool TryGetSequence(string literal, [NotNullWhen(true)] out FhirSequenceCodes fhirSequence) + { + if (_fhirSequenceMap.TryGetValue(literal, out fhirSequence)) + { + return true; + } + + if (literal.Contains('#')) + { + if (_fhirSequenceMap.TryGetValue(literal.Substring(0, literal.IndexOf('#')), out fhirSequence)) + { + return true; + } + } + + if (literal.Contains('-')) + { + if (_fhirSequenceMap.TryGetValue(literal.Substring(0, literal.IndexOf('-')), out fhirSequence)) + { + return true; + } + } + + fhirSequence = FhirSequenceCodes.Unknown; + return false; + } + + /// + /// Converts a FHIR version string (number, literal, r-literal) to a sequence code. + /// + /// The version string. + /// The FhirSequenceCodes. + public static FhirSequenceCodes FhirVersionToSequence(string version) + { + if (_fhirSequenceMap.TryGetValue(version, out FhirSequenceCodes sequence)) + { + return sequence; + } + + // check for a tag we do not know + if (version.Contains('-')) + { + if (_fhirSequenceMap.TryGetValue(version.Substring(0, version.IndexOf('-')), out FhirSequenceCodes sequence2)) + { + return sequence2; + } + } + + return FhirSequenceCodes.Unknown; + } + + /// Convert a FHIR sequence code to the literal for that version. + /// The sequence. + /// A string. + public static string ToLiteral(this FhirSequenceCodes sequence) => sequence switch + { + FhirSequenceCodes.DSTU2 => "DSTU2", + FhirSequenceCodes.STU3 => "STU3", + FhirSequenceCodes.R4 => "R4", + FhirSequenceCodes.R4B => "R4B", + FhirSequenceCodes.R5 => "R5", + FhirSequenceCodes.R6 => "R6", + _ => "Unknown" + }; + + /// Converts a fhir version string to a literal. + /// [out] The version literal (e.g., DSTU2). + /// Sequence as a string. + public static string FhirVersionToLiteral(string version) + { + if (_fhirSequenceMap.TryGetValue(version, out FhirSequenceCodes sequence)) + { + return sequence.ToLiteral(); + } + + // check for a tag we do not know + if (version.Contains('-')) + { + if (_fhirSequenceMap.TryGetValue(version.Substring(0, version.IndexOf('-')), out FhirSequenceCodes sequence2)) + { + return sequence2.ToLiteral(); + } + } + + return string.Empty; + } + + /// Converts a sequence to a r literal. + /// The sequence. + /// Sequence as a string. + public static string ToRLiteral(this FhirSequenceCodes sequence) => sequence switch + { + FhirSequenceCodes.DSTU2 => "R2", + FhirSequenceCodes.STU3 => "R3", + FhirSequenceCodes.R4 => "R4", + FhirSequenceCodes.R4B => "R4B", + FhirSequenceCodes.R5 => "R5", + FhirSequenceCodes.R6 => "R6", + _ => "Unknown" + }; + + /// Converts a fhir version string to an R-literal. + /// [out] The R-Literal string (e.g., R4). + /// Sequence as a string. + public static string FhirVersionToRLiteral(string version) + { + if (_fhirSequenceMap.TryGetValue(version, out FhirSequenceCodes sequence)) + { + return sequence.ToRLiteral(); + } + + // check for a tag we do not know + if (version.Contains('-')) + { + if (_fhirSequenceMap.TryGetValue(version.Substring(0, version.IndexOf('-')), out FhirSequenceCodes sequence2)) + { + return sequence2.ToRLiteral(); + } + } + + return string.Empty; + } + + /// Converts a sequence to a short version. + /// The sequence. + /// Sequence as a string. + public static string ToShortVersion(this FhirSequenceCodes sequence) => sequence switch + { + FhirSequenceCodes.DSTU2 => "1.0", + FhirSequenceCodes.STU3 => "3.0", + FhirSequenceCodes.R4 => "4.0", + FhirSequenceCodes.R4B => "4.3", + FhirSequenceCodes.R5 => "5.0", + FhirSequenceCodes.R6 => "6.0", + _ => "Unknown" + }; + + /// Converts a fhir version string to a short version number. + /// [out] The version string (e.g., 4.0.1). + /// Sequence as a string. + public static string FhirVersionToShortVersion(string version) + { + if (_fhirSequenceMap.TryGetValue(version, out FhirSequenceCodes sequence)) + { + return sequence.ToShortVersion(); + } + + // check for a tag we do not know + if (version.Contains('-')) + { + if (_fhirSequenceMap.TryGetValue(version.Substring(0, version.IndexOf('-')), out FhirSequenceCodes sequence2)) + { + return sequence2.ToShortVersion(); + } + } + + if (version.StartsWith("R", StringComparison.OrdinalIgnoreCase)) + { + return version.Substring(1, 1) + ".0"; + } + + return string.Empty; + } + + /// Converts a sequence to a long version. + /// The sequence. + /// Sequence as a string. + public static string ToLongVersion(this FhirSequenceCodes sequence) => sequence switch + { + FhirSequenceCodes.DSTU2 => "1.0.2", + FhirSequenceCodes.STU3 => "3.0.2", + FhirSequenceCodes.R4 => "4.0.1", + FhirSequenceCodes.R4B => "4.3.0", + FhirSequenceCodes.R5 => "5.0.0", + FhirSequenceCodes.R6 => "6.0.0", + _ => "Unknown" + }; + + /// + /// The FhirSequenceCodes extension method that converts a sequence to a core package + /// directive. + /// + /// The sequence. + /// Sequence as a string. + public static string ToCorePackageDirective(this FhirSequenceCodes sequence) => sequence switch + { + FhirSequenceCodes.DSTU2 => "hl7.fhir.r2.core@" + sequence.ToLongVersion(), + FhirSequenceCodes.STU3 => "hl7.fhir.r3.core@" + sequence.ToLongVersion(), + FhirSequenceCodes.R4 => "hl7.fhir.r4.core@" + sequence.ToLongVersion(), + FhirSequenceCodes.R4B => "hl7.fhir.r4b.core@" + sequence.ToLongVersion(), + FhirSequenceCodes.R5 => "hl7.fhir.r5.core@" + sequence.ToLongVersion(), + FhirSequenceCodes.R6 => "hl7.fhir.r6.core@" + sequence.ToLongVersion(), + _ => "Unknown" + }; + + /// FHIR version to long version. + /// [out] The version literal (e.g., DSTU2). + /// A string. + public static string FhirVersionToLongVersion(string version) + { + if (_fhirSequenceMap.TryGetValue(version, out FhirSequenceCodes sequence)) + { + return sequence.ToLongVersion(); + } + + // check for a tag we do not know + if (version.Contains('-')) + { + if (_fhirSequenceMap.TryGetValue(version.Substring(0, version.IndexOf('-')), out FhirSequenceCodes sequence2)) + { + return sequence2.ToLongVersion(); + } + } + + if (version.StartsWith("R", StringComparison.OrdinalIgnoreCase)) + { + return version.Substring(1, 1) + ".0.0"; + } + + return string.Empty; + } +} diff --git a/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems b/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems index 382087e..ee22430 100644 --- a/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems +++ b/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems @@ -29,7 +29,7 @@ - + diff --git a/src/FhirStore.CommonVersioned/Interactions/IFhirInteractionHook.cs b/src/FhirStore.CommonVersioned/Interactions/IFhirInteractionHook.cs index 3b95f9f..298e30c 100644 --- a/src/FhirStore.CommonVersioned/Interactions/IFhirInteractionHook.cs +++ b/src/FhirStore.CommonVersioned/Interactions/IFhirInteractionHook.cs @@ -20,7 +20,7 @@ public interface IFhirInteractionHook string Id { get; } /// Gets the supported FHIR versions. - HashSet SupportedFhirVersions { get; } + HashSet SupportedFhirVersions { get; } /// /// If this operation requires a specific FHIR package to be loaded, the package identifier. diff --git a/src/FhirStore.CommonVersioned/Operations/IFhirOperation.cs b/src/FhirStore.CommonVersioned/Operations/IFhirOperation.cs index 2bf5488..8c272b5 100644 --- a/src/FhirStore.CommonVersioned/Operations/IFhirOperation.cs +++ b/src/FhirStore.CommonVersioned/Operations/IFhirOperation.cs @@ -8,7 +8,7 @@ namespace FhirCandle.Operations; -/// Interface for executalbe FHIR operations. +/// Interface for executable FHIR operations. public interface IFhirOperation { /// Gets the name of the operation. @@ -18,7 +18,7 @@ public interface IFhirOperation string OperationVersion { get; } /// Gets the canonical by FHIR version. - Dictionary CanonicalByFhirVersion { get; } + Dictionary CanonicalByFhirVersion { get; } /// Gets a value indicating whether this object is named query. bool IsNamedQuery { get; } @@ -76,7 +76,7 @@ bool DoOperation( /// Gets an OperationDefinition resource describing this operation. /// The FHIR version. /// The definition. - Hl7.Fhir.Model.OperationDefinition? GetDefinition(FhirCandle.Models.TenantConfiguration.SupportedFhirVersions fhirVersion); + Hl7.Fhir.Model.OperationDefinition? GetDefinition(FhirCandle.Utils.FhirReleases.FhirSequenceCodes fhirVersion); } diff --git a/src/FhirStore.CommonVersioned/Operations/OpFeatureQuery.cs b/src/FhirStore.CommonVersioned/Operations/OpFeatureQuery.cs index a25783f..c037cb6 100644 --- a/src/FhirStore.CommonVersioned/Operations/OpFeatureQuery.cs +++ b/src/FhirStore.CommonVersioned/Operations/OpFeatureQuery.cs @@ -15,11 +15,11 @@ public class OpFeatureQuery : IFhirOperation public string OperationVersion => "0.0.1"; /// Gets the canonical by FHIR version. - public Dictionary CanonicalByFhirVersion => new() + public Dictionary CanonicalByFhirVersion => new() { - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4, "http://www.hl7.org/fhir/uv/capstmt/OperationDefinition/feature-query" }, - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4B, "http://www.hl7.org/fhir/uv/capstmt/OperationDefinition/feature-query" }, - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R5, "http://www.hl7.org/fhir/uv/capstmt/OperationDefinition/feature-query" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4, "http://www.hl7.org/fhir/uv/capstmt/OperationDefinition/feature-query" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4B, "http://www.hl7.org/fhir/uv/capstmt/OperationDefinition/feature-query" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R5, "http://www.hl7.org/fhir/uv/capstmt/OperationDefinition/feature-query" }, }; /// Gets a value indicating whether this operation is a named query. @@ -54,7 +54,7 @@ public class OpFeatureQuery : IFhirOperation /// Gets the supported resources. public HashSet SupportedResources => []; - private readonly HashSet _exlcudedParams = + private readonly HashSet _excludedParams = [ "_format", ]; @@ -108,7 +108,7 @@ public bool DoOperation( // check for feature requests as http parameters List featureRequests = paramValues - .Where(p => !_exlcudedParams.Contains(p)) + .Where(p => !_excludedParams.Contains(p)) .Select(ParseFeatureRequestParam) .Where(r => r != null) .ToList() ?? []; @@ -274,7 +274,7 @@ private record class FeatureRequestRecord } public Hl7.Fhir.Model.OperationDefinition? GetDefinition( - FhirCandle.Models.TenantConfiguration.SupportedFhirVersions fhirVersion) + FhirCandle.Utils.FhirReleases.FhirSequenceCodes fhirVersion) { return new() { diff --git a/src/FhirStore.CommonVersioned/Operations/OpIsFhir.cs b/src/FhirStore.CommonVersioned/Operations/OpIsFhir.cs index 458df6a..6a6bc8d 100644 --- a/src/FhirStore.CommonVersioned/Operations/OpIsFhir.cs +++ b/src/FhirStore.CommonVersioned/Operations/OpIsFhir.cs @@ -24,11 +24,11 @@ public class OpTestIfFhir : IFhirOperation public string OperationVersion => "0.0.1"; /// Gets the canonical by FHIR version. - public Dictionary CanonicalByFhirVersion => new() + public Dictionary CanonicalByFhirVersion => new() { - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4, "http://ginoc.io/fhir/OperationDefinition/test-if-fhir" }, - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4B, "http://ginoc.io/fhir/OperationDefinition/test-if-fhir" }, - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R5, "http://ginoc.io/fhir/OperationDefinition/test-if-fhir" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4, "http://ginoc.io/fhir/OperationDefinition/test-if-fhir" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4B, "http://ginoc.io/fhir/OperationDefinition/test-if-fhir" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R5, "http://ginoc.io/fhir/OperationDefinition/test-if-fhir" }, }; /// Gets a value indicating whether this operation is a named query. @@ -151,7 +151,7 @@ public bool DoOperation( /// The FHIR version. /// The definition. public Hl7.Fhir.Model.OperationDefinition? GetDefinition( - FhirCandle.Models.TenantConfiguration.SupportedFhirVersions fhirVersion) + FhirCandle.Utils.FhirReleases.FhirSequenceCodes fhirVersion) { Hl7.Fhir.Model.OperationDefinition def = new() { @@ -180,9 +180,9 @@ public bool DoOperation( string GetReturnDocValue() => fhirVersion switch { - Models.TenantConfiguration.SupportedFhirVersions.R4 => "An OperationOutcome with information about the submitted data.", - Models.TenantConfiguration.SupportedFhirVersions.R4B => "An OperationOutcome with information about the submitted data.", - Models.TenantConfiguration.SupportedFhirVersions.R5 => "An OperationOutcome with information about the submitted data.", + Utils.FhirReleases.FhirSequenceCodes.R4 => "An OperationOutcome with information about the submitted data.", + Utils.FhirReleases.FhirSequenceCodes.R4B => "An OperationOutcome with information about the submitted data.", + Utils.FhirReleases.FhirSequenceCodes.R5 => "An OperationOutcome with information about the submitted data.", _ => string.Empty, }; diff --git a/src/FhirStore.CommonVersioned/Operations/OpSubscriptionEvents.cs b/src/FhirStore.CommonVersioned/Operations/OpSubscriptionEvents.cs index 31c44ae..794bf82 100644 --- a/src/FhirStore.CommonVersioned/Operations/OpSubscriptionEvents.cs +++ b/src/FhirStore.CommonVersioned/Operations/OpSubscriptionEvents.cs @@ -22,11 +22,11 @@ public class OpSubscriptionEvents : IFhirOperation public string OperationVersion => "0.0.1"; /// Gets the canonical by FHIR version. - public Dictionary CanonicalByFhirVersion => new() + public Dictionary CanonicalByFhirVersion => new() { - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4, "http://hl7.org/fhir/uv/subscriptions-backport/OperationDefinition/backport-subscription-events" }, - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4B, "http://hl7.org/fhir/uv/subscriptions-backport/OperationDefinition/backport-subscription-events" }, - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R5, "http://hl7.org/fhir/OperationDefinition/Subscription-events" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4, "http://hl7.org/fhir/uv/subscriptions-backport/OperationDefinition/backport-subscription-events" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4B, "http://hl7.org/fhir/uv/subscriptions-backport/OperationDefinition/backport-subscription-events" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R5, "http://hl7.org/fhir/OperationDefinition/Subscription-events" }, }; /// Gets a value indicating whether this operation is a named query. @@ -91,7 +91,7 @@ public bool DoOperation( opResponse = new() { StatusCode = HttpStatusCode.NotFound, - Outcome = FhirCandle.Serialization.Utils.BuildOutcomeForRequest( + Outcome = FhirCandle.Serialization.SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Subscription {ctx.Id} was not found."), }; @@ -179,7 +179,7 @@ public bool DoOperation( eventNumbers, "query-event", contentLevel), - Outcome = FhirCandle.Serialization.Utils.BuildOutcomeForRequest( + Outcome = FhirCandle.Serialization.SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.OK, $"Events for subscription {ctx.Id}."), }; @@ -191,7 +191,7 @@ public bool DoOperation( /// The FHIR version. /// The definition. public Hl7.Fhir.Model.OperationDefinition? GetDefinition( - FhirCandle.Models.TenantConfiguration.SupportedFhirVersions fhirVersion) + FhirCandle.Utils.FhirReleases.FhirSequenceCodes fhirVersion) { Hl7.Fhir.Model.OperationDefinition def = new() { @@ -255,9 +255,9 @@ public bool DoOperation( string GetReturnDocValue() => fhirVersion switch { - Models.TenantConfiguration.SupportedFhirVersions.R4 => "The operation returns a valid notification bundle, with the first entry being a Parameters resource. The bundle type is \"history\".", - Models.TenantConfiguration.SupportedFhirVersions.R4B => "The operation returns a valid notification bundle, with the first entry being a SubscriptionStatus resource. The bundle type is \"history\".", - Models.TenantConfiguration.SupportedFhirVersions.R5 => "The operation returns a valid notification bundle, with the first entry being a SubscriptionStatus resource. The bundle type is \"subscription-notification\".", + Utils.FhirReleases.FhirSequenceCodes.R4 => "The operation returns a valid notification bundle, with the first entry being a Parameters resource. The bundle type is \"history\".", + Utils.FhirReleases.FhirSequenceCodes.R4B => "The operation returns a valid notification bundle, with the first entry being a SubscriptionStatus resource. The bundle type is \"history\".", + Utils.FhirReleases.FhirSequenceCodes.R5 => "The operation returns a valid notification bundle, with the first entry being a SubscriptionStatus resource. The bundle type is \"subscription-notification\".", _ => string.Empty, }; diff --git a/src/FhirStore.CommonVersioned/Operations/OpSubscriptionHook.cs b/src/FhirStore.CommonVersioned/Operations/OpSubscriptionHook.cs index 5539d6e..263ccf9 100644 --- a/src/FhirStore.CommonVersioned/Operations/OpSubscriptionHook.cs +++ b/src/FhirStore.CommonVersioned/Operations/OpSubscriptionHook.cs @@ -22,11 +22,11 @@ public class OpSubscriptionHook : IFhirOperation public string OperationVersion => "0.0.1"; /// Gets the canonical by FHIR version. - public Dictionary CanonicalByFhirVersion => new() + public Dictionary CanonicalByFhirVersion => new() { - { TenantConfiguration.SupportedFhirVersions.R4, "http://argo.run/fhir/OperationDefinition/subscription-hook" }, - { TenantConfiguration.SupportedFhirVersions.R4B, "http://argo.run/fhir/OperationDefinition/subscription-hook" }, - { TenantConfiguration.SupportedFhirVersions.R5, "http://argo.run/fhir/OperationDefinition/subscription-hook" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4, "http://argo.run/fhir/OperationDefinition/subscription-hook" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4B, "http://argo.run/fhir/OperationDefinition/subscription-hook" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R5, "http://argo.run/fhir/OperationDefinition/subscription-hook" }, }; /// Gets a value indicating whether this operation is a named query. @@ -85,7 +85,7 @@ public bool DoOperation( opResponse = new() { StatusCode = HttpStatusCode.UnprocessableEntity, - Outcome = FhirCandle.Serialization.Utils.BuildOutcomeForRequest( + Outcome = FhirCandle.Serialization.SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, "Posted content is not a valid Subscription notification bundle"), }; @@ -105,7 +105,7 @@ public bool DoOperation( opResponse = new() { StatusCode = HttpStatusCode.UnprocessableEntity, - Outcome = FhirCandle.Serialization.Utils.BuildOutcomeForRequest( + Outcome = FhirCandle.Serialization.SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, "Posted content is not a valid Subscription notification bundle"), }; @@ -123,7 +123,7 @@ public bool DoOperation( opResponse = new() { StatusCode = HttpStatusCode.OK, - Outcome = FhirCandle.Serialization.Utils.BuildOutcomeForRequest( + Outcome = FhirCandle.Serialization.SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.OK, "Subscription Notification Received"), }; @@ -135,7 +135,7 @@ public bool DoOperation( /// The FHIR version. /// The definition. public Hl7.Fhir.Model.OperationDefinition? GetDefinition( - TenantConfiguration.SupportedFhirVersions fhirVersion) + FhirCandle.Utils.FhirReleases.FhirSequenceCodes fhirVersion) { Hl7.Fhir.Model.OperationDefinition def = new() { diff --git a/src/FhirStore.CommonVersioned/Operations/OpSubscriptionStatus.cs b/src/FhirStore.CommonVersioned/Operations/OpSubscriptionStatus.cs index e419a3d..1a23a33 100644 --- a/src/FhirStore.CommonVersioned/Operations/OpSubscriptionStatus.cs +++ b/src/FhirStore.CommonVersioned/Operations/OpSubscriptionStatus.cs @@ -21,11 +21,11 @@ public class OpSubscriptionStatus : IFhirOperation public string OperationVersion => "0.0.1"; /// Gets the canonical by FHIR version. - public Dictionary CanonicalByFhirVersion => new() + public Dictionary CanonicalByFhirVersion => new() { - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4, "http://hl7.org/fhir/uv/subscriptions-backport/OperationDefinition/backport-subscription-status" }, - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4B, "http://hl7.org/fhir/uv/subscriptions-backport/OperationDefinition/backport-subscription-status" }, - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R5, "http://hl7.org/fhir/OperationDefinition/Subscription-status" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4, "http://hl7.org/fhir/uv/subscriptions-backport/OperationDefinition/backport-subscription-status" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4B, "http://hl7.org/fhir/uv/subscriptions-backport/OperationDefinition/backport-subscription-status" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R5, "http://hl7.org/fhir/OperationDefinition/Subscription-status" }, }; /// Gets a value indicating whether this object is named query. @@ -213,7 +213,7 @@ public bool DoOperation( { StatusCode = HttpStatusCode.OK, Resource = bundle, - Outcome = FhirCandle.Serialization.Utils.BuildOutcomeForRequest( + Outcome = FhirCandle.Serialization.SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.OK, "See resource for $status data"), }; @@ -226,7 +226,7 @@ public bool DoOperation( /// The FHIR version. /// The definition. public Hl7.Fhir.Model.OperationDefinition? GetDefinition( - FhirCandle.Models.TenantConfiguration.SupportedFhirVersions fhirVersion) + FhirCandle.Utils.FhirReleases.FhirSequenceCodes fhirVersion) { Hl7.Fhir.Model.OperationDefinition def = new() { diff --git a/src/FhirStore.CommonVersioned/Search/EvalDateSearch.cs b/src/FhirStore.CommonVersioned/Search/EvalDateSearch.cs index ae1f4c1..db05f9c 100644 --- a/src/FhirStore.CommonVersioned/Search/EvalDateSearch.cs +++ b/src/FhirStore.CommonVersioned/Search/EvalDateSearch.cs @@ -87,7 +87,7 @@ public static bool TestDate(ITypedElement valueNode, ParsedSearchParameter sp) valueEnd = valueStart.AddMinutes(1).AddTicks(-1); break; - // we choose to igore fractions of seconds + // we choose to ignore fractions of seconds case Hl7.Fhir.ElementModel.Types.DateTimePrecision.Second: case Hl7.Fhir.ElementModel.Types.DateTimePrecision.Fraction: valueEnd = valueStart.AddSeconds(1).AddTicks(-1); diff --git a/src/FhirStore.CommonVersioned/Serialization/Utils.cs b/src/FhirStore.CommonVersioned/Serialization/SerializationUtils.cs similarity index 99% rename from src/FhirStore.CommonVersioned/Serialization/Utils.cs rename to src/FhirStore.CommonVersioned/Serialization/SerializationUtils.cs index bb1a76d..b7467b9 100644 --- a/src/FhirStore.CommonVersioned/Serialization/Utils.cs +++ b/src/FhirStore.CommonVersioned/Serialization/SerializationUtils.cs @@ -18,7 +18,7 @@ namespace FhirCandle.Serialization; /// Serialization utilities. -public static class Utils +public static class SerializationUtils { /// The JSON parser. private static FhirJsonPocoDeserializer _jsonParser = new(new FhirJsonPocoDeserializerSettings() diff --git a/src/FhirStore.CommonVersioned/Storage/ResourceStore.cs b/src/FhirStore.CommonVersioned/Storage/ResourceStore.cs index f3da787..82c8741 100644 --- a/src/FhirStore.CommonVersioned/Storage/ResourceStore.cs +++ b/src/FhirStore.CommonVersioned/Storage/ResourceStore.cs @@ -700,14 +700,14 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model if ((source == null) || (source is not T)) { - outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.BadRequest, $"Invalid resource content for {_resourceName}"); + outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.BadRequest, $"Invalid resource content for {_resourceName}"); sc = HttpStatusCode.BadRequest; return null; } if (string.IsNullOrEmpty(source.Id)) { - outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.BadRequest, "Cannot update resources without an ID"); + outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.BadRequest, "Cannot update resources without an ID"); sc = HttpStatusCode.BadRequest; return null; } @@ -719,7 +719,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model if (protectedResources.Any() && protectedResources.Contains(_resourceName + "/" + source.Id)) { - outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.Unauthorized, $"Resource {_resourceName}/{source.Id} is protected and cannot be changed"); + outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.Unauthorized, $"Resource {_resourceName}/{source.Id} is protected and cannot be changed"); sc = HttpStatusCode.Unauthorized; return null; } @@ -741,7 +741,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model { if (!_topicConverter.TryParse(source, out parsedSubscriptionTopic)) { - outcome = Utils.BuildOutcomeForRequest( + outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, $"Basic-wrapped SubscriptionTopic could not be parsed!"); sc = HttpStatusCode.BadRequest; @@ -755,7 +755,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model // fail the request if this fails if (!_topicConverter.TryParse(source, out parsedSubscriptionTopic)) { - outcome = Utils.BuildOutcomeForRequest( + outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, $"SubscriptionTopic could not be parsed!"); sc = HttpStatusCode.BadRequest; @@ -767,7 +767,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model // fail the request if this fails if (!_subscriptionConverter.TryParse((Subscription)source, out parsedSubscription)) { - outcome = Utils.BuildOutcomeForRequest( + outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, $"Subscription could not be parsed!"); sc = HttpStatusCode.BadRequest; @@ -789,7 +789,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model } else { - outcome = Utils.BuildOutcomeForRequest( + outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, $"Update as Create is disabled"); sc = HttpStatusCode.BadRequest; @@ -810,7 +810,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model // check preconditions if (ifNoneMatch.Equals("*", StringComparison.Ordinal)) { - outcome = Utils.BuildOutcomeForRequest( + outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, "Prior version exists, but If-None-Match is *"); sc = HttpStatusCode.PreconditionFailed; @@ -821,7 +821,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model { if (ifNoneMatch.Equals($"W/\"{previous?.Meta.VersionId ?? string.Empty}\"", StringComparison.Ordinal)) { - outcome = Utils.BuildOutcomeForRequest( + outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"Conditional update query returned a match with version: {previous?.Meta.VersionId}, If-None-Match: {ifNoneMatch}"); sc = HttpStatusCode.PreconditionFailed; @@ -833,7 +833,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model { if (!ifMatch.Equals($"W/\"{previous?.Meta.VersionId}\"", StringComparison.Ordinal)) { - outcome = Utils.BuildOutcomeForRequest( + outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"Conditional update query returned a match with version: {previous?.Meta.VersionId}, If-Match: {ifNoneMatch}"); sc = HttpStatusCode.PreconditionFailed; @@ -913,7 +913,7 @@ public bool TryResolveIdentifier(string system, string value, out Hl7.Fhir.Model break; } - outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Updated {_resourceName}/{source.Id} to version {source.Meta.VersionId}"); + outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Updated {_resourceName}/{source.Id} to version {source.Meta.VersionId}"); sc = HttpStatusCode.OK; return source; } diff --git a/src/FhirStore.CommonVersioned/Storage/VersionedFhirStore.cs b/src/FhirStore.CommonVersioned/Storage/VersionedFhirStore.cs index 50ea504..5bb2b62 100644 --- a/src/FhirStore.CommonVersioned/Storage/VersionedFhirStore.cs +++ b/src/FhirStore.CommonVersioned/Storage/VersionedFhirStore.cs @@ -7,6 +7,7 @@ using FhirCandle.Models; using FhirCandle.Operations; using FhirCandle.Subscriptions; +using FhirCandle.Utils; using Hl7.Fhir.ElementModel; using Hl7.Fhir.FhirPath; using Hl7.Fhir.Model; @@ -63,7 +64,7 @@ public partial class VersionedFhirStore : IFhirStore private static FhirPathCompiler _compiler = null!; /// The store. - private Dictionary _store = new(); + private Dictionary _store = []; /// The search tester. private SearchTester _searchTester; @@ -72,7 +73,7 @@ public partial class VersionedFhirStore : IFhirStore public IEnumerable SupportedResources => _store.Keys.ToArray(); /// (Immutable) The cache of compiled search parameter extraction functions. - private readonly ConcurrentDictionary _compiledSearchParameters = new(); + private readonly ConcurrentDictionary _compiledSearchParameters = []; /// The sp lock object. private object _spLockObject = new(); @@ -84,10 +85,10 @@ public partial class VersionedFhirStore : IFhirStore internal static SubscriptionConverter _subscriptionConverter = null!; /// (Immutable) The topics, by id. - internal readonly ConcurrentDictionary _topics = new(); + internal readonly ConcurrentDictionary _topics = []; /// (Immutable) The subscriptions, by id. - internal readonly ConcurrentDictionary _subscriptions = new(); + internal readonly ConcurrentDictionary _subscriptions = []; /// (Immutable) The FHIRPath variable matcher. [GeneratedRegex("[%][\\w\\-]+", RegexOptions.Compiled)] @@ -103,19 +104,22 @@ public partial class VersionedFhirStore : IFhirStore private const string _capabilityStatementId = "metadata"; /// The operations supported by this server, by name. - private Dictionary _operations = new(); + private Dictionary _operations = []; /// The loaded hooks. - private Dictionary _hookNamesById = new(); + private Dictionary _hookNamesById = []; /// The system hooks. - private Dictionary> _hooksByInteractionByResource = new(); + private Dictionary> _hooksByInteractionByResource = []; /// The loaded directives. - private HashSet _loadedDirectives = new(); + private HashSet _loadedDirectives = []; + + /// List of ids of the loaded packages. + private HashSet _loadedPackageIds = []; /// The loaded supplements. - private HashSet _loadedSupplements = new(); + private HashSet _loadedSupplements = []; /// Values that represent load state codes. private enum LoadStateCodes @@ -135,10 +139,10 @@ private enum LoadStateCodes private int _maxResourceCount = 0; /// Queue of identifiers of resources (used for max resource cleaning). - private ConcurrentQueue _resourceQ = new(); + private ConcurrentQueue _resourceQ = []; /// The received notifications. - private ConcurrentDictionary> _receivedNotifications = new(); + private ConcurrentDictionary> _receivedNotifications = []; /// (Immutable) The received notification window ticks. private static readonly long _receivedNotificationWindowTicks = TimeSpan.FromMinutes(10).Ticks; @@ -147,7 +151,7 @@ private enum LoadStateCodes private bool _hasProtected = false; /// List of identifiers for the protected. - private HashSet _protectedResources = new(); + private HashSet _protectedResources = []; /// The storage capacity timer. private System.Threading.Timer? _capacityMonitor = null; @@ -163,8 +167,11 @@ public VersionedFhirStore() _searchTester = new() { FhirStore = this, }; } - /// Gets a list of names of the loaded packages. - public HashSet LoadedPackages => _loadedDirectives; + /// Gets a list of the loaded package directives. + public HashSet LoadedPackageDirectives => _loadedDirectives; + + /// Gets a list of identifiers of the loaded packages. + public HashSet LoadedPackageIds => _loadedPackageIds; /// Gets the loaded supplements. public HashSet LoadedSupplements => _loadedSupplements; @@ -352,7 +359,8 @@ private void CheckLoadedOperations() } if ((!string.IsNullOrEmpty(fhirOp.RequiresPackage)) && - (!_loadedDirectives.Contains(fhirOp.RequiresPackage))) + (!_loadedDirectives.Contains(fhirOp.RequiresPackage)) && + (!_loadedPackageIds.Contains(fhirOp.RequiresPackage))) { continue; } @@ -396,7 +404,8 @@ private void DiscoverInteractionHooks() } if ((!string.IsNullOrEmpty(hook.RequiresPackage)) && - (!_loadedDirectives.Contains(hook.RequiresPackage))) + (!_loadedDirectives.Contains(hook.RequiresPackage)) && + (!_loadedPackageIds.Contains(hook.RequiresPackage))) { continue; } @@ -474,18 +483,23 @@ public void LoadPackage( { _loadReprocess = new(); _loadState = LoadStateCodes.Read; - + bool success; DirectoryInfo di; + string fhirVersionSuffix = "." + _config.FhirVersion.ToRLiteral().ToLowerInvariant(); + if ((!string.IsNullOrEmpty(directive)) && (!string.IsNullOrEmpty(directory))) { _loadedDirectives.Add(directive); - if (directive.Contains('#')) + + string packageId = directive.Split('#', '@').First(); + _ = _loadedPackageIds.Add(packageId); + if (packageId.EndsWith(fhirVersionSuffix)) { - _loadedDirectives.Add(directive.Split('#')[0]); + _ = _loadedPackageIds.Add(packageId[..^(fhirVersionSuffix.Length)]); } Console.WriteLine($"Store[{_config.ControllerName}] loading {directive}"); @@ -511,10 +525,6 @@ public void LoadPackage( } } - if (!includeExamples) - { - } - FileInfo[] files; if ((!includeExamples) && (!string.IsNullOrEmpty(libDir)) && @@ -540,6 +550,11 @@ public void LoadPackage( // process normally default: + if (file.Name.EndsWith(".openapi.json") || + file.Name.EndsWith(".schema.json")) + { + continue; + } break; } @@ -1226,7 +1241,7 @@ public bool PerformInteraction( default: response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotImplemented, $"Interaction not implemented: {ctx.Interaction}", OperationOutcome.IssueType.NotSupported), @@ -1299,7 +1314,7 @@ public bool InstanceCreate( } else { - HttpStatusCode sc = Utils.TryDeserializeFhir( + HttpStatusCode sc = SerializationUtils.TryDeserializeFhir( ctx.SourceContent, ctx.SourceFormat, out r, @@ -1307,7 +1322,7 @@ public bool InstanceCreate( if ((!sc.IsSuccessful()) || (r == null)) { - OperationOutcome outcome = Utils.BuildOutcomeForRequest( + OperationOutcome outcome = SerializationUtils.BuildOutcomeForRequest( sc, $"Failed to deserialize resource, format: {ctx.SourceFormat}, error: {exMessage}", OperationOutcome.IssueType.Structure); @@ -1315,7 +1330,7 @@ public bool InstanceCreate( response = new() { Outcome = outcome, - SerializedOutcome = Utils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), + SerializedOutcome = SerializationUtils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), StatusCode = sc, }; @@ -1328,8 +1343,8 @@ public bool InstanceCreate( r!, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -1357,7 +1372,7 @@ internal bool DoInstanceCreate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Resource type: {content.TypeName} does not match request: {resourceType}", OperationOutcome.IssueType.Invalid), @@ -1370,7 +1385,7 @@ internal bool DoInstanceCreate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource type: {resourceType} is not supported", OperationOutcome.IssueType.NotSupported), @@ -1439,7 +1454,7 @@ internal bool DoInstanceCreate( ETag = string.IsNullOrEmpty(r.Meta?.VersionId) ? string.Empty : $"W/\"{r.Meta.VersionId}\"", LastModified = r.Meta?.LastUpdated == null ? string.Empty : r.Meta.LastUpdated.Value.UtcDateTime.ToString("r"), Location = $"{_config.BaseUrl}/{resourceType}/{r.Id}", - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.OK, $"Created {resourceType}/{r.Id}"), StatusCode = HttpStatusCode.OK, @@ -1452,7 +1467,7 @@ internal bool DoInstanceCreate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"If-None-Exist query returned too many matches: {bundle?.Total}"), StatusCode = HttpStatusCode.PreconditionFailed, @@ -1465,7 +1480,7 @@ internal bool DoInstanceCreate( { response = new() { - Outcome = searchResp.Outcome ?? Utils.BuildOutcomeForRequest( + Outcome = searchResp.Outcome ?? SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"If-None-Exist search failed: {ctx.IfNoneExist}"), StatusCode = HttpStatusCode.PreconditionFailed, @@ -1512,7 +1527,7 @@ internal bool DoInstanceCreate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, "Failed to create resource"), StatusCode = HttpStatusCode.InternalServerError, @@ -1537,7 +1552,7 @@ internal bool DoInstanceCreate( ETag = string.IsNullOrEmpty(stored.Meta?.VersionId) ? string.Empty : $"W/\"{stored.Meta.VersionId}\"", LastModified = stored.Meta?.LastUpdated == null ? string.Empty : stored.Meta.LastUpdated.Value.UtcDateTime.ToString("r"), Location = $"{_config.BaseUrl}/{resourceType}/{stored.Id}", - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.Created, $"Created {resourceType}/{stored.Id}"), StatusCode = HttpStatusCode.Created, @@ -1563,7 +1578,7 @@ public bool ProcessBundle( } else { - HttpStatusCode sc = Utils.TryDeserializeFhir( + HttpStatusCode sc = SerializationUtils.TryDeserializeFhir( ctx.SourceContent, ctx.SourceFormat, out r, @@ -1571,7 +1586,7 @@ public bool ProcessBundle( if ((!sc.IsSuccessful()) || (r == null)) { - OperationOutcome outcome = Utils.BuildOutcomeForRequest( + OperationOutcome outcome = SerializationUtils.BuildOutcomeForRequest( sc, $"Failed to deserialize resource, format: {ctx.SourceFormat}, error: {exMessage}", OperationOutcome.IssueType.Structure); @@ -1579,7 +1594,7 @@ public bool ProcessBundle( response = new() { Outcome = outcome, - SerializedOutcome = Utils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), + SerializedOutcome = SerializationUtils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), StatusCode = sc, }; @@ -1590,7 +1605,7 @@ public bool ProcessBundle( if ((r!.TypeName != resourceType) || (r is not Bundle requestBundle)) { - OperationOutcome outcome = Utils.BuildOutcomeForRequest( + OperationOutcome outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Cannot process non-Bundle resource type ({r.TypeName}) as a Bundle", OperationOutcome.IssueType.Invalid); @@ -1598,7 +1613,7 @@ public bool ProcessBundle( response = new() { Outcome = outcome, - SerializedOutcome = Utils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), + SerializedOutcome = SerializationUtils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), StatusCode = HttpStatusCode.UnprocessableEntity, }; @@ -1610,8 +1625,8 @@ public bool ProcessBundle( requestBundle, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -1652,7 +1667,7 @@ internal bool DoProcessBundle( default: { - OperationOutcome outcome = Utils.BuildOutcomeForRequest( + OperationOutcome outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Unsupported Bundle process request! Type: {requestBundle.Type}", OperationOutcome.IssueType.NotSupported); @@ -1660,7 +1675,7 @@ internal bool DoProcessBundle( response = new() { Outcome = outcome, - SerializedOutcome = Utils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), + SerializedOutcome = SerializationUtils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), StatusCode = HttpStatusCode.UnprocessableEntity, }; @@ -1671,7 +1686,7 @@ internal bool DoProcessBundle( response = new() { Resource = responseBundle, - Outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Processed {requestBundle.Type} bundle"), + Outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Processed {requestBundle.Type} bundle"), StatusCode = HttpStatusCode.OK, }; @@ -1690,8 +1705,8 @@ public bool InstanceDelete( ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -1715,7 +1730,7 @@ internal bool DoInstanceDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource type: {ctx.ResourceType} is not supported", OperationOutcome.IssueType.NotSupported), @@ -1784,7 +1799,7 @@ internal bool DoInstanceDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource {ctx.ResourceType}/{ctx.Id} not found"), StatusCode = HttpStatusCode.NotFound, @@ -1797,7 +1812,7 @@ internal bool DoInstanceDelete( Resource = resource, ResourceType = resource.TypeName, Id = resource.Id, - Outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Deleted {ctx.ResourceType}/{ctx.Id}"), + Outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Deleted {ctx.ResourceType}/{ctx.Id}"), StatusCode = HttpStatusCode.OK, }; return true; @@ -1831,8 +1846,8 @@ public bool InstanceRead( { bool success = DoInstanceRead(ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -1856,7 +1871,7 @@ internal bool DoInstanceRead( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, "Resource type is required", OperationOutcome.IssueType.Structure), @@ -1869,7 +1884,7 @@ internal bool DoInstanceRead( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource type: {ctx.ResourceType} is not supported", OperationOutcome.IssueType.NotSupported), @@ -1882,7 +1897,7 @@ internal bool DoInstanceRead( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, "ID required for instance level read.", OperationOutcome.IssueType.Structure), @@ -1948,7 +1963,7 @@ internal bool DoInstanceRead( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource: {ctx.ResourceType}/{ctx.Id} not found", OperationOutcome.IssueType.Exception), @@ -1965,7 +1980,7 @@ internal bool DoInstanceRead( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"If-Match: {ctx.IfMatch} does not equal found eTag: {eTag}", OperationOutcome.IssueType.BusinessRule), @@ -1982,7 +1997,7 @@ internal bool DoInstanceRead( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotModified, $"Last modified: {lastModified} is prior to If-Modified-Since: {ctx.IfModifiedSince}", OperationOutcome.IssueType.Informational), @@ -1998,7 +2013,7 @@ internal bool DoInstanceRead( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, "Prior version exists, but If-None-Match is *"), StatusCode = HttpStatusCode.PreconditionFailed, @@ -2013,7 +2028,7 @@ internal bool DoInstanceRead( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotModified, $"Read {ctx.ResourceType}/{ctx.Id} found version: {eTag}, equals If-None-Match: {ctx.IfNoneMatch}"), StatusCode = HttpStatusCode.NotModified, @@ -2032,7 +2047,7 @@ internal bool DoInstanceRead( ETag = eTag, LastModified = lastModified, Location = string.IsNullOrEmpty(r.Id) ? string.Empty : $"{_config.BaseUrl}/{r.TypeName}/{r.Id}", - Outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Read {r.TypeName}/{r.Id}"), + Outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Read {r.TypeName}/{r.Id}"), StatusCode = HttpStatusCode.OK, }; @@ -2051,7 +2066,7 @@ public bool TryInstanceUpdate( out string resourceType, out string id) { - HttpStatusCode sc = Utils.TryDeserializeFhir( + HttpStatusCode sc = SerializationUtils.TryDeserializeFhir( content, mimeType, out Resource? r, @@ -2144,7 +2159,7 @@ public bool InstanceUpdate( } else { - HttpStatusCode sc = Utils.TryDeserializeFhir( + HttpStatusCode sc = SerializationUtils.TryDeserializeFhir( ctx.SourceContent, ctx.SourceFormat, out r, @@ -2152,7 +2167,7 @@ public bool InstanceUpdate( if ((!sc.IsSuccessful()) || (r == null)) { - OperationOutcome outcome = Utils.BuildOutcomeForRequest( + OperationOutcome outcome = SerializationUtils.BuildOutcomeForRequest( sc, $"Failed to deserialize resource, format: {ctx.SourceFormat}, error: {exMessage}", OperationOutcome.IssueType.Structure); @@ -2160,7 +2175,7 @@ public bool InstanceUpdate( response = new() { Outcome = outcome, - SerializedOutcome = Utils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), + SerializedOutcome = SerializationUtils.SerializeFhir(outcome, ctx.DestinationFormat, ctx.SerializePretty), StatusCode = sc, }; @@ -2172,7 +2187,7 @@ public bool InstanceUpdate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, "Resource is required", OperationOutcome.IssueType.Structure), @@ -2186,8 +2201,8 @@ public bool InstanceUpdate( r, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -2225,7 +2240,7 @@ internal bool DoInstanceUpdate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Resource type: {content.TypeName} does not match request: {resourceType}", OperationOutcome.IssueType.Invalid), @@ -2238,7 +2253,7 @@ internal bool DoInstanceUpdate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource type: {resourceType} is not supported", OperationOutcome.IssueType.NotSupported), @@ -2306,7 +2321,7 @@ internal bool DoInstanceUpdate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"Conditional update query returned a match with a id: {bundle.Entry[0].Resource.Id}, expected {id}"), StatusCode = HttpStatusCode.PreconditionFailed, @@ -2321,7 +2336,7 @@ internal bool DoInstanceUpdate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"Conditional update query returned too many matches: {bundle?.Total}"), StatusCode = HttpStatusCode.PreconditionFailed, @@ -2334,7 +2349,7 @@ internal bool DoInstanceUpdate( { response = new() { - Outcome = searchResp.Outcome ?? Utils.BuildOutcomeForRequest( + Outcome = searchResp.Outcome ?? SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"Conditional update query failed: {ctx.UrlQuery}"), StatusCode = HttpStatusCode.PreconditionFailed, @@ -2387,7 +2402,7 @@ internal bool DoInstanceUpdate( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, "Failed to update resource"), StatusCode = HttpStatusCode.InternalServerError, @@ -2904,7 +2919,7 @@ public string SerializeSubscriptionEvents( return string.Empty; } - string serialized = Utils.SerializeFhir( + string serialized = SerializationUtils.SerializeFhir( bundle, string.IsNullOrEmpty(contentType) ? _subscriptions[subscriptionId].ContentType : contentType, pretty, @@ -2939,7 +2954,7 @@ public bool TrySerializeToSubscription( destFormat = "application/fhir+json"; } - serialized = Utils.SerializeFhir(subscription, destFormat, pretty); + serialized = SerializationUtils.SerializeFhir(subscription, destFormat, pretty); return true; } @@ -3179,8 +3194,8 @@ public bool SystemOperation( ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -3206,7 +3221,7 @@ internal bool DoSystemOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Operation {ctx.OperationName} does not have an executable implementation on this server."), StatusCode = HttpStatusCode.NotFound, @@ -3220,7 +3235,7 @@ internal bool DoSystemOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Operation {ctx.OperationName} does not allow system-level execution.", OperationOutcome.IssueType.NotSupported), @@ -3241,7 +3256,7 @@ internal bool DoSystemOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnsupportedMediaType, $"Operation {ctx.OperationName} does not consume non-FHIR content.", OperationOutcome.IssueType.Invalid), @@ -3252,14 +3267,14 @@ internal bool DoSystemOperation( } else if (!string.IsNullOrEmpty(ctx.SourceContent)) { - HttpStatusCode deserializeSc = Utils.TryDeserializeFhir(ctx.SourceContent, ctx.SourceFormat, out r, out _); + HttpStatusCode deserializeSc = SerializationUtils.TryDeserializeFhir(ctx.SourceContent, ctx.SourceFormat, out r, out _); if ((!deserializeSc.IsSuccessful()) && (!op.AcceptsNonFhir)) { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnsupportedMediaType, $"Operation {ctx.OperationName} does not consume non-FHIR content.", OperationOutcome.IssueType.Invalid), @@ -3351,7 +3366,7 @@ internal bool DoSystemOperation( Resource = r, ResourceType = r?.TypeName ?? string.Empty, Id = r?.Id ?? string.Empty, - Outcome = opResponse.Outcome ?? Utils.BuildOutcomeForRequest( + Outcome = opResponse.Outcome ?? SerializationUtils.BuildOutcomeForRequest( opResponse.StatusCode ?? (success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError), $"System-Level Operation {ctx.OperationName} {(success ? "succeeded" : "failed")}: {opResponse.StatusCode}"), StatusCode = success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError, @@ -3371,8 +3386,8 @@ public bool TypeOperation( ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -3396,7 +3411,7 @@ internal bool DoTypeOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource type {ctx.ResourceType} does not exist on this server.", OperationOutcome.IssueType.NotSupported), @@ -3409,7 +3424,7 @@ internal bool DoTypeOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Operation {ctx.OperationName} does not have an executable implementation on this server."), StatusCode = HttpStatusCode.NotFound, @@ -3423,7 +3438,7 @@ internal bool DoTypeOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Operation {ctx.OperationName} does not allow type-level execution.", OperationOutcome.IssueType.NotSupported), @@ -3436,7 +3451,7 @@ internal bool DoTypeOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Operation {ctx.OperationName} is not defined for resource: {ctx.ResourceType}.", OperationOutcome.IssueType.NotSupported), @@ -3457,7 +3472,7 @@ internal bool DoTypeOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnsupportedMediaType, $"Operation {ctx.OperationName} does not consume non-FHIR content.", OperationOutcome.IssueType.Invalid), @@ -3468,14 +3483,14 @@ internal bool DoTypeOperation( } else if (!string.IsNullOrEmpty(ctx.SourceContent)) { - HttpStatusCode deserializeSc = Utils.TryDeserializeFhir(ctx.SourceContent, ctx.SourceFormat, out r, out _); + HttpStatusCode deserializeSc = SerializationUtils.TryDeserializeFhir(ctx.SourceContent, ctx.SourceFormat, out r, out _); if ((!deserializeSc.IsSuccessful()) && (!op.AcceptsNonFhir)) { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnsupportedMediaType, $"Operation {ctx.OperationName} does not consume non-FHIR content.", OperationOutcome.IssueType.Invalid), @@ -3567,7 +3582,7 @@ internal bool DoTypeOperation( Resource = r, ResourceType = r?.TypeName ?? string.Empty, Id = r?.Id ?? string.Empty, - Outcome = opResponse.Outcome ?? Utils.BuildOutcomeForRequest( + Outcome = opResponse.Outcome ?? SerializationUtils.BuildOutcomeForRequest( opResponse.StatusCode ?? (success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError), $"Type-Level Operation {ctx.ResourceType}/{ctx.OperationName} {(success ? "succeeded" : "failed")}: {opResponse.StatusCode}"), StatusCode = success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError, @@ -3587,8 +3602,8 @@ public bool InstanceOperation( ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -3612,7 +3627,7 @@ internal bool DoInstanceOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource type {ctx.ResourceType} does not exist on this server.", OperationOutcome.IssueType.NotSupported), @@ -3626,7 +3641,7 @@ internal bool DoInstanceOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Instance {ctx.ResourceType}/{ctx.Id} does not exist on this server."), StatusCode = HttpStatusCode.NotFound, @@ -3638,7 +3653,7 @@ internal bool DoInstanceOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Operation {ctx.OperationName} does not have an executable implementation on this server."), StatusCode = HttpStatusCode.NotFound, @@ -3652,7 +3667,7 @@ internal bool DoInstanceOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Operation {ctx.OperationName} does not allow instance-level execution.", OperationOutcome.IssueType.NotSupported), @@ -3665,7 +3680,7 @@ internal bool DoInstanceOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, $"Operation {ctx.OperationName} is not defined for resource: {ctx.ResourceType}.", OperationOutcome.IssueType.NotSupported), @@ -3686,7 +3701,7 @@ internal bool DoInstanceOperation( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnsupportedMediaType, $"Operation {ctx.OperationName} does not consume non-FHIR content.", OperationOutcome.IssueType.Invalid), @@ -3697,14 +3712,14 @@ internal bool DoInstanceOperation( } else if (!string.IsNullOrEmpty(ctx.SourceContent)) { - HttpStatusCode deserializeSc = Utils.TryDeserializeFhir(ctx.SourceContent, ctx.SourceFormat, out r, out _); + HttpStatusCode deserializeSc = SerializationUtils.TryDeserializeFhir(ctx.SourceContent, ctx.SourceFormat, out r, out _); if ((!deserializeSc.IsSuccessful()) && (!op.AcceptsNonFhir)) { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnsupportedMediaType, $"Operation {ctx.OperationName} does not consume non-FHIR content.", OperationOutcome.IssueType.Invalid), @@ -3798,7 +3813,7 @@ internal bool DoInstanceOperation( Resource = r, ResourceType = r?.TypeName ?? string.Empty, Id = r?.Id ?? string.Empty, - Outcome = opResponse.Outcome ?? Utils.BuildOutcomeForRequest( + Outcome = opResponse.Outcome ?? SerializationUtils.BuildOutcomeForRequest( opResponse.StatusCode ?? (success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError), $"Instance-Level Operation {ctx.ResourceType}/{ctx.Id}/{ctx.OperationName} {(success ? "succeeded" : "failed")}: {opResponse.StatusCode}"), StatusCode = success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError, @@ -3818,8 +3833,8 @@ public bool SystemDelete( ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -3848,7 +3863,7 @@ internal bool DoSystemDelete( { response = new() { - Outcome = searchResp.Outcome ?? Utils.BuildOutcomeForRequest( + Outcome = searchResp.Outcome ?? SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, "System search failed"), StatusCode = HttpStatusCode.InternalServerError, @@ -3861,7 +3876,7 @@ internal bool DoSystemDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, "No matches found for system delete"), StatusCode = HttpStatusCode.NotFound, @@ -3874,7 +3889,7 @@ internal bool DoSystemDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"Too many matches found for system delete: ({resultBundle.Total})", OperationOutcome.IssueType.MultipleMatches), @@ -3889,7 +3904,7 @@ internal bool DoSystemDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, $"Resource ({resultBundle.Entry.First().FullUrl}) not accessible post search!", OperationOutcome.IssueType.Processing), @@ -3971,7 +3986,7 @@ internal bool DoSystemDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, $"Matched delete resource {id} could not be deleted"), StatusCode = HttpStatusCode.InternalServerError, @@ -3984,7 +3999,7 @@ internal bool DoSystemDelete( Resource = resource, ResourceType = resourceType, Id = id, - Outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Deleted {resourceType}/{id}"), + Outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Deleted {resourceType}/{id}"), StatusCode = HttpStatusCode.OK, }; return true; @@ -4002,8 +4017,8 @@ public bool TypeDelete( ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -4027,7 +4042,7 @@ internal bool DoTypeDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, "Resource type is required for type-delete interactions", OperationOutcome.IssueType.Structure), @@ -4040,7 +4055,7 @@ internal bool DoTypeDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource type: {ctx.ResourceType} is not supported", OperationOutcome.IssueType.NotSupported), @@ -4058,7 +4073,7 @@ internal bool DoTypeDelete( { response = new() { - Outcome = searchResp.Outcome ?? Utils.BuildOutcomeForRequest( + Outcome = searchResp.Outcome ?? SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, $"Type search against {ctx.ResourceType} failed"), StatusCode = searchResp.StatusCode ?? HttpStatusCode.InternalServerError, @@ -4071,7 +4086,7 @@ internal bool DoTypeDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"No matches found for type ({ctx.ResourceType}) delete"), StatusCode = HttpStatusCode.NotFound, @@ -4084,7 +4099,7 @@ internal bool DoTypeDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.PreconditionFailed, $"Too many matches found for type ({ctx.ResourceType}) delete: ({resultBundle.Total})", OperationOutcome.IssueType.MultipleMatches), @@ -4099,7 +4114,7 @@ internal bool DoTypeDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, $"Resource ({resultBundle.Entry.First().FullUrl}) not accessible post search!", OperationOutcome.IssueType.Processing), @@ -4187,7 +4202,7 @@ internal bool DoTypeDelete( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, $"Matched delete resource {id} could not be deleted"), StatusCode = HttpStatusCode.InternalServerError, @@ -4200,7 +4215,7 @@ internal bool DoTypeDelete( Resource = resource, ResourceType = resourceType, Id = id, - Outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Deleted {resourceType}/{id}"), + Outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.OK, $"Deleted {resourceType}/{id}"), StatusCode = HttpStatusCode.OK, }; return true; @@ -4218,8 +4233,8 @@ public bool TypeSearch( ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -4245,7 +4260,7 @@ internal bool DoTypeSearch( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.BadRequest, "Resource type is required for type-delete interactions", OperationOutcome.IssueType.Structure), @@ -4258,7 +4273,7 @@ internal bool DoTypeSearch( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotFound, $"Resource type: {ctx.ResourceType} is not supported", OperationOutcome.IssueType.NotSupported), @@ -4309,7 +4324,7 @@ internal bool DoTypeSearch( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, $"Type Search against {ctx.ResourceType} failed", OperationOutcome.IssueType.Processing), @@ -4412,7 +4427,7 @@ internal bool DoTypeSearch( { Resource = bundle, ResourceType = "Bundle", - Outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.OK, $"System search successful"), + Outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.OK, $"System search successful"), StatusCode = HttpStatusCode.OK, }; return true; @@ -4430,8 +4445,8 @@ public bool SystemSearch( ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -4470,7 +4485,7 @@ internal bool DoSystemSearch( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.Forbidden, $"System search with no resource types is too costly.", OperationOutcome.IssueType.TooCostly), @@ -4523,7 +4538,7 @@ internal bool DoSystemSearch( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, $"System search into {resourceType} failed"), StatusCode = HttpStatusCode.InternalServerError, @@ -4640,7 +4655,7 @@ internal bool DoSystemSearch( { Resource = bundle, ResourceType = "Bundle", - Outcome = Utils.BuildOutcomeForRequest(HttpStatusCode.OK, $"System search successful"), + Outcome = SerializationUtils.BuildOutcomeForRequest(HttpStatusCode.OK, $"System search successful"), StatusCode = HttpStatusCode.OK, }; return true; @@ -4934,8 +4949,8 @@ public bool GetMetadata( { bool success = DoGetMetadata(ctx, out response); - string sr = response.Resource == null ? string.Empty : Utils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); - string so = response.Outcome == null ? string.Empty : Utils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); + string sr = response.Resource == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Resource, ctx.DestinationFormat, ctx.SerializePretty); + string so = response.Outcome == null ? string.Empty : SerializationUtils.SerializeFhir((Resource)response.Outcome, ctx.DestinationFormat, ctx.SerializePretty); response = response with { @@ -5027,7 +5042,7 @@ internal bool DoGetMetadata( { response = new() { - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.InternalServerError, $"CapabilityStatement could not be retrieved", OperationOutcome.IssueType.Exception), @@ -5042,7 +5057,7 @@ internal bool DoGetMetadata( Resource = r, ResourceType = "CapabilityStatement", Id = _capabilityStatementId, - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.OK, $"Retrieved current CapabilityStatement", OperationOutcome.IssueType.Success), @@ -5310,17 +5325,17 @@ private bool TryTestCapabilityFeatureInstantiates( /// required range. /// The SupportedFhirVersions to process. /// A FHIRVersion. - private FHIRVersion CommonToFirelyVersion(TenantConfiguration.SupportedFhirVersions v) + private FHIRVersion CommonToFirelyVersion(FhirReleases.FhirSequenceCodes v) { switch (v) { - case TenantConfiguration.SupportedFhirVersions.R4: + case FhirReleases.FhirSequenceCodes.R4: return FHIRVersion.N4_0_1; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: return FHIRVersion.N4_3_0; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: return FHIRVersion.N5_0_0; default: @@ -5380,9 +5395,9 @@ private CapabilityStatement UpdateCapabilities() string securityCodeSystemUrl = _config.FhirVersion switch { - TenantConfiguration.SupportedFhirVersions.R4 => "http://terminology.hl7.org/CodeSystem/restful-security-service", - TenantConfiguration.SupportedFhirVersions.R4B => "http://terminology.hl7.org/CodeSystem/restful-security-service", - TenantConfiguration.SupportedFhirVersions.R5 => "http://hl7.org/fhir/restful-security-service", + FhirReleases.FhirSequenceCodes.R4 => "http://terminology.hl7.org/CodeSystem/restful-security-service", + FhirReleases.FhirSequenceCodes.R4B => "http://terminology.hl7.org/CodeSystem/restful-security-service", + FhirReleases.FhirSequenceCodes.R5 => "http://hl7.org/fhir/restful-security-service", _ => "http://hl7.org/fhir/restful-security-service", }; @@ -5664,7 +5679,7 @@ private void ProcessBatch( Response = new Bundle.ResponseComponent() { Status = HttpStatusCode.BadRequest.ToString(), - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.UnprocessableEntity, "Entry is missing a request", OperationOutcome.IssueType.Required), @@ -5699,7 +5714,7 @@ private void ProcessBatch( Response = new Bundle.ResponseComponent() { Status = HttpStatusCode.InternalServerError.ToString(), - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotImplemented, $"Request could not be parsed to known interaction: {entry.Request.Method} {entry.Request.Url}", OperationOutcome.IssueType.NotSupported), @@ -5719,7 +5734,7 @@ private void ProcessBatch( Response = new Bundle.ResponseComponent() { Status = HttpStatusCode.Unauthorized.ToString(), - Outcome = Utils.BuildOutcomeForRequest( + Outcome = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.Unauthorized, $"Unauthorized request: {entry.Request.Method} {entry.Request.Url}, parsed interaction: {entryCtx.Interaction}", OperationOutcome.IssueType.Forbidden), @@ -5751,7 +5766,7 @@ private void ProcessBatch( { if ((opResponse.Outcome == null) || (opResponse.Outcome is not OperationOutcome oo)) { - oo = Utils.BuildOutcomeForRequest( + oo = SerializationUtils.BuildOutcomeForRequest( HttpStatusCode.NotImplemented, $"Unsupported request: {entry.Request.Method} {entry.Request.Url}, parsed interaction: {entryCtx.Interaction}", OperationOutcome.IssueType.NotSupported); diff --git a/src/FhirStore.R4/FhirCandle.R4.csproj b/src/FhirStore.R4/FhirCandle.R4.csproj index dd2f8b2..6f9df8d 100644 --- a/src/FhirStore.R4/FhirCandle.R4.csproj +++ b/src/FhirStore.R4/FhirCandle.R4.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/FhirStore.R4/InteractionHooks/CDexTaskProcess.cs b/src/FhirStore.R4/InteractionHooks/CDexTaskProcess.cs index 6de9e36..8354977 100644 --- a/src/FhirStore.R4/InteractionHooks/CDexTaskProcess.cs +++ b/src/FhirStore.R4/InteractionHooks/CDexTaskProcess.cs @@ -21,9 +21,9 @@ public class CDexTaskProcess : IFhirInteractionHook public string Id => "036a8204-4d4f-46fc-a715-900bc2790a16"; /// Gets the supported FHIR versions. - public HashSet SupportedFhirVersions => new() + public HashSet SupportedFhirVersions => new() { - TenantConfiguration.SupportedFhirVersions.R4, + FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4, }; /// diff --git a/src/FhirStore.R4/Operations/OpPasClaimInquiry.cs b/src/FhirStore.R4/Operations/OpPasClaimInquiry.cs index 6efd839..eb1d961 100644 --- a/src/FhirStore.R4/Operations/OpPasClaimInquiry.cs +++ b/src/FhirStore.R4/Operations/OpPasClaimInquiry.cs @@ -25,9 +25,9 @@ public class OpPasClaimInquiry : IFhirOperation public string OperationVersion => "1.2.0"; /// Gets the canonical by FHIR version. - public Dictionary CanonicalByFhirVersion => new() + public Dictionary CanonicalByFhirVersion => new() { - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4, "http://hl7.org/fhir/us/davinci-pas/OperationDefinition/Claim-inquiry" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4, "http://hl7.org/fhir/us/davinci-pas/OperationDefinition/Claim-inquiry" }, }; /// Gets a value indicating whether this operation is a named query. @@ -276,7 +276,7 @@ public bool DoOperation( /// The FHIR version. /// The definition. public Hl7.Fhir.Model.OperationDefinition? GetDefinition( - FhirCandle.Models.TenantConfiguration.SupportedFhirVersions fhirVersion) + FhirCandle.Utils.FhirReleases.FhirSequenceCodes fhirVersion) { // operation has canonical definition in package return null; diff --git a/src/FhirStore.R4/Operations/OpPasClaimSubmit.cs b/src/FhirStore.R4/Operations/OpPasClaimSubmit.cs index f1fd779..88d49c8 100644 --- a/src/FhirStore.R4/Operations/OpPasClaimSubmit.cs +++ b/src/FhirStore.R4/Operations/OpPasClaimSubmit.cs @@ -28,9 +28,9 @@ public class OpPasClaimSubmit : IFhirOperation public string OperationVersion => "1.2.0"; /// Gets the canonical by FHIR version. - public Dictionary CanonicalByFhirVersion => new() + public Dictionary CanonicalByFhirVersion => new() { - { FhirCandle.Models.TenantConfiguration.SupportedFhirVersions.R4, "http://hl7.org/fhir/us/davinci-pas/OperationDefinition/Claim-submit" }, + { FhirCandle.Utils.FhirReleases.FhirSequenceCodes.R4, "http://hl7.org/fhir/us/davinci-pas/OperationDefinition/Claim-submit" }, }; /// Gets a value indicating whether this operation is a named query. @@ -454,7 +454,7 @@ public bool DoOperation( /// The FHIR version. /// The definition. public Hl7.Fhir.Model.OperationDefinition? GetDefinition( - FhirCandle.Models.TenantConfiguration.SupportedFhirVersions fhirVersion) + FhirCandle.Utils.FhirReleases.FhirSequenceCodes fhirVersion) { // operation has canonical definition in package return null; diff --git a/src/FhirStore.R4B/FhirCandle.R4B.csproj b/src/FhirStore.R4B/FhirCandle.R4B.csproj index cef3bcb..1abde5e 100644 --- a/src/FhirStore.R4B/FhirCandle.R4B.csproj +++ b/src/FhirStore.R4B/FhirCandle.R4B.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/FhirStore.R5/FhirCandle.R5.csproj b/src/FhirStore.R5/FhirCandle.R5.csproj index a877b3b..941a03b 100644 --- a/src/FhirStore.R5/FhirCandle.R5.csproj +++ b/src/FhirStore.R5/FhirCandle.R5.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/fhir-candle.Tests/AuthTests.cs b/src/fhir-candle.Tests/AuthTests.cs index c29e977..1c1426a 100644 --- a/src/fhir-candle.Tests/AuthTests.cs +++ b/src/fhir-candle.Tests/AuthTests.cs @@ -11,6 +11,7 @@ using FhirCandle.Models; using FhirCandle.Smart; using FhirCandle.Storage; +using FhirCandle.Utils; using FluentAssertions; using Microsoft.IdentityModel.Tokens; using Org.BouncyCastle.Asn1.Ocsp; @@ -364,7 +365,7 @@ public AuthTestFixture() { ConfigR4 = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4, + FhirVersion = FhirReleases.FhirSequenceCodes.R4, ControllerName = Name, BaseUrl = "http://localhost:5826/fhir/r4", SmartRequired = true, @@ -377,16 +378,16 @@ public AuthTestFixture() AuthR4 = new SmartAuthManager( Tenants, - new ServerConfiguration() + new() { PublicUrl = "http://localhost:5826/fhir/r4", ListenPort = 5826, OpenBrowser = false, - TenantsR4 = new() { Name }, - SmartRequiredTenants = new() { Name }, + TenantsR4 = [ Name ], + SmartRequiredTenants = [ Name ], }, null); AuthR4.Init(); } -} \ No newline at end of file +} diff --git a/src/fhir-candle.Tests/FhirStoreTests.cs b/src/fhir-candle.Tests/FhirStoreTests.cs index 509f377..3686dec 100644 --- a/src/fhir-candle.Tests/FhirStoreTests.cs +++ b/src/fhir-candle.Tests/FhirStoreTests.cs @@ -10,6 +10,7 @@ using fhir.candle.Tests.Models; using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using FluentAssertions; using Hl7.Fhir.Model; using Hl7.Fhir.Rest; @@ -31,15 +32,15 @@ public class FhirStoreTests { new object[] { - TenantConfiguration.SupportedFhirVersions.R4, + FhirReleases.FhirSequenceCodes.R4, }, new object[] { - TenantConfiguration.SupportedFhirVersions.R4B, + FhirReleases.FhirSequenceCodes.R4B, }, new object[] { - TenantConfiguration.SupportedFhirVersions.R5, + FhirReleases.FhirSequenceCodes.R5, }, }; @@ -62,14 +63,14 @@ public class FhirStoreTests internal IFhirStore _candleR5; /// The stores. - internal Dictionary _stores = new(); + internal Dictionary _stores = new(); /// The expected REST resources. - internal Dictionary _expectedRestResources = new() + internal Dictionary _expectedRestResources = new() { - { TenantConfiguration.SupportedFhirVersions.R4, 146 }, - { TenantConfiguration.SupportedFhirVersions.R4B, 140 }, - { TenantConfiguration.SupportedFhirVersions.R5, 157 }, + { FhirReleases.FhirSequenceCodes.R4, 146 }, + { FhirReleases.FhirSequenceCodes.R4B, 140 }, + { FhirReleases.FhirSequenceCodes.R5, 157 }, }; /// @@ -80,7 +81,7 @@ public FhirStoreTests() { _configR4 = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4, + FhirVersion = FhirReleases.FhirSequenceCodes.R4, ControllerName = "r4", BaseUrl = "http://localhost/fhir/r4", AllowExistingId = true, @@ -89,7 +90,7 @@ public FhirStoreTests() _configR4B = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4B, + FhirVersion = FhirReleases.FhirSequenceCodes.R4B, ControllerName = "r4b", BaseUrl = "http://localhost/fhir/r4b", AllowExistingId = true, @@ -98,7 +99,7 @@ public FhirStoreTests() _configR5 = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R5, + FhirVersion = FhirReleases.FhirSequenceCodes.R5, ControllerName = "r5", BaseUrl = "http://localhost/fhir/r5", AllowExistingId = true, @@ -107,15 +108,15 @@ public FhirStoreTests() _candleR4 = new candleR4::FhirCandle.Storage.VersionedFhirStore(); _candleR4.Init(_configR4); - _stores.Add(TenantConfiguration.SupportedFhirVersions.R4, _candleR4); + _stores.Add(FhirReleases.FhirSequenceCodes.R4, _candleR4); _candleR4B = new candleR4B::FhirCandle.Storage.VersionedFhirStore(); _candleR4B.Init(_configR4B); - _stores.Add(TenantConfiguration.SupportedFhirVersions.R4B, _candleR4B); + _stores.Add(FhirReleases.FhirSequenceCodes.R4B, _candleR4B); _candleR5 = new candleR5::FhirCandle.Storage.VersionedFhirStore(); _candleR5.Init(_configR5); - _stores.Add(TenantConfiguration.SupportedFhirVersions.R5, _candleR5); + _stores.Add(FhirReleases.FhirSequenceCodes.R5, _candleR5); } /// Gets store for version. @@ -123,17 +124,17 @@ public FhirStoreTests() /// illegal values. /// The version. /// The store for version. - public IFhirStore GetStoreForVersion(TenantConfiguration.SupportedFhirVersions version) + public IFhirStore GetStoreForVersion(FhirReleases.FhirSequenceCodes version) { switch (version) { - case TenantConfiguration.SupportedFhirVersions.R4: + case FhirReleases.FhirSequenceCodes.R4: return _candleR4; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: return _candleR4B; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: return _candleR5; } @@ -161,7 +162,7 @@ public MetadataJson(FhirStoreTests fixture, ITestOutputHelper testOutputHelper) [Theory] [MemberData(nameof(Configurations))] - public void GetMetadata(TenantConfiguration.SupportedFhirVersions version) + public void GetMetadata(FhirReleases.FhirSequenceCodes version) { IFhirStore fhirStore = _fixture.GetStoreForVersion(version); @@ -219,7 +220,7 @@ public MetadataXml(FhirStoreTests fixture, ITestOutputHelper testOutputHelper) [Theory] [MemberData(nameof(Configurations))] - public void GetMetadata(TenantConfiguration.SupportedFhirVersions version) + public void GetMetadata(FhirReleases.FhirSequenceCodes version) { IFhirStore fhirStore = _fixture.GetStoreForVersion(version); @@ -278,7 +279,7 @@ public TestPatientCRUD(FhirStoreTests fixture, ITestOutputHelper testOutputHelpe [Theory] [MemberData(nameof(Configurations))] - public void PatientCRUD(TenantConfiguration.SupportedFhirVersions version) + public void PatientCRUD(FhirReleases.FhirSequenceCodes version) { string json1 = "{\"resourceType\":\"" + _resourceType + "\",\"id\":\"" + _id + "\",\"language\":\"en\"}"; string json2 = "{\"resourceType\":\"" + _resourceType + "\",\"id\":\"" + _id + "\",\"language\":\"en-US\"}"; @@ -424,7 +425,7 @@ public TestResourceWrongLocation(FhirStoreTests fixture, ITestOutputHelper testO [Theory] [MemberData(nameof(Configurations))] - public void ResourceWrongLocation(TenantConfiguration.SupportedFhirVersions version) + public void ResourceWrongLocation(FhirReleases.FhirSequenceCodes version) { string json = "{\"resourceType\":\"" + _resourceType1 + "\",\"id\":\"" + _id + "\",\"language\":\"en\"}"; @@ -474,7 +475,7 @@ public TestResourceInvalidElement(FhirStoreTests fixture, ITestOutputHelper test [Theory] [MemberData(nameof(Configurations))] - public void ResourceWrongLocation(TenantConfiguration.SupportedFhirVersions version) + public void ResourceWrongLocation(FhirReleases.FhirSequenceCodes version) { string json = "{\"resourceType\":\"" + _resourceType + "\",\"id\":\"" + _id + "\",\"garbage\":true}"; @@ -630,4 +631,4 @@ public void DetermineInteraction(string verb, string url, StoreInteractionCodes? ctx?.Interaction.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/src/fhir-candle.Tests/FhirStoreTestsR4.cs b/src/fhir-candle.Tests/FhirStoreTestsR4.cs index 92e31d4..8827547 100644 --- a/src/fhir-candle.Tests/FhirStoreTestsR4.cs +++ b/src/fhir-candle.Tests/FhirStoreTestsR4.cs @@ -7,6 +7,7 @@ using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using fhir.candle.Tests.Extensions; using fhir.candle.Tests.Models; using FluentAssertions; @@ -28,7 +29,7 @@ public class FhirStoreTestsR4: IDisposable /// (Immutable) The configuration. private static readonly TenantConfiguration _config = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4, + FhirVersion = FhirReleases.FhirSequenceCodes.R4, ControllerName = "r4", BaseUrl = "http://localhost/fhir/r4", AllowExistingId = true, @@ -223,4 +224,4 @@ public void CreateSearchParameterCapabilityCount(string json) break; } } -} \ No newline at end of file +} diff --git a/src/fhir-candle.Tests/FhirStoreTestsR4B.cs b/src/fhir-candle.Tests/FhirStoreTestsR4B.cs index 51ca48c..6cb3695 100644 --- a/src/fhir-candle.Tests/FhirStoreTestsR4B.cs +++ b/src/fhir-candle.Tests/FhirStoreTestsR4B.cs @@ -7,6 +7,7 @@ using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using fhir.candle.Tests.Extensions; using fhir.candle.Tests.Models; using FluentAssertions; @@ -26,7 +27,7 @@ public class FhirStoreTestsR4B: IDisposable /// (Immutable) The configuration. private static readonly TenantConfiguration _config = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4B, + FhirVersion = FhirReleases.FhirSequenceCodes.R4B, ControllerName = "r4b", BaseUrl = "http://localhost/fhir/r4b", AllowExistingId = true, @@ -221,4 +222,4 @@ public void CreateSearchParameterCapabilityCount(string json) break; } } -} \ No newline at end of file +} diff --git a/src/fhir-candle.Tests/FhirStoreTestsR5.cs b/src/fhir-candle.Tests/FhirStoreTestsR5.cs index 7a7c579..2074df0 100644 --- a/src/fhir-candle.Tests/FhirStoreTestsR5.cs +++ b/src/fhir-candle.Tests/FhirStoreTestsR5.cs @@ -7,6 +7,7 @@ using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using fhir.candle.Tests.Extensions; using fhir.candle.Tests.Models; using FluentAssertions; @@ -29,7 +30,7 @@ public class FhirStoreTestsR5: IDisposable /// (Immutable) The configuration. private static readonly TenantConfiguration _config = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R5, + FhirVersion = FhirReleases.FhirSequenceCodes.R5, ControllerName = "r5", BaseUrl = "http://localhost/fhir/r5", AllowExistingId = true, @@ -739,4 +740,4 @@ private static HttpStatusCode DoUpdate( return response.StatusCode ?? HttpStatusCode.InternalServerError; } -} \ No newline at end of file +} diff --git a/src/fhir-candle.Tests/R4BTests.cs b/src/fhir-candle.Tests/R4BTests.cs index c10d67e..6de8d88 100644 --- a/src/fhir-candle.Tests/R4BTests.cs +++ b/src/fhir-candle.Tests/R4BTests.cs @@ -8,6 +8,7 @@ using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using fhir.candle.Tests.Models; using FluentAssertions; using System.Text.Json; @@ -60,7 +61,7 @@ public R4BTests() _config = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4B, + FhirVersion = FhirReleases.FhirSequenceCodes.R4B, ControllerName = "r4b", BaseUrl = "http://localhost/fhir/r4b", LoadDirectory = loadDirectory, @@ -353,7 +354,7 @@ public R4BTestSubscriptions(R4BTests fixture, ITestOutputHelper testOutputHelper [FileData("data/r4b/SubscriptionTopic-encounter-complete-qualified.json")] public void ParseTopic(string json) { - HttpStatusCode sc = candleR4B.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4B.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -383,7 +384,7 @@ public void ParseTopic(string json) [FileData("data/r4b/Subscription-encounter-complete.json")] public void ParseSubscription(string json) { - HttpStatusCode sc = candleR4B.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4B.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -415,7 +416,7 @@ public void ParseSubscription(string json) [FileData("data/r4b/Bundle-notification-handshake.json")] public void ParseHandshake(string json) { - HttpStatusCode sc = candleR4B.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4B.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -585,4 +586,4 @@ public void TestSubEncounterNoFilters( subscription.ClearEvents(); } } -} \ No newline at end of file +} diff --git a/src/fhir-candle.Tests/R4Tests.cs b/src/fhir-candle.Tests/R4Tests.cs index 77e6fa1..5029356 100644 --- a/src/fhir-candle.Tests/R4Tests.cs +++ b/src/fhir-candle.Tests/R4Tests.cs @@ -8,6 +8,7 @@ using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using fhir.candle.Tests.Models; using FluentAssertions; using System.Text.Json; @@ -67,7 +68,7 @@ public R4Tests() _config = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4, + FhirVersion = FhirReleases.FhirSequenceCodes.R4, ControllerName = "r4", BaseUrl = "http://localhost/fhir/r4", LoadDirectory = loadDirectory, @@ -416,7 +417,7 @@ private static string ChangeId(string json, string id) throw new ArgumentNullException(nameof(id)); } - HttpStatusCode sc = candleR4.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -434,7 +435,7 @@ private static string ChangeId(string json, string id) r.Id = id; - return candleR4.FhirCandle.Serialization.Utils.SerializeFhir(r, "application/fhir+json", false); + return candleR4.FhirCandle.Serialization.SerializationUtils.SerializeFhir(r, "application/fhir+json", false); } /// Conditional create no match. @@ -472,7 +473,7 @@ public void ConditionalCreateNoMatch(string resourceType, string json) response.LastModified.Should().NotBeNullOrEmpty(); response.Location.Should().Contain($"{resourceType}/{id}"); - HttpStatusCode sc = candleR4.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( response.SerializedResource, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -537,7 +538,7 @@ public void ConditionalCreateOneMatch(string resourceType, string json) response.LastModified.Should().NotBeNullOrEmpty(); response.Location.Should().Contain($"{resourceType}/{id}"); - HttpStatusCode sc = candleR4.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( response.SerializedResource, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -585,7 +586,7 @@ public void ConditionalCreateMultipleMatches(string resourceType, string json) response.LastModified.Should().NotBeNullOrEmpty(); response.Location.Should().Contain($"{resourceType}/{id1}"); - HttpStatusCode sc = candleR4.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( response.SerializedResource, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -666,7 +667,7 @@ public R4TestSubscriptions(R4Tests fixture, ITestOutputHelper testOutputHelper) [FileData("data/r4/Basic-topic-encounter-complete.json")] public void ParseTopic(string json) { - HttpStatusCode sc = candleR4.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -697,7 +698,7 @@ public void ParseTopic(string json) [FileData("data/r4/Subscription-encounter-complete.json")] public void ParseSubscription(string json) { - HttpStatusCode sc = candleR4.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -729,7 +730,7 @@ public void ParseSubscription(string json) [FileData("data/r4/Bundle-notification-handshake.json")] public void ParseHandshake(string json) { - HttpStatusCode sc = candleR4.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR4.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -953,4 +954,4 @@ public void TestSubEncounterNoFilters( // r.Should().NotBeNull(); // r!.TypeName.Should().Be("Bundle"); // } -// } \ No newline at end of file +// } diff --git a/src/fhir-candle.Tests/R5Tests.cs b/src/fhir-candle.Tests/R5Tests.cs index a532ea5..c47c236 100644 --- a/src/fhir-candle.Tests/R5Tests.cs +++ b/src/fhir-candle.Tests/R5Tests.cs @@ -8,6 +8,7 @@ using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using fhir.candle.Tests.Models; using FluentAssertions; using System.Text.Json; @@ -71,7 +72,7 @@ public R5Tests() _config = new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R5, + FhirVersion = FhirReleases.FhirSequenceCodes.R5, ControllerName = "r5", BaseUrl = "http://localhost/fhir/r5", LoadDirectory = loadDirectory, @@ -436,7 +437,7 @@ public R5TestSubscriptions(R5Tests fixture, ITestOutputHelper testOutputHelper) [FileData("data/r5/SubscriptionTopic-encounter-complete.json")] public void ParseTopic(string json) { - HttpStatusCode sc = candleR5.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR5.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -463,7 +464,7 @@ public void ParseTopic(string json) [FileData("data/r5/Subscription-encounter-complete.json")] public void ParseSubscription(string json) { - HttpStatusCode sc = candleR5.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR5.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, @@ -494,7 +495,7 @@ public void ParseSubscription(string json) [FileData("data/r5/Bundle-notification-handshake.json")] public void ParseHandshake(string json) { - HttpStatusCode sc = candleR5.FhirCandle.Serialization.Utils.TryDeserializeFhir( + HttpStatusCode sc = candleR5.FhirCandle.Serialization.SerializationUtils.TryDeserializeFhir( json, "application/fhir+json", out Hl7.Fhir.Model.Resource? r, diff --git a/src/fhir-candle.Tests/fhir-candle.Tests.csproj b/src/fhir-candle.Tests/fhir-candle.Tests.csproj index b767fea..c1d7135 100644 --- a/src/fhir-candle.Tests/fhir-candle.Tests.csproj +++ b/src/fhir-candle.Tests/fhir-candle.Tests.csproj @@ -18,9 +18,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + diff --git a/src/fhir-candle/Layout/MainLayout.razor b/src/fhir-candle/Layout/MainLayout.razor index d804c18..70b7e74 100644 --- a/src/fhir-candle/Layout/MainLayout.razor +++ b/src/fhir-candle/Layout/MainLayout.razor @@ -1,4 +1,5 @@ -@using Microsoft.FluentUI.AspNetCore.Components; +@using FhirCandle.Configuration +@using Microsoft.FluentUI.AspNetCore.Components; @using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; @namespace fhir.candle.Layout @@ -8,7 +9,7 @@ @inject ProtectedLocalStorage BrowserStore @inject IFhirStoreManager StoreManager @inject ISmartAuthManager AuthManager -@inject ServerConfiguration ServerConfig +@inject CandleConfig ServerConfig @inject IJSRuntime JS @inject IDialogService DialogService diff --git a/src/fhir-candle/Models/RegistryPackageManifest.cs b/src/fhir-candle/Models/RegistryPackageManifest.cs index fee51ef..3841a4c 100644 --- a/src/fhir-candle/Models/RegistryPackageManifest.cs +++ b/src/fhir-candle/Models/RegistryPackageManifest.cs @@ -3,7 +3,9 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // +using fhir.candle._ForPackages; using fhir.candle.Services; +using FhirCandle.Utils; using System.Text.Json; using System.Text.Json.Serialization; @@ -60,14 +62,14 @@ public class RegistryPackageManifest foreach (string key in manifest.Versions.Keys) { - FhirPackageService.FhirSequenceEnum sequence = FhirPackageService.SequenceForVersion(key); + FhirReleases.FhirSequenceCodes sequence = FhirReleases.FhirVersionToSequence(key); bool remove = false; string name = manifest.Versions[key].Name; if (string.IsNullOrEmpty(manifest.Versions[key].PackageKind) || (manifest.Versions[key].PackageKind == "??")) { - if (FhirPackageService.PackageIsFhirCore(name)) + if (VersionExtensions.PackageIsFhirCore(name)) { manifest.Versions[key].PackageKind = "Core"; } @@ -81,9 +83,9 @@ public class RegistryPackageManifest (manifest.Versions[key].FhirVersion == "??")) { if (manifest.Versions[key].PackageKind.Equals("core", StringComparison.OrdinalIgnoreCase) && - (sequence != FhirPackageService.FhirSequenceEnum.Unknown)) + (sequence != FhirReleases.FhirSequenceCodes.Unknown)) { - manifest.Versions[key].FhirVersion = FhirPackageService.LiteralForSequence(sequence); + manifest.Versions[key].FhirVersion = sequence.ToLiteral(); } else { @@ -93,7 +95,7 @@ public class RegistryPackageManifest if (manifest.Versions[key].PackageKind.Equals("core", StringComparison.OrdinalIgnoreCase)) { - manifest.Versions[key].FhirVersion = FhirPackageService.LiteralForSequence(sequence); + manifest.Versions[key].FhirVersion = sequence.ToLiteral(); } if (remove) diff --git a/src/fhir-candle/Models/ServerConfiguration.cs b/src/fhir-candle/Models/ServerConfiguration.cs deleted file mode 100644 index 6094cd8..0000000 --- a/src/fhir-candle/Models/ServerConfiguration.cs +++ /dev/null @@ -1,94 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// - -namespace fhir.candle.Models; - -/// A server configuration. -public class ServerConfiguration -{ - /// Gets or sets URL of the public. - public string PublicUrl { get; set; } = string.Empty; - - /// Gets or sets the listen port. - public required int ListenPort { get; set; } - - /// Gets or sets a value indicating whether the browser should be opened at launch. - public bool OpenBrowser { get; set; } = false; - - /// Gets or sets the number of maximum resources. - public int MaxResourceCount { get; set; } = 0; - - /// Gets or sets the UI Mode. - public bool? DisableUi { get; set; } = null; - - /// Gets or sets the pathname of the FHIR cache directory. - public string? FhirCacheDirectory { get; set; } = null; - - /// Gets or sets the published packages to load. - public List PublishedPackages { get; set; } = new(); - - /// Gets or sets the list of packages to load from the CI build server. - public List CiPackages { get; set; } = new(); - - /// Gets or sets the load package examples. - public bool? LoadPackageExamples { get; set; } = null; - - /// Gets or sets the reference implementation package. - public string? ReferenceImplementation { get; set; } = null; - - /// Gets or sets the pathname of the source directory. - public string? SourceDirectory { get; set; } = null; - - /// Gets or sets the protect loaded content. - public bool ProtectLoadedContent { get; set; } = false; - - /// Gets or sets the FHIR R4 tenants. - public List TenantsR4 { get; set; } = new(); - - /// Gets or sets the FHIR R4B tenants. - public List TenantsR4B { get; set; } = new(); - - /// Gets or sets the FHIR R5 tenants. - public List TenantsR5 { get; set; } = new(); - - /// Gets or sets the tenants that REQUIRE SMART launch. - public List SmartRequiredTenants { get; set; } = new(); - - /// Gets or sets the tenants that allow SMART launch. - public List SmartOptionalTenants { get; set; } = new(); - - /// Gets or sets a value indicating whether we allow existing identifier. - public bool AllowExistingId { get; set; } = true; - - /// Gets or sets a value indicating whether we allow create as update. - public bool AllowCreateAsUpdate { get; set; } = true; - - /// Gets or sets the max allowed subscription expiration minutes. - public int MaxSubscriptionExpirationMinutes { get; set; } = 30; - - /// Gets or sets the zulip email. - public string ZulipEmail { get; set; } = string.Empty; - - /// Gets or sets the zulip key. - public string ZulipKey { get; set; } = string.Empty; - - /// Gets or sets URL of the zulip site. - public string ZulipUrl { get; set; } = string.Empty; - - /// Gets or sets the SMTP host. - public string SmtpHost { get; set; } = string.Empty; - - /// Gets or sets the SMTP port. - public int SmtpPort { get; set; } = 587; - - /// Gets or sets the SMTP user. - public string SmtpUser { get; set; } = string.Empty; - - /// Gets or sets the SMTP password. - public string SmtpPassword { get; set; } = string.Empty; - - /// Gets or sets the FHIRPath Lab URL. - public string FhirPathLabUrl { get; set; } = string.Empty; -} diff --git a/src/fhir-candle/Pages/Index.razor b/src/fhir-candle/Pages/Index.razor index b143608..60b6a4a 100644 --- a/src/fhir-candle/Pages/Index.razor +++ b/src/fhir-candle/Pages/Index.razor @@ -1,4 +1,5 @@ -@using Microsoft.FluentUI.AspNetCore.Components; +@using FhirCandle.Configuration +@using Microsoft.FluentUI.AspNetCore.Components; @page "/" @@ -7,7 +8,7 @@ @inject NavigationManager NavigationManager @inject IFhirStoreManager StoreManager -@inject ServerConfiguration ServerConfig +@inject CandleConfig ServerConfig @inject IJSRuntime JS @implements IDisposable @@ -40,7 +41,7 @@ @switch (StoreManager.First().Value.Config.FhirVersion) { - case TenantConfiguration.SupportedFhirVersions.R4: + case FhirReleases.FhirSequenceCodes.R4: break; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: break; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: @switch (kvp.Value.Config.FhirVersion) { - case TenantConfiguration.SupportedFhirVersions.R4: + case FhirReleases.FhirSequenceCodes.R4: break; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: break; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: @switch (_store.Config.FhirVersion) { - case TenantConfiguration.SupportedFhirVersions.R4: + case FhirReleases.FhirSequenceCodes.R4: break; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: break; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: _store.Config.FhirVersion switch { - TenantConfiguration.SupportedFhirVersions.R4 => FhirCandle.Ui.R4.Subscriptions.TourUtils.EncounterJson, - TenantConfiguration.SupportedFhirVersions.R4B => FhirCandle.Ui.R4B.Subscriptions.TourUtils.EncounterJson, - TenantConfiguration.SupportedFhirVersions.R5 => FhirCandle.Ui.R5.Subscriptions.TourUtils.EncounterJson, + FhirReleases.FhirSequenceCodes.R4 => FhirCandle.Ui.R4.Subscriptions.TourUtils.EncounterJson, + FhirReleases.FhirSequenceCodes.R4B => FhirCandle.Ui.R4B.Subscriptions.TourUtils.EncounterJson, + FhirReleases.FhirSequenceCodes.R5 => FhirCandle.Ui.R5.Subscriptions.TourUtils.EncounterJson, _ => string.Empty, }; } @@ -489,7 +489,7 @@ subscription.Endpoint = _store.Config.BaseUrl + "/$subscription-hook"; - encounterStatus = _store.Config.FhirVersion >= TenantConfiguration.SupportedFhirVersions.R5 + encounterStatus = _store.Config.FhirVersion >= FhirReleases.FhirSequenceCodes.R5 ? "completed" : "finished"; } @@ -615,4 +615,4 @@ _store.OnSubscriptionsChanged -= Store_OnSubscriptionsChanged; } } -} \ No newline at end of file +} diff --git a/src/fhir-candle/Pages/Store/ResourceViewer.razor b/src/fhir-candle/Pages/Store/ResourceViewer.razor index 253ea47..b14b71e 100644 --- a/src/fhir-candle/Pages/Store/ResourceViewer.razor +++ b/src/fhir-candle/Pages/Store/ResourceViewer.razor @@ -1,4 +1,5 @@ -@using Microsoft.FluentUI.AspNetCore.Components; +@using FhirCandle.Configuration +@using Microsoft.FluentUI.AspNetCore.Components; @page "/store/resource-viewer" @@ -6,7 +7,7 @@ @using BlazorMonaco @using BlazorMonaco.Editor -@inject ServerConfiguration ServerConfig +@inject CandleConfig ServerConfig @inject NavigationManager NavigationManager @inject IJSRuntime JS @inject IFhirStoreManager StoreManager @@ -30,7 +31,7 @@ @switch (_store.Config.FhirVersion) { - case TenantConfiguration.SupportedFhirVersions.R4: + case FhirReleases.FhirSequenceCodes.R4: break; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: break; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: break; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: break; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: +using SCL = System.CommandLine; // this is present to disambiguate Option from System.CommandLine and Microsoft.FluentUI.AspNetCore.Components using System; -using System.CommandLine; using System.CommandLine.Binding; using System.CommandLine.Invocation; using System.Diagnostics; @@ -17,6 +17,7 @@ using FhirCandle.Extensions; using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -28,18 +29,25 @@ using Microsoft.Extensions.Hosting; using Microsoft.FluentUI.AspNetCore.Components; using fhir.candle; +using System.CommandLine.Builder; +using System.CommandLine.Parsing; +using System.Text; +using System.CommandLine; +using FhirCandle.Configuration; +using System.Reflection; +using static Org.BouncyCastle.Math.EC.ECCurve; +using BlazorMonaco.Languages; namespace fhir.candle; /// A program. public static partial class Program { + private static List _optsWithEnums = []; + [GeneratedRegex("(http[s]*:\\/\\/.*(:\\d+)*)")] private static partial Regex InputUrlFormatRegex(); - /// (Immutable) The default listen port. - private const int _defaultListenPort = 5826; - /// (Immutable) The default subscription expiration. private static readonly int DefaultSubscriptionExpirationMinutes = 30; @@ -48,235 +56,208 @@ public static partial class Program public static async Task Main(string[] args) { // setup our configuration (command line > environment > appsettings.json) - IConfiguration configuration = new ConfigurationBuilder() + IConfiguration envConfig = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables() .Build(); - System.CommandLine.Option optPublicUrl = new( - aliases: new[] { "--url", "-u" }, - getDefaultValue: () => configuration.GetValue("Public_Url", string.Empty) ?? string.Empty, - "Public URL for the server"); - - System.CommandLine.Option optListenPort = new( - aliases: new[] { "--port", "-p" }, - getDefaultValue: () => configuration.GetValue("Listen_Port", _defaultListenPort) ?? _defaultListenPort, - "Listen port for the server"); - - System.CommandLine.Option optOpenBrowser = new( - aliases: new[] { "--open-browser", "-o" }, - getDefaultValue: () => configuration.GetValue("Open_Browser", false), - "Open a browser once the server starts."); - - System.CommandLine.Option optMaxResourceCount = new( - aliases: new[] { "--max-resources", "-m" }, - getDefaultValue: () => configuration.GetValue("Max_Resources", null), - "Maximum number of resources allowed per tenant."); - - System.CommandLine.Option optDisableUi = new( - name: "--disable-ui", - getDefaultValue: () => configuration.GetValue("Disable_Ui", null), - "If the server should run headless."); - - System.CommandLine.Option optPackageCache = new( - name: "--fhir-package-cache", - getDefaultValue: () => configuration.GetValue("Fhir_Cache", null), - "Location of the FHIR package cache, for use with registries and IG packages. Use empty quoted string to disable cache."); - - System.CommandLine.Option> optPublishedPackages = new( - name: "--load-package", - getDefaultValue: () => configuration.GetValue>("Load_Packages", new List())!, - "Published packages to load. Specifying package name alone loads highest version."); - - System.CommandLine.Option> optCiPackages = new( - name: "--ci-package", - getDefaultValue: () => configuration.GetValue>("Ci_Packages", new List())!, - "Continuous Integration (CI) packages to load. You may specify either just the branch name or a full URL."); - - System.CommandLine.Option optLoadPackageExamples = new( - name: "--load-examples", - getDefaultValue: () => configuration.GetValue("Load_Examples", null), - "If package loading should include example instances."); - - System.CommandLine.Option optPackageReferenceImplementation = new( - name: "--reference-implementation", - getDefaultValue: () => configuration.GetValue("Reference_Implementation", null), - "If running as the Reference Implementation, the package directive or literal."); - - System.CommandLine.Option optSourceDirectory = new( - name: "--fhir-source", - getDefaultValue: () => null, - "FHIR Contents to load, either in this directory or by subdirectories named per tenant."); - - System.CommandLine.Option optProtectLoadedContent = new( - name: "--protect-source", - getDefaultValue: () => null, - "If any loaded FHIR contents cannot be altered."); - - System.CommandLine.Option> optTenantsR4 = new( - name: "--r4", - getDefaultValue: () => new(), - "FHIR R4 Tenants to provide"); - - System.CommandLine.Option> optTenantsR4B = new( - name: "--r4b", - getDefaultValue: () => new(), - "FHIR R4B Tenants to provide"); - - System.CommandLine.Option> optTenantsR5 = new( - name: "--r5", - getDefaultValue: () => new(), - "FHIR R5 Tenants to provide"); - - System.CommandLine.Option> optTenantsSmartRequired = new( - name: "--smart-required", - getDefaultValue: () => new(), - "FHIR Tenants that require SMART auth"); - - System.CommandLine.Option> optTenantsSmartOptional = new( - name: "--smart-optional", - getDefaultValue: () => new(), - "FHIR Tenants that allow (but do not require) SMART auth"); - - System.CommandLine.Option optCreateExistingId = new( - name: "--create-existing-id", - getDefaultValue: () => configuration.GetValue("Create_Existing_Id", true), - "Allow Create interactions (POST) to specify an ID."); - - System.CommandLine.Option optCreateAsUpdate = new( - name: "--create-as-update", - getDefaultValue: () => configuration.GetValue("Create_As_Update", true), - "Allow Update interactions (PUT) to create new resources."); - - System.CommandLine.Option optMaxSubscriptionExpirationMinutes = new( - name: "--max-subscription-minutes", - getDefaultValue: () => configuration.GetValue("Max_Subscription_Minutes", null), - "Maximum number of minutes a subscription is allowed to expire in."); - - System.CommandLine.Option optZulipEmail = new( - name: "--zulip-email", - getDefaultValue: () => configuration.GetValue("Zulip_Email", string.Empty) ?? string.Empty, - "Zulip bot email address"); - - System.CommandLine.Option optZulipKey = new( - name: "--zulip-key", - getDefaultValue: () => configuration.GetValue("Zulip_Key", string.Empty) ?? string.Empty, - "Zulip bot API key"); - - System.CommandLine.Option optZulipUrl = new( - name: "--zulip-url", - getDefaultValue: () => configuration.GetValue("Zulip_Url", string.Empty) ?? string.Empty, - "Zulip bot email address"); - - System.CommandLine.Option optSmtpHost = new( - name: "--smtp-host", - getDefaultValue: () => configuration.GetValue("SMTP_Host", string.Empty) ?? string.Empty, - "SMTP Host name/address"); - - System.CommandLine.Option optSmtpPort = new( - name: "--smtp-port", - getDefaultValue: () => configuration.GetValue("SMTP_Port", null), - "SMTP Port"); - - System.CommandLine.Option optSmtpUser = new( - name: "--smtp-user", - getDefaultValue: () => configuration.GetValue("SMTP_User", string.Empty) ?? string.Empty, - "SMTP Username"); - - System.CommandLine.Option optSmtpPassword = new( - name: "--smtp-password", - getDefaultValue: () => configuration.GetValue("SMTP_Password", string.Empty) ?? string.Empty, - "SMTP Password"); - - System.CommandLine.Option optFhirPathLabUrl = new( - name: "--fhirpath-lab-url", - getDefaultValue: () => configuration.GetValue("FHIRPath_Lab_Url", string.Empty) ?? string.Empty, - "FHIRPath Lab URL"); - - RootCommand rootCommand = new() + //// in order to process help correctly we have to build a parser independent of the command + //SCL.Parsing.Parser parser = BuildParser(envConfig); + + //// attempt a parse + //SCL.Parsing.ParseResult pr = parser.Parse(args); + + SCL.RootCommand rootCommand = new("A lightweight in-memory FHIR server, for when a small FHIR will do."); + foreach (SCL.Option option in BuildCliOptions(typeof(CandleConfig), envConfig: envConfig)) { - optPublicUrl, - optListenPort, - optOpenBrowser, - optMaxResourceCount, - optDisableUi, - optPackageCache, - optPublishedPackages, - optCiPackages, - optLoadPackageExamples, - optPackageReferenceImplementation, - optSourceDirectory, - optProtectLoadedContent, - optTenantsR4, - optTenantsR4B, - optTenantsR5, - optTenantsSmartRequired, - optTenantsSmartOptional, - optCreateExistingId, - optCreateAsUpdate, - optMaxSubscriptionExpirationMinutes, - optZulipEmail, - optZulipKey, - optZulipUrl, - optSmtpHost, - optSmtpPort, - optSmtpUser, - optSmtpPassword, - optFhirPathLabUrl, - }; - - rootCommand.Description = "A lightweight in-memory FHIR server, for when a small FHIR will do."; - - rootCommand.SetHandler(async (context) => + // note that 'global' here is just recursive DOWNWARD + rootCommand.AddGlobalOption(option); + } + rootCommand.SetHandler(async (context) => await RunServer(context.ParseResult, context.GetCancellationToken())); + + return await rootCommand.InvokeAsync(args); + + //// check for invalid arguments, help, a generate command with no subcommand, or a generate with no packages to trigger the nicely formatted help + //if (pr.UnmatchedTokens.Any() || + // !pr.Tokens.Any() || + // (!pr.CommandResult.Command.Parents?.Any() ?? false) || + // pr.Tokens.Any(t => t.Value.Equals("-?", StringComparison.Ordinal)) || + // pr.Tokens.Any(t => t.Value.Equals("-h", StringComparison.Ordinal)) || + // pr.Tokens.Any(t => t.Value.Equals("--help", StringComparison.Ordinal)) || + // pr.Tokens.Any(t => t.Value.Equals("help", StringComparison.Ordinal))) + + //{ + // return await parser.InvokeAsync(args); + //} + + + //return await RunServer(pr); + + + + //// in order to process help correctly we have to build a parser independent of the command + //SCL.Parsing.Parser parser = BuildParser(envConfig); + + //// attempt a parse + //SCL.Parsing.ParseResult pr = parser.Parse(args); + + //return await parser.InvokeAsync(args); + + ////System.CommandLine.Parsing.Parser clParser = new System.CommandLine.Builder.CommandLineBuilder(_rootCommand).Build(); + + //return await rootCommand.InvokeAsync(args); + } + + + private static SCL.Parsing.Parser BuildParser(IConfiguration envConfig) + { + SCL.RootCommand command = new("A lightweight in-memory FHIR server, for when a small FHIR will do."); + foreach (SCL.Option option in BuildCliOptions(typeof(CandleConfig), envConfig: envConfig)) { - ServerConfiguration config = new() + // note that 'global' here is just recursive DOWNWARD + command.AddGlobalOption(option); + TrackIfEnum(option); + } + + //command.SetHandler(async (context) => await RunServer(context.ParseResult, context.GetCancellationToken())); + + SCL.Parsing.Parser parser = new CommandLineBuilder(command) + .UseExceptionHandler((ex, ctx) => + { + Console.WriteLine($"Error: {ex.Message}"); + ctx.ExitCode = 1; + }) + .UseDefaults() + .UseHelp(ctx => { - PublicUrl = context.ParseResult.GetValueForOption(optPublicUrl) ?? string.Empty, - ListenPort = context.ParseResult.GetValueForOption(optListenPort) ?? _defaultListenPort, - OpenBrowser = context.ParseResult.GetValueForOption(optOpenBrowser) ?? false, - MaxResourceCount = context.ParseResult.GetValueForOption(optMaxResourceCount) ?? 0, - DisableUi = context.ParseResult.GetValueForOption(optDisableUi) ?? false, - FhirCacheDirectory = context.ParseResult.GetValueForOption(optPackageCache), - PublishedPackages = context.ParseResult.GetValueForOption(optPublishedPackages) ?? new(), - CiPackages = context.ParseResult.GetValueForOption(optCiPackages) ?? new(), - LoadPackageExamples = context.ParseResult.GetValueForOption(optLoadPackageExamples) ?? false, - ReferenceImplementation = context.ParseResult.GetValueForOption(optPackageReferenceImplementation) ?? string.Empty, - SourceDirectory = context.ParseResult.GetValueForOption(optSourceDirectory), - ProtectLoadedContent = context.ParseResult.GetValueForOption(optProtectLoadedContent) ?? false, - TenantsR4 = context.ParseResult.GetValueForOption(optTenantsR4) ?? new(), - TenantsR4B = context.ParseResult.GetValueForOption(optTenantsR4B) ?? new(), - TenantsR5 = context.ParseResult.GetValueForOption(optTenantsR5) ?? new(), - SmartRequiredTenants = context.ParseResult.GetValueForOption(optTenantsSmartRequired) ?? new(), - SmartOptionalTenants = context.ParseResult.GetValueForOption(optTenantsSmartOptional) ?? new(), - AllowExistingId = context.ParseResult.GetValueForOption(optCreateExistingId) ?? true, - AllowCreateAsUpdate = context.ParseResult.GetValueForOption(optCreateAsUpdate) ?? true, - MaxSubscriptionExpirationMinutes = context.ParseResult.GetValueForOption(optMaxSubscriptionExpirationMinutes) ?? DefaultSubscriptionExpirationMinutes, - ZulipEmail = context.ParseResult.GetValueForOption(optZulipEmail) ?? string.Empty, - ZulipKey = context.ParseResult.GetValueForOption(optZulipKey) ?? string.Empty, - ZulipUrl = context.ParseResult.GetValueForOption(optZulipUrl) ?? string.Empty, - SmtpHost = context.ParseResult.GetValueForOption(optSmtpHost) ?? string.Empty, - SmtpPort = context.ParseResult.GetValueForOption(optSmtpPort) ?? 0, - SmtpUser = context.ParseResult.GetValueForOption(optSmtpUser) ?? string.Empty, - SmtpPassword = context.ParseResult.GetValueForOption(optSmtpPassword) ?? string.Empty, - FhirPathLabUrl = context.ParseResult.GetValueForOption(optFhirPathLabUrl) ?? string.Empty, - }; - - await RunServer(config, context.GetCancellationToken()); - }); - - //System.CommandLine.Parsing.Parser clParser = new System.CommandLine.Builder.CommandLineBuilder(_rootCommand).Build(); + foreach (SCL.Option option in _optsWithEnums) + { + StringBuilder sb = new(); + if (option.Aliases.Count != 0) + { + sb.AppendLine(string.Join(", ", option.Aliases)); + } + else + { + sb.AppendLine(option.Name); + } - return await rootCommand.InvokeAsync(args); + Type et = option.ValueType; + + if (option.ValueType.IsGenericType) + { + et = option.ValueType.GenericTypeArguments.First(); + } + + if (option.ValueType.IsArray) + { + et = option.ValueType.GetElementType()!; + } + + foreach (MemberInfo mem in et.GetMembers(BindingFlags.Public | BindingFlags.Static).Where(m => m.DeclaringType == et).OrderBy(m => m.Name)) + { + sb.AppendLine($" opt: {mem.Name}"); + } + + ctx.HelpBuilder.CustomizeSymbol( + option, + firstColumnText: (ctx) => sb.ToString()); + //secondColumnText: (ctx) => option.Description); + } + }) + .Build(); + + return parser; + + void TrackIfEnum(SCL.Option option) + { + if (option.ValueType.IsEnum) + { + _optsWithEnums.Add(option); + return; + } + + if (option.ValueType.IsGenericType) + { + if (option.ValueType.GenericTypeArguments.First().IsEnum) + { + _optsWithEnums.Add(option); + } + + return; + } + + if (option.ValueType.IsArray) + { + if (option.ValueType.GetElementType()!.IsEnum) + { + _optsWithEnums.Add(option); + } + + return; + } + } + } + + private static IEnumerable BuildCliOptions( + Type forType, + Type? excludeFromType = null, + IConfiguration? envConfig = null) + { + HashSet inheritedPropNames = []; + + if (excludeFromType != null) + { + PropertyInfo[] exProps = excludeFromType.GetProperties(); + foreach (PropertyInfo exProp in exProps) + { + inheritedPropNames.Add(exProp.Name); + } + } + + object? configDefault = null; + if (forType.IsAbstract) + { + throw new Exception($"Config type cannot be abstract! {forType.Name}"); + } + + configDefault = Activator.CreateInstance(forType); + + if (configDefault is not CandleConfig config) + { + throw new Exception("Config type must be CandleConfig"); + } + + foreach (ConfigurationOption opt in config.GetOptions()) + { + // need to configure default values + if ((envConfig != null) && + (!string.IsNullOrEmpty(opt.EnvVarName))) + { + opt.CliOption.SetDefaultValueFactory(() => envConfig.GetSection(opt.EnvVarName).GetChildren().Select(c => c.Value)); + } + else + { + opt.CliOption.SetDefaultValue(opt.DefaultValue); + } + + yield return opt.CliOption; + } } /// Executes the server operation. /// The configuration. /// A token that allows processing to be cancelled. /// An asynchronous result that yields an int. - public static async Task RunServer(ServerConfiguration config, CancellationToken cancellationToken) + public static async Task RunServer(SCL.Parsing.ParseResult pr, CancellationToken? cancellationToken = null) { try { + CandleConfig config = new(); + + // parse the arguments into the configuration object + config.Parse(pr); + if (string.IsNullOrEmpty(config.PublicUrl)) { config.PublicUrl = $"http://localhost:{config.ListenPort}"; @@ -291,7 +272,7 @@ public static async Task RunServer(ServerConfiguration config, Cancellation config.PublicUrl = config.PublicUrl.Substring(0, config.PublicUrl.Length - 1); } - if (config.FhirPathLabUrl.EndsWith('/')) + if (config.FhirPathLabUrl?.EndsWith('/') ?? false) { config.FhirPathLabUrl = config.FhirPathLabUrl.Substring(0, config.FhirPathLabUrl.Length - 1); } @@ -301,16 +282,9 @@ public static async Task RunServer(ServerConfiguration config, Cancellation (!config.TenantsR4B.Any()) && (!config.TenantsR5.Any())) { - config.TenantsR4.Add("r4"); - config.TenantsR4B.Add("r4b"); - config.TenantsR5.Add("r5"); - } - - if (config.FhirCacheDirectory == null) - { - config.FhirCacheDirectory = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".fhir"); + config.TenantsR4 = ["r4"]; + config.TenantsR4B = ["r4b"]; + config.TenantsR5 = ["r5"]; } Dictionary tenants = BuildTenantConfigurations(config); @@ -437,8 +411,10 @@ public static async Task RunServer(ServerConfiguration config, Cancellation //await app.RunAsync(cancellationToken); _ = app.StartAsync(); + cancellationToken ??= new CancellationToken(); + AfterServerStart(app, config); - await app.WaitForShutdownAsync(cancellationToken); + await app.WaitForShutdownAsync((CancellationToken)cancellationToken); return 0; } @@ -457,7 +433,7 @@ public static async Task RunServer(ServerConfiguration config, Cancellation /// After server start. /// The application. /// The configuration. - private static void AfterServerStart(WebApplication app, ServerConfiguration config) + private static void AfterServerStart(WebApplication app, CandleConfig config) { Console.WriteLine("Press CTRL+C to exit"); @@ -502,7 +478,7 @@ private static void LaunchBrowser(string url) /// An enumerator that allows foreach to be used to process build tenant configurations in this /// collection. /// - private static Dictionary BuildTenantConfigurations(ServerConfiguration config) + private static Dictionary BuildTenantConfigurations(CandleConfig config) { HashSet smartRequired = config.SmartRequiredTenants.ToHashSet(); HashSet smartOptional = config.SmartOptionalTenants.ToHashSet(); @@ -513,7 +489,7 @@ private static Dictionary BuildTenantConfigurations { tenants.Add(tenant, new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4, + FhirVersion = FhirReleases.FhirSequenceCodes.R4, ControllerName = tenant, BaseUrl = config.PublicUrl + "/fhir/" + tenant, ProtectLoadedContent = config.ProtectLoadedContent, @@ -530,7 +506,7 @@ private static Dictionary BuildTenantConfigurations { tenants.Add(tenant, new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R4B, + FhirVersion = FhirReleases.FhirSequenceCodes.R4B, ControllerName = tenant, BaseUrl = config.PublicUrl + "/fhir/" + tenant, ProtectLoadedContent = config.ProtectLoadedContent, @@ -547,7 +523,7 @@ private static Dictionary BuildTenantConfigurations { tenants.Add(tenant, new() { - FhirVersion = TenantConfiguration.SupportedFhirVersions.R5, + FhirVersion = FhirReleases.FhirSequenceCodes.R5, ControllerName = tenant, BaseUrl = config.PublicUrl + "/fhir/" + tenant, ProtectLoadedContent = config.ProtectLoadedContent, diff --git a/src/fhir-candle/Properties/launchSettings.json b/src/fhir-candle/Properties/launchSettings.json index 9dbe5bc..58599b2 100644 --- a/src/fhir-candle/Properties/launchSettings.json +++ b/src/fhir-candle/Properties/launchSettings.json @@ -67,4 +67,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/fhir-candle/Services/FhirPackageService.cs b/src/fhir-candle/Services/FhirPackageService.cs index 1a8d45b..19b5654 100644 --- a/src/fhir-candle/Services/FhirPackageService.cs +++ b/src/fhir-candle/Services/FhirPackageService.cs @@ -3,11 +3,15 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // +using fhir.candle._ForPackages; using fhir.candle.Models; +using FhirCandle.Utils; +using FhirCandle.Configuration; using FhirCandle.Extensions; using FhirCandle.Models; -using IniParser; -using IniParser.Configuration; +using Firely.Fhir.Packages; +using Hl7.Fhir.Specification; +using System.Collections.Concurrent; using System.Formats.Tar; using System.IO.Compression; using System.Net; @@ -18,6 +22,21 @@ namespace fhir.candle.Services; /// A service for accessing FHIR packages. public partial class FhirPackageService : IFhirPackageService, IDisposable { + internal enum VersionHandlingTypes + { + /// Unprocessed / unknown / SemVer / ranges / etc (pass through). + Passthrough, + + /// Latest release. + Latest, + + /// Local build. + Local, + + /// CI Build. + ContinuousIntegration, + } + /// Values that represent package load state enums. public enum PackageLoadStateEnum { @@ -43,33 +62,16 @@ public enum PackageLoadStateEnum Parsed, } - /// Values that represent FHIR major releases. - public enum FhirSequenceEnum : int - { - /// An enum constant representing the unknown option. - [FhirLiteral("")] - Unknown = 0, - - /// An enum constant representing the DSTU2 option. - [FhirLiteral("DSTU2")] - DSTU2 = 2, + /// (Immutable) The cache. + private _ForPackages.DiskPackageCache? _cache = null; - /// An enum constant representing the STU3 option. - [FhirLiteral("STU3")] - STU3 = 3, + /// (Immutable) The package clients. + private readonly List _packageClients = []; - /// An enum constant representing the R4 option. - [FhirLiteral("R4")] - R4 = 4, + /// (Immutable) The FHIR CI client (build.fhir.org). + private readonly FhirCiClient _ciClient = new(); - /// An enum constant representing the R4B option. - [FhirLiteral("R4B")] - R4B = -1, - - /// An enum constant representing the R5 option. - [FhirLiteral("R5")] - R5 = 5, - } + private readonly HashSet _processedMonikers = []; /// Information about a package in the cache. public readonly record struct PackageCacheRecord( @@ -77,45 +79,30 @@ public readonly record struct PackageCacheRecord( PackageLoadStateEnum PackageState, string PackageName, string Version, - FhirSequenceEnum FhirVersion, + FhirReleases.FhirSequenceCodes FhirVersion, string DownloadDateTime, long PackageSize, FhirNpmPackageDetails Details); - /// (Immutable) The package registry uris. - private static readonly Uri[] PackageRegistryUris = + /// (Immutable) The package registry URIs. + private static readonly string[] _officialRegistryUrls = [ - new("http://packages.fhir.org/"), - new("http://packages2.fhir.org/packages/") + "https://packages.fhir.org/", + "https://packages2.fhir.org/packages/", ]; - /// (Immutable) URI of the FHIR published server. - private static readonly Uri FhirPublishedUri = new("http://hl7.org/fhir/"); - - /// (Immutable) URI of the FHIR CI server. - private static readonly Uri FhirCiUri = new("http://build.fhir.org/"); - /// The logger. private ILogger _logger; - /// True if this service is available. - private bool _hasCacheDirectory = false; - /// True if is initialized, false if not. private bool _isInitialized = false; - /// Pathname of the cache directory. - private string _cacheDirectory = string.Empty; + /// Server configuration. + private CandleConfig _config; /// Pathname of the cache package directory. private string _cachePackageDirectory = string.Empty; - /// Full pathname of the initialize file. - private string _iniFilePath = string.Empty; - - /// The HTTP client. - private HttpClient _httpClient = new(); - /// True to disposed value. private bool _disposedValue = false; @@ -131,32 +118,17 @@ public readonly record struct PackageCacheRecord( /// Occurs when On Changed. public event EventHandler? OnChanged = null; - /// Test if a name matches known core packages. - /// A RegEx. - [GeneratedRegex("^hl7.fhir.r\\d+[a-z]?.(core|expansions|examples|search|elements|corexml)$")] - private static partial Regex MatchCorePackageNames(); - - /// Test if a name matches known core packages. - private static Regex _matchCorePackageNames = MatchCorePackageNames(); - /// Initializes a new instance of the class. /// The logger. /// The server configuration. public FhirPackageService( ILogger logger, - ServerConfiguration serverConfiguration) + CandleConfig serverConfiguration) { _logger = logger; + _config = serverConfiguration; _singleton = this; - - if (string.IsNullOrEmpty(serverConfiguration.FhirCacheDirectory)) - { - _hasCacheDirectory = false; - return; - } - - _cacheDirectory = serverConfiguration.FhirCacheDirectory; - _hasCacheDirectory = true; + _cache = null; } /// Gets the current singleton. @@ -166,7 +138,7 @@ public FhirPackageService( public Dictionary PackagesByDirective => _packagesByDirective; /// Gets a value indicating whether this object is available. - public bool IsConfigured => _hasCacheDirectory; + public bool IsConfigured => _cache != null; /// Gets a value indicating whether the package service is ready. public bool IsReady => _isInitialized; @@ -182,34 +154,61 @@ public void Init() return; } - if (!_hasCacheDirectory) + if (_config.FhirCacheDirectory == string.Empty) { _logger.LogInformation("Disabling FhirPackageService, --fhir-package-cache set to empty."); return; } - _logger.LogInformation($"Initializing FhirPackageService with cache: {_cacheDirectory}"); + if (_config.FhirCacheDirectory == null) + { + _config.FhirCacheDirectory = Platform.GetFhirPackageRoot(); + } + + _logger.LogInformation($"Initializing FhirPackageService with cache: {_config.FhirCacheDirectory}"); _isInitialized = true; - _cachePackageDirectory = Path.Combine(_cacheDirectory, "packages"); - _iniFilePath = Path.Combine(_cachePackageDirectory, "packages.ini"); + if (!Directory.Exists(_config.FhirCacheDirectory)) + { + Directory.CreateDirectory(_config.FhirCacheDirectory); + Directory.CreateDirectory(Path.Combine(_config.FhirCacheDirectory, "packages")); + } - if (!Directory.Exists(_cacheDirectory)) + if (Directory.Exists(Path.Combine(_config.FhirCacheDirectory, "packages"))) + { + _cachePackageDirectory = Path.Combine(_config.FhirCacheDirectory, "packages"); + } + else { - Directory.CreateDirectory(_cacheDirectory); + _cachePackageDirectory = _config.FhirCacheDirectory; } - if (!Directory.Exists(_cachePackageDirectory)) + _cache = new(_config.FhirCacheDirectory); + + // check if we are using the official registries + if (_config.UseOfficialRegistries == true) { - Directory.CreateDirectory(_cachePackageDirectory); + foreach (string url in _officialRegistryUrls) + { + _packageClients.Add(PackageClient.Create(url)); + } } - if (!File.Exists(_iniFilePath)) + if (_config.AdditionalFhirRegistryUrls.Any()) { - CreateEmptyCacheIni(); + foreach (string url in _config.AdditionalFhirRegistryUrls) + { + _packageClients.Add(PackageClient.Create(url, npm: false)); + } } - SynchronizeCache(); + if (_config.AdditionalNpmRegistryUrls.Any()) + { + foreach (string url in _config.AdditionalNpmRegistryUrls) + { + _packageClients.Add(PackageClient.Create(url, npm: true)); + } + } } /// Triggered when the application host is ready to start the service. @@ -217,7 +216,7 @@ public void Init() /// An asynchronous result. Task IHostedService.StartAsync(CancellationToken cancellationToken) { - if (!_hasCacheDirectory) + if (_cache == null) { _logger.LogInformation("Disabling FhirPackageService, --fhir-package-cache set to empty."); return Task.CompletedTask; @@ -249,1479 +248,444 @@ Task IHostedService.StopAsync(CancellationToken cancellationToken) /// The version of the package. /// The umbrella package name that this package is part of. public record struct PackageCacheEntry( - FhirSequenceEnum fhirVersion, + FhirReleases.FhirSequenceCodes fhirVersion, string directory, string resolvedDirective, string name, string version, string umbrellaPackageName); - /// Attempts to find locally or download a given package. - /// The directive. - /// Name of the branch. - /// - /// True to enable offline mode, false to disable it. - /// True if it succeeds, false if it fails. - public bool FindOrDownload( - string directive, - string branchName, - out IEnumerable packages, - bool offlineMode = false) + public async Task> InstallPackages( + string[]? packageDirectives, + string[]? ciLiterals, + List? fhirVersions) { - if (string.IsNullOrEmpty(branchName)) - { - _logger.LogInformation($"FhirPackageService <<< attempting to load: {directive}"); - } - else + List localPackages = []; + + List directives = packageDirectives?.ToList() ?? new(); + + directives.AddRange(await ResolveCiLiterals(ciLiterals)); + + if (directives.Count == 0) { - _logger.LogInformation($"FhirPackageService <<< attempting to load branch: {branchName}"); + return []; } - if (!_hasCacheDirectory) + if (_cache == null) { - _logger.LogInformation($"FhirPackageService <<< Package service is unavailable, package will NOT be loaded!"); - packages = Enumerable.Empty(); - return false; + _logger.LogError("InstallPackages <<< Packages have been requested, but no cache has been configured!"); + return []; } - string key = $"{directive}|{branchName}"; - if (_processed.Contains(key)) + // traverse our package directives + foreach (string inputDirective in directives) { - // if we have already processed this once, force into offline for performance - offlineMode = true; - } + // TODO(ginoc): PR in to Parse FHIR-style directives, remove when added. + string directive = inputDirective.Contains('@') + ? inputDirective + : inputDirective.Replace('#', '@'); - _processed.Add(key); + PackageReference packageReference = PackageReference.Parse(directive); - string name; - string version; - List packageList = new(); + if (packageReference.Name == null) + { + _logger.LogWarning($"InstallPackages <<< Failed to parse package reference: {directive}"); + continue; + } - string directory; - FhirSequenceEnum fhirVersion; - string resolvedDirective; + bool needsInstall = true; - if (directive.Contains('#')) - { - string[] components = directive.Split('#', StringSplitOptions.TrimEntries); - name = components[0]; - version = components[1]; - } - else - { - name = directive; - version = string.Empty; - } + VersionHandlingTypes vht = GetVersionHandlingType(packageReference.Version); - if (!string.IsNullOrEmpty(branchName)) - { - branchName = GetIgBranchFromInput(branchName); - if (string.IsNullOrEmpty(version)) + // do special handling for versions if necessary + switch (vht) { - version = "dev"; - } - } + case VersionHandlingTypes.Latest: + { + // resolve the version via Firely Packages so that we have access to the actual version number + (PackageReference pr, IPackageServer? _) = await ResolveLatest(packageReference.Name); - string directiveVersion = version; + if ((pr == PackageReference.None) || (pr.Name == null)) + { + throw new Exception($"Failed to resolve latest version of {packageReference.Name} ({directive})"); + } - name = GetPackageNameFromInput(name); + packageReference = pr; + needsInstall = !await _cache.IsInstalled(packageReference); + } + break; - if (version.Equals("dev", StringComparison.OrdinalIgnoreCase)) - { - if (PackageIsFhirCore(name)) - { - if (TryDownloadCoreViaCI(name, branchName, out directory, out fhirVersion, out resolvedDirective)) - { - packageList.Add(new() + case VersionHandlingTypes.Local: + // ensure there is a local build, there is no other source { - fhirVersion = fhirVersion, - directory = directory, - resolvedDirective = resolvedDirective, - name = name, - version = resolvedDirective.Contains('#') ? resolvedDirective.Split('#')[1] : version, - umbrellaPackageName = name, - }); - - packages = packageList; - return true; - } + if (!_cache.IsInstalled(packageReference).Result) + { + throw new Exception($"Local build of {packageReference.Name} is not installed ({directive})"); + } + } + break; + + case VersionHandlingTypes.ContinuousIntegration: + // always trigger install/update for CI builds + needsInstall = true; + packageReference.Scope = FhirCiClient.FhirCiScope; + break; + + default: + needsInstall = !await _cache.IsInstalled(packageReference); + break; } - else + + // skip if we have already loaded this package + if (_processedMonikers.Contains(packageReference.Moniker)) { - if (TryDownloadGuideViaCI(branchName, out name, out directory, out fhirVersion, out resolvedDirective)) - { - packageList.Add(new() - { - fhirVersion = fhirVersion, - directory = directory, - resolvedDirective = resolvedDirective, - name = name, - version = resolvedDirective.Contains('#') ? resolvedDirective.Split('#')[1] : version, - umbrellaPackageName = name, - }); - - packages = packageList; - return true; - } + _logger.LogInformation($"Skipping already loaded dependency: {packageReference.Moniker}"); + continue; + } + _processedMonikers.Add(packageReference.Moniker); + + _logger.LogInformation($"Processing {packageReference.Moniker}..."); + + // check to see if this package needs to be installed + if (needsInstall && + (await InstallPackage(packageReference) == false)) + { + // failed to install + throw new Exception($"Failed to install package {packageReference.Moniker} as requested by {inputDirective}"); } - // resolve dev (local only) version or fail - if (string.IsNullOrEmpty(name) || - (!HasCachedVersion(name, version, out directory))) + // add this package + localPackages.Add(packageReference); + + // check to see if we have a specified FHIR versions and need to filter + if (fhirVersions?.Count > 0) { - if (string.IsNullOrEmpty(branchName)) + // read the manifest to pull the FHIR version of the package + _ForPackages.PackageManifest manifest = await _cache.ReadManifestEx(packageReference) ?? throw new Exception("Failed to load package manifest"); + + if (manifest.AnyFhirVersions?.FirstOrDefault() is not string manifestFhirVersion) { - _logger.LogInformation($"FindOrDownload <<< package: {directive}, branch: {branchName} not accessible!"); + _logger.LogInformation($"InstallPackages <<< Package {packageReference.Moniker} does not report a FHIR version!"); + continue; } - else + + // get the FHIR version of the package + FhirReleases.FhirSequenceCodes packageFhirSequence = FhirReleases.FhirVersionToSequence(manifestFhirVersion); + + // iterate over our requested FHIR versions + foreach (FhirReleases.FhirSequenceCodes fhirSequence in fhirVersions) { - _logger.LogInformation($"FindOrDownload <<< package: {directive} not accessible!"); - } + if (packageFhirSequence == fhirSequence) + { + continue; + } - packages = Enumerable.Empty(); - return false; - } + _logger.LogInformation($"InstallPackages <<< {packageReference.Moniker} ({manifestFhirVersion}) does not match requested FHIR version {fhirSequence}!"); - packageList.Add(new() - { - fhirVersion = SequenceForVersion(_packagesByDirective[directive].Details.FhirVersion), - directory = directory, - resolvedDirective = directive, - name = name, - version = directive.Contains('#') ? directive.Split('#')[1] : version, - umbrellaPackageName = name, - }); + string packageIdSuffix = packageReference.Name.Split('.')[^1]; + FhirReleases.FhirSequenceCodes packageIdSuffixCode = FhirReleases.FhirVersionToSequence(packageIdSuffix); - packages = packageList; - return true; - } + string requiredRLiteral = fhirSequence.ToRLiteral().ToLowerInvariant(); + string desiredName = (packageIdSuffixCode == FhirReleases.FhirSequenceCodes.Unknown) + ? $"{packageReference.Name}.{requiredRLiteral}" + : $"{string.Join('.', packageReference.Name.Split('.')[..^1])}.{requiredRLiteral}"; + string desiredMoniker = $"{desiredName}@{packageReference.Version}"; - if (version.Equals("current", StringComparison.OrdinalIgnoreCase)) - { - if (PackageIsFhirCore(name)) - { - if (TryDownloadCoreViaCI(name, branchName, out directory, out fhirVersion, out resolvedDirective)) - { - packageList.Add(new() + // check to see if this package exists anywhere + if (!await PackageExists(desiredName)) { - fhirVersion = fhirVersion, - directory = directory, - resolvedDirective = resolvedDirective, - name = name, - version = resolvedDirective.Contains('#') ? resolvedDirective.Split('#')[1] : version, - umbrellaPackageName = name, - }); - - packages = packageList; - return true; - } - } - else - { - if (TryDownloadGuideViaCI(branchName, out name, out directory, out fhirVersion, out resolvedDirective)) - { - packageList.Add(new() + continue; + } + + // install this package + List deps = await InstallPackages([desiredMoniker], null, fhirVersions); + + if (_processedMonikers.Contains(desiredMoniker)) { - fhirVersion = fhirVersion, - directory = directory, - resolvedDirective = resolvedDirective, - name = name, - version = resolvedDirective.Contains('#') ? resolvedDirective.Split('#')[1] : version, - umbrellaPackageName = name, - }); - - packages = packageList; - return true; + _logger.LogInformation($"Package {desiredMoniker} loaded for {packageReference.Moniker}!"); + } + else + { + _logger.LogInformation($"Could not find substitute for {packageReference.Moniker} - please specify manually if this is required!"); + } + + localPackages.AddRange(deps); } } + } - if (string.IsNullOrEmpty(branchName)) - { - _logger.LogInformation($"FindOrDownload <<< package: {directive}, branch: {branchName} not accessible!"); - } - else + return localPackages; + } + + private async ValueTask<(PackageReference, IPackageServer?)> ResolveLatest(string name) + { + ConcurrentBag<(PackageReference pr, IPackageServer server)> latestRecs = new(); + + IEnumerable tasks = _packageClients.Select(async server => + { + PackageReference pr = await server.GetLatest(name); + if (pr == PackageReference.None) { - _logger.LogInformation($"FindOrDownload <<< package: {directive} not accessible!"); + return; } - packages = Enumerable.Empty(); - return false; - } + latestRecs.Append((pr, server)); + }); - // check to see if this package already has a version trailer - string lastComponent = name.Split('.').Last(); + await System.Threading.Tasks.Task.WhenAll(tasks); - Dictionary sequencesToTest = new(); - - if (lastComponent.TryFhirEnum(out FhirSequenceEnum seq)) - { - sequencesToTest.Add(seq, string.Empty); - } - else + if (latestRecs.Count == 0) { - sequencesToTest = Enum.GetValues(typeof(FhirSequenceEnum)) - .Cast() - .ToDictionary(x => x, x => LiteralForSequence(x).ToLowerInvariant()); + return (PackageReference.None, null); } - bool foundLocally = false; + return latestRecs.OrderByDescending(v => v.pr.Version).First(); + } - // want to check for fhir-version named packages - foreach ((FhirSequenceEnum sequence, string trailer) in sequencesToTest) + /// + /// Installs a package. + /// + /// The package reference. + /// A task representing the asynchronous operation. The task result contains a boolean value indicating whether the package was installed successfully. + private async Task InstallPackage(PackageReference packageReference) + { + if (_cache == null) { - if ((sequence == FhirSequenceEnum.DSTU2) || - (sequence == FhirSequenceEnum.STU3)) - { - continue; - } + return false; + } - version = directiveVersion; - bool isLocal = false; - directory = string.Empty; - - string sequencedName = string.IsNullOrEmpty(trailer) ? name : $"{name}.{trailer}"; + if (packageReference.Scope == FhirCiClient.FhirCiScope) + { + await _ciClient.InstallOrUpdate(packageReference, _cache); + return true; + } - if (string.IsNullOrEmpty(version) || - version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + foreach (IPackageServer pc in _packageClients) + { + try { - TryGetHighestVersion(sequencedName, offlineMode, out version, out isLocal, out directory); - } + // try to download this package + byte[] data = await pc.GetPackage(packageReference); - if ((isLocal && !string.IsNullOrEmpty(directory)) || - HasCachedVersion(sequencedName, version, out directory)) - { - packageList.Add(new() - { - fhirVersion = _packagesByDirective[$"{sequencedName}#{version}"].FhirVersion, - directory = directory, - resolvedDirective = $"{sequencedName}#{version}", - name = sequencedName, - version = version, - umbrellaPackageName = name, - }); - - foundLocally = true; - continue; - } + // try to install this package + await _cache.Install(packageReference, data); - // do not check online if we already have the package locally - // note this can have an issue if we have a 'root' package and not a version-specific package - // but that is a rare case and can be solved by cleaning the cache. - if ((!isLocal && offlineMode) || - foundLocally) - { - continue; + // only need to install from first hit + return true; } - - if (TryDownloadViaRegistry(sequencedName, version, out directory, out fhirVersion, out resolvedDirective)) + catch (Exception) { - packageList.Add(new() - { - fhirVersion = fhirVersion, - directory = directory, - resolvedDirective = resolvedDirective, - name = sequencedName, - version = version, - umbrellaPackageName = name, - }); - - continue; + // ignore } - - //if (TryDownloadCoreViaPublication(sequencedName, version, out directory, out fhirVersion, out resolvedDirective)) - //{ - // packageList.Add(new() - // { - // fhirVersion = fhirVersion, - // directory = directory, - // resolvedDirective = resolvedDirective, - // name = sequencedName, - // version = version, - // umbrellaPackageName = name, - // }); - - // continue; - //} - } - - if (packageList.Any()) - { - packages = packageList; - return true; } - _logger.LogInformation($"FindOrDownload <<< unable to resolve directive: {directive}"); - packages = Enumerable.Empty(); return false; } - /// Attempts to download via registry a string from the given string. - /// The name. - /// [out] The version string (e.g., 4.0.1). - /// [out] Pathname of the directory. - /// [out] The FHIR version. - /// - /// True if it succeeds, false if it fails. - private bool TryDownloadViaRegistry( - string name, - string version, - out string directory, - out FhirSequenceEnum fhirVersion, - out string resolvedDirective) + private async Task PackageExists(string packageId) { - foreach (Uri registryUri in PackageRegistryUris) + if (_cache == null) { - Uri uri = new Uri(registryUri, $"{name}/{version}"); - directory = Path.Combine(_cachePackageDirectory, $"{name}#{version}"); - - string directive = name + "#" + version; + return false; + } - if (TryDownloadAndExtract(uri, directory, directive, out fhirVersion, out resolvedDirective)) + foreach (IPackageServer pc in _packageClients) + { + try { - UpdatePackageCacheIndex(directive, directory); + Firely.Fhir.Packages.Versions? versions = await pc.GetVersions(packageId); - return true; + if (versions?.IsEmpty == false) + { + return true; + } + } + catch (Exception) + { + // ignore } } - directory = string.Empty; - fhirVersion = FhirSequenceEnum.Unknown; - resolvedDirective = string.Empty; return false; } - /// Gets directory size. - /// [out] Pathname of the directory. - /// The directory size. - private static long GetDirectorySize(string directory) + /// + /// Retrieves the FHIR versions supported by a package. + /// + /// The package reference. + /// A list of FHIR sequence codes representing the supported versions. + public async Task?> InstalledPackageFhirVersions(PackageReference packageReference) { - DirectoryInfo dirInfo = new(directory); - IEnumerable fileInfos = dirInfo.EnumerateFiles("*.*", SearchOption.AllDirectories); + if (_cache == null) + { + return null; + } + + if (!await _cache.IsInstalled(packageReference)) + { + return null; + } - return fileInfos.Select(fi => fi.Length).Sum(); + _ForPackages.PackageManifest manifest = await _cache.ReadManifestEx(packageReference) ?? throw new Exception("Failed to load package manifest"); + + return manifest.AnyFhirVersions?.Select(FhirReleases.FhirVersionToSequence).ToList(); } - /// Updates the cache package initialize. - /// The directive. - /// [out] Pathname of the directory. - /// (Optional) Information describing the initialize. - private void UpdatePackageCacheIndex( - string directive, - string directory, - IniData? iniData = null) + /// + /// Gets the content directory for a specific package. + /// + /// The package reference. + /// The content directory for the package, or null if the cache is not configured. + public string? GetPackageContentDirectory(PackageReference packageReference) { - string[] components = directive.Split('#'); - string name = components[0]; - string directiveVersion = components.Length > 1 ? components[1] : "current"; + if (_cache == null) + { + return null; + } + + return _cache.PackageContentFolder(packageReference); + } - if (!Directory.Exists(directory)) + /// + /// Deletes a package based on the provided package directive. + /// + /// The package directive specifying the package to delete. + public void DeletePackage(string packageDirective) + { + if (_cache == null) { - if (iniData == null) - { - IniDataParser parser = new(); + return; + } - IniData data = parser.Parse(File.ReadAllText(_iniFilePath)); + string[] components = packageDirective.Split('@', '#'); - if (data["packages"].Contains(directive)) - { - data["packages"].Remove(directive); - } + if (components.Length != 2) + { + _logger.LogWarning($"DeletePackage <<< invalid package directive: {packageDirective}"); + return; + } - if (data["package-sizes"].Contains(directive)) - { - data["package-sizes"].Remove(directive); - } + _ = _cache.Delete(new PackageReference(components[0], components[1])); + } - SaveIniData(_iniFilePath, data); - } - else - { - if (iniData["packages"].Contains(directive)) - { - iniData["packages"].Remove(directive); - } + /// + /// Gets the version handling type based on the provided version string. + /// + /// The version string. + /// The version handling type. + private VersionHandlingTypes GetVersionHandlingType(string? version) + { + // handle simple literals + switch (version) + { + case null: + case "": + case "latest": + return VersionHandlingTypes.Latest; - if (iniData["package-sizes"].Contains(directive)) - { - iniData["package-sizes"].Remove(directive); - } - } + case "current": + return VersionHandlingTypes.ContinuousIntegration; - if (_packagesByDirective.ContainsKey(directive)) - { - _packagesByDirective.Remove(directive); - } + case "dev": + return VersionHandlingTypes.Local; + } - if (_versionsByName.ContainsKey(name)) - { - _versionsByName[name] = _versionsByName[name].Where((v) => !v.Equals(directiveVersion)).ToList(); - } + // check for local or current with branch names + if (version.StartsWith("current$", StringComparison.Ordinal)) + { + return VersionHandlingTypes.ContinuousIntegration; + } - return; + if (version.StartsWith("dev$", StringComparison.Ordinal)) + { + return VersionHandlingTypes.Local; } - long size = GetDirectorySize(directory); - string packageDate = DateTime.Now.ToString("yyyyMMddHHmmss"); + return VersionHandlingTypes.Passthrough; + } - string npmJson = Path.Combine(directory, "package", "package.json"); + /// + /// Resolves the CI literals into standard directives. + /// + /// The CI literals to resolve. + /// A list of resolved directives. + private async Task> ResolveCiLiterals(string[]? ciLiterals) + { + List directives = []; - if (File.Exists(npmJson)) + // iterate over CI directives to resolve them into standard directives + foreach (string literal in ciLiterals ?? Array.Empty()) { - FhirNpmPackageDetails npmDetails = FhirNpmPackageDetails.Load(npmJson); - if (!string.IsNullOrEmpty(npmDetails.BuildDate)) + // check to see if this is a tagged package literal + if (literal.EndsWith("current") || + literal.Contains("current$")) { - packageDate = npmDetails.BuildDate; + directives.Add(literal); + continue; } - PackageCacheRecord record = new( - directive, - PackageLoadStateEnum.NotLoaded, - name, - npmDetails.Version, - SequenceForVersion(npmDetails.FhirVersion), - packageDate, - size, - npmDetails); - - _packagesByDirective[directive] = record; - - if (!_versionsByName.ContainsKey(name)) + // try the repository reference first + List entries = await _ciClient.CatalogPackagesAsync(repo: literal); + if (entries.Count == 0) { - _versionsByName.Add(name, new()); + // check for a publication URL + entries = await _ciClient.CatalogPackagesAsync(site: literal); } - if (!_versionsByName[name].Contains(npmDetails.Version)) + if (entries.Count == 0) { - _versionsByName[name].Add(npmDetails.Version); + // check for a package name + entries = await _ciClient.CatalogPackagesAsync(pkgname: literal); } - } - - if (iniData == null) - { - IniDataParser parser = new(); - IniData data = parser.Parse(File.ReadAllText(_iniFilePath)); - - if (data["packages"].Contains(directive)) + if (entries.Count == 0) { - data["packages"].Remove(directive); + _logger.LogWarning($"ResolveCiLiterals <<< cannot resolve CI directive: {literal}!"); + continue; } - data["packages"].Add(directive, packageDate); + PackageCatalogEntry entry = entries.First(); - if (data["package-sizes"].Contains(directive)) + // check to see if we have a package name and repository URL + if (string.IsNullOrEmpty(entry.Name) || + string.IsNullOrEmpty(entry.Description) || + !entry.Description.Contains('/')) { - data["package-sizes"].Remove(directive); + _logger.LogWarning($"ResolveCiLiterals <<< invalid resolution for CI directive: {literal}! Name: {entry.Name}, Description: {entry.Description}"); + continue; } - data["package-sizes"].Add(directive, size.ToString()); + // get the branch name from the repo url + (string? branchName, bool isDefaultBranch) = FhirCiClient.GetBranchNameRepoLiteral(entry.Description); - SaveIniData(_iniFilePath, data); - } - else - { - if (iniData["packages"].Contains(directive)) + if (isDefaultBranch) { - iniData["packages"].Remove(directive); + directives.Add(entry.Name + "#current"); + continue; } - iniData["packages"].Add(directive, packageDate); - - if (iniData["package-sizes"].Contains(directive)) + if (string.IsNullOrEmpty(branchName)) { - iniData["package-sizes"].Remove(directive); + _logger.LogWarning($"ResolveCiLiterals <<< invalid resolution for CI directive: {literal} - no branch name and not default branch!"); + continue; } - iniData["package-sizes"].Add(directive, size.ToString()); + directives.Add(entry.Name + "#current$" + branchName); } + + return directives; } - /// Deletes the package described by packageDirective. - /// The package directive. - public void DeletePackage(string packageDirective) - { - if (!_hasCacheDirectory) { return; } - - string directory = Path.Combine(_cachePackageDirectory, packageDirective); - - if (!Directory.Exists(directory)) - { - return; - } - - try - { - Directory.Delete(directory, true); - - UpdatePackageCacheIndex(packageDirective, directory); - } - catch (Exception ex) - { - _logger.LogInformation($"DeletePackage <<< caught exception: {ex.Message}"); - if (ex.InnerException != null) - { - _logger.LogInformation($" <<< {ex.InnerException.Message}"); - } - } - } - - /// Attempts to download and extract a string from the given URI. - /// URI of the resource. - /// Pathname of the directory. - /// The package directive. - /// [out] The FHIR version. - /// [out] The resolved directive. - /// True if it succeeds, false if it fails. - private bool TryDownloadAndExtract( - Uri uri, - string directory, - string packageDirective, - out FhirSequenceEnum fhirVersion, - out string resolvedDirective) - { - try - { - using (Stream rawStream = _httpClient.GetStreamAsync(uri).Result) - using (Stream gzipStream = new GZipStream(rawStream, CompressionMode.Decompress)) - { - // make sure our destination directory exists - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - TarFile.ExtractToDirectory(gzipStream, directory, true); - } - - UpdatePackageCacheIndex(packageDirective, directory); - - fhirVersion = _packagesByDirective[packageDirective].FhirVersion; - resolvedDirective = packageDirective; - return true; - } - catch (HttpRequestException hex) - { - // we have a lot of not found because of package nesting, this is reported elsewhere - if (hex.StatusCode == HttpStatusCode.NotFound) - { - fhirVersion = FhirSequenceEnum.Unknown; - resolvedDirective = string.Empty; - return false; - } - - _logger.LogInformation($"TryDownloadAndExtract <<< exception downloading {uri}: {hex.Message}"); - if (hex.InnerException != null) - { - _logger.LogInformation($" <<< inner: {hex.InnerException.Message}"); - } - } - catch (Exception ex) - { - if ((ex.InnerException != null) && - (ex.InnerException is HttpRequestException hex)) - { - // we have a lot of not found because of package nesting, this is reported elsewhere - if (hex.StatusCode == HttpStatusCode.NotFound) - { - fhirVersion = FhirSequenceEnum.Unknown; - resolvedDirective = string.Empty; - return false; - } - } - - _logger.LogInformation($"TryDownloadAndExtract <<< exception downloading: {uri}: {ex.Message}"); - if (ex.InnerException != null) - { - _logger.LogInformation($" <<< inner: {ex.InnerException.Message}"); - } - } - - fhirVersion = FhirSequenceEnum.Unknown; - resolvedDirective = string.Empty; - return false; - } - - /// Package is FHIR core. - /// Name of the package. - /// True if it succeeds, false if it fails. - public static bool PackageIsFhirCore(string packageName) - { - string name = packageName.Contains('#') - ? packageName.Substring(0, packageName.IndexOf('#')) - : packageName; - - return _matchCorePackageNames.IsMatch(name); - } - - /// Attempts to download core via ci a string from the given string. - /// The name. - /// Name of the branch. - /// [out] Pathname of the directory. - /// [out] The FHIR version. - /// [out] The resolved directive. - /// True if it succeeds, false if it fails. - public bool TryDownloadCoreViaCI( - string name, - string branchName, - out string directory, - out FhirSequenceEnum fhirVersion, - out string resolvedDirective) - { - branchName = GetCoreBranchFromInput(branchName); - - Uri branchUri; - - switch (branchName.ToLowerInvariant()) - { - case "master": - case "main": - branchUri = FhirCiUri; - break; - - case null: - case "": - if (name.Contains("r4b", StringComparison.OrdinalIgnoreCase)) - { - branchUri = new Uri(FhirCiUri, $"branches/R4B/"); - } - else - { - branchUri = FhirCiUri; - } - - break; - - default: - branchUri = new Uri(FhirCiUri, $"branches/{branchName}/"); - break; - } - - directory = Path.Combine(_cachePackageDirectory, $"{name}#current"); - - string localNpmFilename = Path.Combine(directory, "package", "package.json"); - if (File.Exists(localNpmFilename)) - { - try - { - FhirNpmPackageDetails cachedNpm = FhirNpmPackageDetails.Load(localNpmFilename); - - Uri versionInfoUri = new Uri(branchUri, "version.info"); - string contents = _httpClient.GetStringAsync(versionInfoUri).Result; - - ParseVersionInfoIni( - contents, - out string ciFhirVersion, - out string ciVersion, - out string ciBuildId, - out string ciBuildDate); - - if (String.Compare(cachedNpm.BuildDate, ciBuildDate, StringComparison.Ordinal) > 0) - { - fhirVersion = SequenceForVersion(ciFhirVersion); - resolvedDirective = $"{cachedNpm.Name}#{cachedNpm.Version}"; - return true; - } - } - catch (Exception ex) - { - _logger.LogInformation($"TryDownloadCoreViaCI <<< failed to compare local to CI, forcing download ({ex.Message})"); - } - } - - Uri uri = new Uri(branchUri, $"{name}.tgz"); - - return TryDownloadAndExtract(uri, directory, $"{name}#current", out fhirVersion, out resolvedDirective); - } - - /// Attempts to download guide via ci a string from the given string. - /// Name of the branch. - /// [out] The name. - /// [out] Pathname of the directory. - /// [out] The FHIR version. - /// [out] The resolved directive. - /// True if it succeeds, false if it fails. - public bool TryDownloadGuideViaCI( - string branchName, - out string name, - out string directory, - out FhirSequenceEnum fhirVersion, - out string resolvedDirective) - { - branchName = GetIgBranchFromInput(branchName); - - try - { - Uri versionInfoUri = new Uri(FhirCiUri, $"ig/{branchName}/package.manifest.json"); - string contents = _httpClient.GetStringAsync(versionInfoUri).Result; - - FhirNpmPackageDetails ciNpm = FhirNpmPackageDetails.Parse(contents); - - name = ciNpm.Name; - directory = Path.Combine(_cachePackageDirectory, $"{ciNpm.Name}#current"); - - string localNpmFilename = Path.Combine(directory, "package", "package.json"); - - if (File.Exists(localNpmFilename)) - { - FhirNpmPackageDetails cachedNpm = FhirNpmPackageDetails.Load(localNpmFilename); - - if (String.Compare(cachedNpm.BuildDate, ciNpm.BuildDate, StringComparison.Ordinal) <= 0) - { - fhirVersion = SequenceForVersion(cachedNpm.FhirVersion); - resolvedDirective = $"{cachedNpm.Name}#{cachedNpm.Version}"; - return true; - } - } - } - catch (Exception ex) - { - _logger.LogInformation($"TryDownloadCoreViaCI <<< failed to compare local to CI, forcing download ({ex.Message})"); - name = string.Empty; - directory = string.Empty; - fhirVersion = FhirSequenceEnum.Unknown; - resolvedDirective = string.Empty; - return false; - } - - Uri uri = new Uri(FhirCiUri, $"ig/{branchName}/package.tgz"); - - return TryDownloadAndExtract(uri, directory, $"{name}#current", out fhirVersion, out resolvedDirective); - } - - /// - /// Attempts to get guide ci package details the NpmPackageDetails from the given string. - /// - /// Name of the branch. - /// [out] The details. - /// True if it succeeds, false if it fails. - public bool TryGetGuideCiPackageDetails(string branchName, out FhirNpmPackageDetails details) - { - branchName = GetIgBranchFromInput(branchName); - - try - { - Uri versionInfoUri = new Uri(FhirCiUri, $"ig/{branchName}/package.manifest.json"); - - string contents = _httpClient.GetStringAsync(versionInfoUri).Result; - - details = FhirNpmPackageDetails.Parse(contents); - - if (string.IsNullOrEmpty(details.Url)) - { - details.Url = new Uri(FhirCiUri, $"ig/{branchName}").ToString(); - } - - if (string.IsNullOrEmpty(details.Title)) - { - details.Title = $"FHIR IG: {details.Name}"; - } - - if (string.IsNullOrEmpty(details.Description)) - { - details.Description = $"CI Build from branch {branchName}, current as of: {DateTime.Now}"; - } - - if (string.IsNullOrEmpty(details.PackageType)) - { - details.PackageType = "ig"; - } - - return true; - } - catch (Exception ex) - { - _logger.LogInformation($"TryGetGuideCiPackageDetails <<< failed to find CI IG ({ex.Message})"); - } - - details = null!; - return false; - } - - /// Sequence for version. - /// [out] The version string (e.g., 4.0.1). - /// A FhirSequenceEnum. - public static FhirSequenceEnum SequenceForVersion(string version) - { - if (string.IsNullOrEmpty(version)) - { - return FhirSequenceEnum.Unknown; - } - - string val; - - if (version.Contains('.')) - { - val = version.Length > 2 - ? version.Substring(0, 3) - : version.Substring(0, 1); - } - else - { - val = version; - } - - // fallback to guessing - switch (val.ToUpperInvariant()) - { - case "DSTU2": - case "STU2": - case "R2": - case "1.0": - case "1": - case "2.0": - case "2": - return FhirSequenceEnum.DSTU2; - - case "STU3": - case "R3": - case "3.0": - case "3": - return FhirSequenceEnum.STU3; - - case "R4": - case "4": - case "4.0": - return FhirSequenceEnum.R4; - - case "R4B": - case "4B": - case "4.1": - case "4.3": - return FhirSequenceEnum.R4B; - - case "R5": - case "4.2": - case "4.4": - case "4.5": - case "4.6": - case "5.0": - case "5": - return FhirSequenceEnum.R5; - } - - return FhirSequenceEnum.Unknown; - } - - /// Package base for sequence. - /// The sequence. - /// A string. - public static string PackageBaseForSequence(FhirSequenceEnum seq) => seq switch - { - FhirSequenceEnum.R4B => "hl7.fhir.r4b", - _ => $"hl7.fhir.r{(int)seq}", - }; - - /// Literal for sequence. - /// The sequence. - /// A string. - public static string LiteralForSequence(FhirSequenceEnum seq) => seq.ToLiteral(); - - /// Attempts to get core ci package details. - /// Name of the branch. - /// [out] The details. - /// True if it succeeds, false if it fails. - public bool TryGetCoreCiPackageDetails( - string branchName, - out FhirNpmPackageDetails details) - { - branchName = GetCoreBranchFromInput(branchName); - - Uri branchUri; - - switch (branchName.ToLowerInvariant()) - { - case "master": - case "main": - case "": - branchUri = FhirCiUri; - break; - - case "r4b": - branchUri = new Uri(FhirCiUri, $"branches/R4B/"); - break; - - default: - branchUri = new Uri(FhirCiUri, $"branches/{branchName}/"); - break; - } - - try - { - Uri versionInfoUri = new Uri(branchUri, "version.info"); - string contents = _httpClient.GetStringAsync(versionInfoUri).Result; - - ParseVersionInfoIni( - contents, - out string fhirVersion, - out string version, - out string buildId, - out string buildDate); - - FhirSequenceEnum sequence = SequenceForVersion(version); - - string corePackageName = PackageBaseForSequence(sequence) + ".core"; - - details = new FhirNpmPackageDetails() - { - Name = corePackageName, - Version = version, - BuildDate = buildDate, - FhirVersionList = new string[1] { version }, - FhirVersions = new string[1] { version }, - PackageType = "core", - ToolsVersion = 3M, - Url = branchUri.ToString(), - Title = $"FHIR {sequence}: {fhirVersion}", - Description = $"CI Build from branch {branchName}, current as of: {DateTime.Now}", - Dependencies = new(), - }; - - return true; - } - catch (Exception ex) - { - _logger.LogInformation($"TryGetCoreCiPackageDetails <<< failed to get CI package details: {ex.Message}"); - } - - details = null!; - return false; - } - - /// Attempts to download guide via ci a string from the given string. - /// The name. - /// Name of the branch. - /// [out] Pathname of the directory. - /// - /// - /// True if it succeeds, false if it fails. - public bool TryDownloadGuideViaCI( - string name, - string branchName, - out string directory, - out FhirSequenceEnum fhirVersion, - out string resolvedDirective) - { - directory = Path.Combine(_cachePackageDirectory, $"{name}#current"); - - branchName = GetIgBranchFromInput(branchName); - - string localNpmFilename = Path.Combine(directory, "package", "package.json"); - if (File.Exists(localNpmFilename)) - { - try - { - FhirNpmPackageDetails cachedNpm = FhirNpmPackageDetails.Load(localNpmFilename); - - Uri versionInfoUri = new Uri(FhirCiUri, $"ig/{branchName}/package.manifest.json"); - - string contents = _httpClient.GetStringAsync(versionInfoUri).Result; - - FhirNpmPackageDetails ciNpm = FhirNpmPackageDetails.Parse(contents); - - if (String.Compare(cachedNpm.BuildDate, ciNpm.BuildDate, StringComparison.Ordinal) <= 0) - { - fhirVersion = SequenceForVersion(ciNpm.FhirVersion); - resolvedDirective = $"{ciNpm.Name}#{ciNpm.Version}"; - return true; - } - } - catch (Exception ex) - { - _logger.LogInformation($"TryDownloadCoreViaCI <<< failed to compare local to CI, forcing download ({ex.Message})"); - fhirVersion = FhirSequenceEnum.Unknown; - resolvedDirective = string.Empty; - return false; - } - } - - Uri uri = new Uri(FhirCiUri, $"ig/{branchName}/package.tgz"); - - return TryDownloadAndExtract(uri, directory, $"{name}#current", out fhirVersion, out resolvedDirective); - } - - /// - /// Attempts to get relative base for version a string from the given string. - /// Note that this does not work for ballot versions of core, but that requires - /// tracking versions individually. As this is only a fallback for when the - /// package servers are offline, it feels like a reasonable compromise. - /// - /// [out] The version string (e.g., 4.0.1). - /// [out] The relative. - /// True if it succeeds, false if it fails. - public static bool TryGetRelativeBaseForVersion(string version, out string relative) - { - // versions with a dash are not promoted to their version name root - if (version.Contains('-')) - { - relative = version; - return true; - } - - FhirSequenceEnum sequence = SequenceForVersion(version); - - if (sequence == FhirSequenceEnum.Unknown) - { - relative = string.Empty; - return false; - } - - // major releases are promoted to their version name root - relative = LiteralForSequence(sequence); - return true; - } - - /// Attempts to download core via publication a string from the given string. - /// The name. - /// [out] The version string (e.g., 4.0.1). - /// [out] Pathname of the directory. - /// [out] The FHIR version. - /// [out] The resolved directive. - /// True if it succeeds, false if it fails. - private bool TryDownloadCoreViaPublication( - string name, - string version, - out string directory, - out FhirSequenceEnum fhirVersion, - out string resolvedDirective) - { - if (!TryGetRelativeBaseForVersion(version, out string relative)) - { - directory = string.Empty; - fhirVersion = FhirSequenceEnum.Unknown; - resolvedDirective = string.Empty; - return false; - } - - string directive = name + "#" + version; - directory = Path.Combine(_cachePackageDirectory, directive); - - // most publication versions are named with correct package information - Uri uri = new Uri(FhirPublishedUri, $"{relative}/{name}.tgz"); - if (TryDownloadAndExtract(uri, directory, directive, out fhirVersion, out resolvedDirective)) - { - UpdatePackageCacheIndex(directive, directory); - return true; - } - - // some ballot versions are published directly as CI versions - uri = new Uri(FhirPublishedUri, $"{relative}/package.tgz"); - directory = Path.Combine(_cachePackageDirectory, $"hl7.fhir.core#{version}"); - if (TryDownloadAndExtract(uri, directory, directive, out fhirVersion, out resolvedDirective)) - { - UpdatePackageCacheIndex(directive, directory); - return true; - } - - return false; - } - - /// Gets package name from canonical. - /// The input. - /// The package name from canonical. - private static string GetPackageNameFromInput(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } - - if (input.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - if (input.EndsWith('/')) - { - return input.Substring(0, input.Length - 1).Split('/').Last(); - } - - return input.Split('/').Last(); - } - - return input; - } - - /// Gets ig branch from input. - /// The input. - /// The ig branch from input. - private static string GetIgBranchFromInput(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } - - string branchName = input; - - if (branchName.StartsWith("http://build.fhir.org/ig/", StringComparison.OrdinalIgnoreCase)) - { - branchName = branchName.Substring(25); - } - else if (branchName.StartsWith("https://build.fhir.org/ig/", StringComparison.OrdinalIgnoreCase)) - { - branchName = branchName.Substring(26); - } - else if (branchName.StartsWith("ig/", StringComparison.OrdinalIgnoreCase)) - { - branchName = branchName.Substring(3); - } - - if (branchName.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) - { - int last = branchName.LastIndexOf('/'); - branchName = branchName.Substring(0, last); - } - - return branchName; - } - - /// Gets core branch from input. - /// The input. - /// The core branch from input. - private static string GetCoreBranchFromInput(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } - - string branchName = input; - - if (branchName.StartsWith("http://build.fhir.org/branches/", StringComparison.OrdinalIgnoreCase)) - { - branchName = branchName.Substring(31); - } - else if (branchName.StartsWith("https://build.fhir.org/branches/", StringComparison.OrdinalIgnoreCase)) - { - branchName = branchName.Substring(32); - } - else if (branchName.StartsWith("branches/", StringComparison.OrdinalIgnoreCase)) - { - branchName = branchName.Substring(9); - } - - if (branchName.Contains('/')) - { - branchName = branchName.Split('/')[0]; - } - - return branchName; - } - - /// Gets the cached packages in this collection. - /// - /// An enumerator that allows foreach to be used to process the cached packages in this - /// collection. - /// - public IEnumerable GetCachedPackages() - { - return _packagesByDirective.Select(kvp => kvp.Value.Details).ToArray(); - } - - /// Query if 'name' has cached version. - /// The name. - /// [out] The version string (e.g., 4.0.1). - /// [out] Pathname of the directory. - /// True if cached version, false if not. - private bool HasCachedVersion( - string name, - string version, - out string directory) - { - directory = Path.Combine(_cachePackageDirectory, $"{name}#{version}"); - - if (Directory.Exists(directory)) - { - return true; - } - - directory = string.Empty; - return false; - } - - /// Attempts to get highest version. - /// The name. - /// True to enable offline mode, false to disable it. - /// [out] The version string (e.g., 4.0.1). - /// [out] True if is cached, false if not. - /// [out] Pathname of the directory. - /// True if it succeeds, false if it fails. - private bool TryGetHighestVersion( - string name, - bool offlineMode, - out string version, - out bool isCached, - out string directory) - { - string highestOnline = string.Empty; - - _ = TryGetHighestVersionOffline(name, out string highestCached); - - if (!offlineMode) - { - TryGetHighestVersionOnline(name, out highestOnline); - } - - if (string.IsNullOrEmpty(highestCached) && string.IsNullOrEmpty(highestOnline)) - { - version = string.Empty; - isCached = false; - directory = string.Empty; - return false; - } - - if (highestCached.Equals(highestOnline, StringComparison.Ordinal)) - { - version = highestCached; - isCached = true; - directory = Path.Combine(_cachePackageDirectory, $"{name}#{version}"); - return true; - } - - if (RegistryPackageManifest.IsFirstHigherVersion(highestCached, highestOnline)) - { - version = highestCached; - isCached = true; - directory = Path.Combine(_cachePackageDirectory, $"{name}#{version}"); - return true; - } - - version = highestOnline; - isCached = false; - directory = string.Empty; - return true; - } - - /// Attempts to get highest version of a package from the local cache. - /// The name. - /// [out] The version string (e.g., 4.0.1). - /// True if it succeeds, false if it fails. - private bool TryGetHighestVersionOffline( - string name, - out string version) - { - if (!_versionsByName.ContainsKey(name)) - { - version = string.Empty; - return false; - } - - string highestVersion = string.Empty; - - foreach (string cachedVersion in _versionsByName[name]) - { - if (cachedVersion.Equals("dev", StringComparison.OrdinalIgnoreCase) || - cachedVersion.Equals("current", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (RegistryPackageManifest.IsFirstHigherVersion(cachedVersion, highestVersion)) - { - highestVersion = cachedVersion; - } - } - - if (string.IsNullOrEmpty(highestVersion)) - { - version = string.Empty; - return false; - } - - version = highestVersion; - return true; - } - - /// - /// Attempts to get highest version of a package from the package registries online. - /// - /// The name. - /// [out] The version string (e.g., 4.0.1). - /// True if it succeeds, false if it fails. - private bool TryGetHighestVersionOnline( - string name, - out string version) - { - if (!TryGetPackageManifests(name, out IEnumerable manifests)) - { - version = string.Empty; - return false; - } - - string highestVersion = string.Empty; - - foreach (RegistryPackageManifest manifest in manifests) - { - string manifestHighest = manifest.HighestVersion(); - - if (RegistryPackageManifest.IsFirstHigherVersion(manifestHighest, highestVersion)) - { - highestVersion = manifestHighest; - } - } - - version = highestVersion; - return !string.IsNullOrEmpty(version); - } - - /// Attempts to get a package manifest for a given package. - /// Name of the package. - /// [out] The manifest. - /// True if it succeeds, false if it fails. - public bool TryGetPackageManifests(string packageName, out IEnumerable manifests) - { - List manifestList = new(); - - packageName = GetPackageNameFromInput(packageName); - - foreach (Uri registryUri in PackageRegistryUris) - { - try - { - Uri requestUri = new(registryUri, packageName); - - HttpResponseMessage response = _httpClient.GetAsync(requestUri).Result; - - if (!response.IsSuccessStatusCode) - { - //_logger.LogInformation( - // $"GetPackageVersionsAndUrls <<<" + - // $" Failed to get package info: {response.StatusCode}" + - // $" {requestUri.AbsoluteUri}"); - continue; - } - - string json = response.Content.ReadAsStringAsync().Result; - - RegistryPackageManifest? info = RegistryPackageManifest.Parse(json); - - if (!(info?.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase) ?? false)) - { - //_logger.LogInformation( - // $"GetPackageVersionsAndUrls <<<" + - // $" Package information mismatch: requested {requestUri.AbsoluteUri}" + - // $" received manifest for {info?.Name}"); - continue; - } - - if (!(info.Versions?.Any() ?? false)) - { - //_logger.LogInformation( - // $"GetPackageVersionsAndUrls <<<" + - // $" package {requestUri.AbsoluteUri}" + - // $" contains NO versions"); - continue; - } - - manifestList.Add(info); - } - catch (Exception ex) - { - _logger.LogInformation( - $"GetPackageVersionsAndUrls <<<" + - $" Server {registryUri.AbsoluteUri}" + - $" Package {packageName}" + - $" threw: {ex.Message}"); - if (ex.InnerException != null) - { - _logger.LogInformation($" <<< {ex.InnerException.Message}"); - } - } - } - - if (manifestList.Any()) - { - manifests = manifestList.AsEnumerable(); - return true; - } - - //_logger.LogInformation( - // $"Package {packageName}" + - // $" was not found on any registry."); - manifests = Enumerable.Empty(); - return false; - } - - /// Discover cached packages. - private void SynchronizeCache() - { - bool modified = false; - - IniParser.IniDataParser parser = new(); - - IniData data = parser.Parse(File.ReadAllText(_iniFilePath)); - - if (!data.Sections.Contains("packages")) - { - _packagesByDirective.Clear(); - _versionsByName.Clear(); - return; - } - - List directivesToRemove = new(); - - foreach (IniParser.Model.Property? line in data["packages"]) - { - if (line == null) - { - continue; - } - - ProcessSync(data, line.Key, line.Value, out bool shouldRemove); - - if (shouldRemove) - { - directivesToRemove.Add(line.Key); - } - } - - foreach (string directive in directivesToRemove) - { - _logger.LogInformation($" <<< removing {directive} from ini"); - - if (data["packages"].Contains(directive)) - { - data["packages"].Remove(directive); - } - - if (data["package-sizes"].Contains(directive)) - { - data["package-sizes"].Remove(directive); - } - - modified = true; - } - - IEnumerable directories = Directory.EnumerateDirectories(_cachePackageDirectory, "*", SearchOption.TopDirectoryOnly); - - foreach (string directory in directories) - { - string directive = Path.GetFileName(directory); - - if (!data["packages"].Contains(directive)) - { - _logger.LogInformation($" <<< adding {directive} to ini"); - UpdatePackageCacheIndex(directive, directory, data); - modified = true; - ProcessSync(data, directive, data["packages"][directive], out _); - } - } - - if (modified) - { - SaveIniData(_iniFilePath, data); - } - - _logger.LogInformation($" << cache contains {_packagesByDirective.Count} packages"); - } - - /// Updates the package state. - /// The directive. - /// Name of the resolved. - /// The resolved version. - /// State of to. - public void UpdatePackageState( - string directive, - string resolvedName, - string resolvedVersion, - PackageLoadStateEnum toState) + /// Updates the package state. + /// The directive. + /// Name of the resolved. + /// The resolved version. + /// State of to. + public void UpdatePackageState( + string directive, + string resolvedName, + string resolvedVersion, + PackageLoadStateEnum toState) { if (!_packagesByDirective.ContainsKey(directive)) { @@ -1761,173 +725,6 @@ public bool TryGetPackageState(string directive, out PackageLoadStateEnum state) return true; } - /// Process the synchronize. - /// The data. - /// The directive. - /// - /// [out] True if should remove. - private void ProcessSync( - IniData data, - string directive, - string packageDate, - out bool shouldRemove) - { - if (!directive.Contains('#')) - { - _logger.LogInformation($" <<< unknown package directive: {directive}"); - shouldRemove = true; - return; - } - - if (!Directory.Exists(Path.Combine(_cachePackageDirectory, directive))) - { - _logger.LogInformation($"SynchronizeCache <<< removing entry {directive}, directory not found!"); - shouldRemove = true; - return; - } - - string[] components = directive.Split('#', StringSplitOptions.TrimEntries); - if (components.Length != 2) - { - _logger.LogInformation($"SynchronizeCache <<< unparseable package directive: {directive}"); - shouldRemove = true; - return; - } - - shouldRemove = false; - - string name = components[0]; - string version = components[1]; - long size = -1; - FhirNpmPackageDetails versionInfo; - - if (data.Sections.Contains("package-sizes") && - data["package-sizes"].Contains(directive)) - { - _ = long.TryParse(data["package-sizes"][directive], out size); - } - - try - { - versionInfo = FhirNpmPackageDetails.Load(Path.Combine(_cachePackageDirectory, directive)); - } - catch (Exception ex) - { - _logger.LogInformation($"DiscoverCachedPackages <<< skipping package: {directive} - {ex.Message}"); - if (ex.InnerException != null) - { - _logger.LogInformation($" <<< inner: {ex.InnerException.Message}"); - } - - return; - } - - PackageCacheRecord record = new( - directive, - PackageLoadStateEnum.NotLoaded, - name, - version, - SequenceForVersion(versionInfo.FhirVersion), - packageDate, - size, - versionInfo); - - _packagesByDirective[directive] = record; - - if (!_versionsByName.ContainsKey(name)) - { - _versionsByName.Add(name, new()); - } - - _versionsByName[name].Add(version); - } - - /// Gets local version information. - /// Thrown when the requested file is not present. - /// The contents. - /// [out] The FHIR version. - /// [out] The version string (e.g., 4.0.1). - /// [out] Identifier for the build. - /// [out] The build date. - private static void ParseVersionInfoIni( - string contents, - out string fhirVersion, - out string version, - out string buildId, - out string buildDate) - { - IEnumerable lines = contents.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); - - fhirVersion = string.Empty; - version = string.Empty; - buildId = string.Empty; - buildDate = string.Empty; - - foreach (string line in lines) - { - if (!line.Contains('=', StringComparison.Ordinal)) - { - continue; - } - - string[] kvp = line.Split('='); - - if (kvp.Length != 2) - { - continue; - } - - switch (kvp[0]) - { - case "FhirVersion": - fhirVersion = kvp[1]; - break; - - case "version": - version = kvp[1]; - break; - - case "buildId": - buildId = kvp[1]; - break; - - case "date": - buildDate = kvp[1]; - break; - } - } - } - - /// Creates empty cache initialize. - private void CreateEmptyCacheIni() - { - IniData data = new(); - - data.Sections.Add(new IniParser.Model.Section("cache")); - data["cache"].Add("version", "3"); - data.Sections.Add(new IniParser.Model.Section("urls")); - data.Sections.Add(new IniParser.Model.Section("local")); - data.Sections.Add(new IniParser.Model.Section("packages")); - data.Sections.Add(new IniParser.Model.Section("package-sizes")); - - SaveIniData(_iniFilePath, data); - } - - /// Saves an initialize data. - /// Full pathname of the destination file. - /// The data. - private void SaveIniData(string destinationPath, IniData data) - { - IniFormattingConfiguration formattingConfig = new() - { - NewLineType = IniParser.Configuration.IniFormattingConfiguration.ENewLine.Windows, - }; - - IniDataFormatter formatter = new(); - - File.WriteAllText(destinationPath, formatter.Format(data, formattingConfig)); - } - /// /// Releases the unmanaged resources used by the /// and optionally releases the managed resources. diff --git a/src/fhir-candle/Services/FhirStoreManager.cs b/src/fhir-candle/Services/FhirStoreManager.cs index 28736a5..d7fa917 100644 --- a/src/fhir-candle/Services/FhirStoreManager.cs +++ b/src/fhir-candle/Services/FhirStoreManager.cs @@ -10,10 +10,14 @@ using System.Collections; using System.Linq; using fhir.candle.Models; +using FhirCandle.Configuration; using FhirCandle.Extensions; using FhirCandle.Models; using FhirCandle.Storage; +using FhirCandle.Utils; using FhirStore.Smart; +using Firely.Fhir.Packages; +using Hl7.Fhir.Utility; using static Org.BouncyCastle.Math.EC.ECCurve; namespace fhir.candle.Services; @@ -34,7 +38,7 @@ public class FhirStoreManager : IFhirStoreManager, IDisposable private Dictionary _tenants; /// The server configuration. - private ServerConfiguration _serverConfig; + private CandleConfig _serverConfig; /// The package service. private IFhirPackageService _packageService; @@ -109,7 +113,7 @@ public class FhirStoreManager : IFhirStoreManager, IDisposable public FhirStoreManager( Dictionary tenants, ILogger logger, - ServerConfiguration serverConfiguration, + CandleConfig serverConfiguration, IFhirPackageService fhirPackageService) { _tenants = tenants; @@ -158,15 +162,15 @@ public void Init() switch (config.FhirVersion) { - case TenantConfiguration.SupportedFhirVersions.R4: + case FhirReleases.FhirSequenceCodes.R4: _storesByController.Add(name, new candleR4::FhirCandle.Storage.VersionedFhirStore()); break; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: _storesByController.Add(name, new candleR4B::FhirCandle.Storage.VersionedFhirStore()); break; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: _storesByController.Add(name, new candleR5::FhirCandle.Storage.VersionedFhirStore()); break; } @@ -308,7 +312,10 @@ private void LoadPackagePages() { foreach ((string name, IFhirStore store) in _storesByController) { - if (store.LoadedPackages.Contains(page.ContentFor) || store.LoadedSupplements.Contains(page.ContentFor)) + if ((page.ContentFor == _serverConfig.ReferenceImplementation) || + store.LoadedPackageDirectives.Contains(page.ContentFor) || + store.LoadedPackageIds.Contains(page.ContentFor) || + store.LoadedSupplements.Contains(page.ContentFor)) { Console.WriteLine($"Testing page: {page.PageName} (only for: {page.OnlyShowOnEndpoint}) against store {name}"); @@ -330,7 +337,7 @@ private void LoadPackagePages() continue; } - if (!page.FhirVersionLiteral.TryFhirEnum(out TenantConfiguration.SupportedFhirVersions pageFhirVersion)) + if (!FhirReleases.TryGetSequence(page.FhirVersionLiteral, out FhirReleases.FhirSequenceCodes pageFhirVersion)) { continue; } @@ -339,7 +346,10 @@ private void LoadPackagePages() foreach ((string name, IFhirStore store) in _storesByController) { if ((store.Config.FhirVersion == pageFhirVersion) && - (store.LoadedPackages.Contains(page.ContentFor) || store.LoadedSupplements.Contains(page.ContentFor))) + ((page.ContentFor == _serverConfig.ReferenceImplementation) || + store.LoadedPackageDirectives.Contains(page.ContentFor) || + store.LoadedPackageIds.Contains(page.ContentFor) || + store.LoadedSupplements.Contains(page.ContentFor))) { Console.WriteLine($"Testing page: {page.PageName} (only for: {page.OnlyShowOnEndpoint}) against store {name}"); @@ -375,7 +385,7 @@ private record struct LoadedPackageRec( string version, string directory, string supplementDirectory, - FhirPackageService.FhirSequenceEnum fhirVersion); + FhirReleases.FhirSequenceCodes fhirVersion); /// Loads ri contents. /// The dir. @@ -394,7 +404,7 @@ public void LoadRiContents(string dir) { switch (config.FhirVersion) { - case TenantConfiguration.SupportedFhirVersions.R4: + case FhirReleases.FhirSequenceCodes.R4: if (Directory.Exists(Path.Combine(dir, "r4"))) { _storesByController[tenantName].LoadPackage( @@ -420,7 +430,7 @@ public void LoadRiContents(string dir) true); } break; - case TenantConfiguration.SupportedFhirVersions.R4B: + case FhirReleases.FhirSequenceCodes.R4B: if (Directory.Exists(Path.Combine(dir, "r4b"))) { _storesByController[tenantName].LoadPackage( @@ -446,7 +456,7 @@ public void LoadRiContents(string dir) true); } break; - case TenantConfiguration.SupportedFhirVersions.R5: + case FhirReleases.FhirSequenceCodes.R5: if (Directory.Exists(Path.Combine(dir, "r5"))) { _storesByController[tenantName].LoadPackage( @@ -502,230 +512,63 @@ public async Task LoadRequestedPackages(string supplementalRoot, bool loadExampl } } - List loadRecs = new(); - - foreach (string branchName in _serverConfig.CiPackages) + if (!_packageService.IsConfigured) { - _logger.LogInformation($"FhirStoreManager <<< loading CI package {branchName}..."); - - if (!_packageService.FindOrDownload(string.Empty, branchName, out IEnumerable pacakges, false)) - { - throw new Exception($"Unable to find or download CI package: {branchName}"); - } - - List directiveRecs = new(); - - foreach (FhirPackageService.PackageCacheEntry entry in pacakges) - { - directiveRecs.Add(EntryToRec(supplementalRoot, entry)); - } - - // single entry means single package, multiple means first is umbrella package - if (directiveRecs.Count == 1) - { - loadRecs.Add(directiveRecs[0]); - } - else - { - loadRecs.AddRange(directiveRecs.Skip(1)); - } + _logger.LogInformation("FhirStoreManager <<< Package service is not configured and will not be available!"); + return; } - foreach (string directive in _serverConfig.PublishedPackages) - { - _logger.LogInformation($"FhirStoreManager <<< Loading published package {directive}..."); - - if (!_packageService.FindOrDownload(directive, string.Empty, out IEnumerable pacakges, false)) - { - throw new Exception($"Unable to find or download published package: {directive}"); - } + List loadRecs = []; - List directiveRecs = new(); - - foreach (FhirPackageService.PackageCacheEntry entry in pacakges) - { - directiveRecs.Add(EntryToRec(supplementalRoot, entry)); - } + List allTenantFhirVersions = _tenants.Values.Select(t => t.FhirVersion).Distinct().ToList(); - // single entry means single package, multiple means first is umbrella package - if (directiveRecs.Count == 1) - { - loadRecs.Add(directiveRecs[0]); - } - else - { - loadRecs.AddRange(directiveRecs.Skip(1)); - } - } + List localPackages = await _packageService.InstallPackages( + _serverConfig.PublishedPackages, + _serverConfig.CiPackages, + allTenantFhirVersions); - foreach (LoadedPackageRec r in loadRecs) + // loop over package references to load - go in ascending version order the newest versions are loaded last + foreach (PackageReference pr in localPackages.OrderBy(r => r.Version)) { - _logger.LogInformation($"FhirStoreManager <<< discovering and loading additional content for {r.directive}..."); + _logger.LogInformation($"FhirStoreManager <<< discovering and loading additional content for {pr.Moniker}..."); + + List? packageFhirVersions = await _packageService.InstalledPackageFhirVersions(pr); // loop over controllers to see where we can add this foreach ((string tenantName, TenantConfiguration config) in _tenants) { - switch (config.FhirVersion) + // if this package lists FHIR versions and it doesn't include the tenant's version, skip it + if ((packageFhirVersions != null) && + !packageFhirVersions.Contains(config.FhirVersion)) { - case TenantConfiguration.SupportedFhirVersions.R4: - if (r.fhirVersion == FhirPackageService.FhirSequenceEnum.R4) - { - if ((!string.IsNullOrEmpty(r.supplementDirectory)) && - Directory.Exists(Path.Combine(r.supplementDirectory, "r4"))) - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - Path.Combine(r.supplementDirectory, "r4"), - loadExamples); - } - else if ((!string.IsNullOrEmpty(r.supplementDirectory)) && - Directory.Exists(Path.Combine(r.supplementDirectory, tenantName))) - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - Path.Combine(r.supplementDirectory, tenantName), - loadExamples); - } - else - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - r.supplementDirectory, - loadExamples); - } - } - break; - case TenantConfiguration.SupportedFhirVersions.R4B: - if (r.fhirVersion == FhirPackageService.FhirSequenceEnum.R4B) - { - if ((!string.IsNullOrEmpty(r.supplementDirectory)) && - Directory.Exists(Path.Combine(r.supplementDirectory, "r4b"))) - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - Path.Combine(r.supplementDirectory, "r4b"), - loadExamples); - } - else if ((!string.IsNullOrEmpty(r.supplementDirectory)) && - Directory.Exists(Path.Combine(r.supplementDirectory, tenantName))) - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - Path.Combine(r.supplementDirectory, tenantName), - loadExamples); - } - else - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - r.supplementDirectory, - loadExamples); - } - } - break; - case TenantConfiguration.SupportedFhirVersions.R5: - if (r.fhirVersion == FhirPackageService.FhirSequenceEnum.R5) - { - if ((!string.IsNullOrEmpty(r.supplementDirectory)) && - Directory.Exists(Path.Combine(r.supplementDirectory, "r5"))) - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - Path.Combine(r.supplementDirectory, - "r5"), - loadExamples); - } - else if ((!string.IsNullOrEmpty(r.supplementDirectory)) && - Directory.Exists(Path.Combine(r.supplementDirectory, tenantName))) - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - Path.Combine(r.supplementDirectory, tenantName), - loadExamples); - } - else - { - _storesByController[tenantName].LoadPackage( - r.directive, - r.directory, - r.supplementDirectory, - loadExamples); - } - } - break; - default: - break; + continue; } - } - } - - LoadedPackageRec EntryToRec(string entryRoot, FhirPackageService.PackageCacheEntry entry) - { - string supplementDir = GetSupplementDir(entryRoot, entry); - - string[] directiveComponents = entry.resolvedDirective.Split('#'); - string packageName = directiveComponents.Any() ? directiveComponents[0] : string.Empty; - string packageVersion = directiveComponents.Length > 1 ? directiveComponents[1] : string.Empty; + // make sure this package exists on disk + if (_packageService.GetPackageContentDirectory(pr) is string contentDir) + { + // check to see if we should skip this package for this tenant because a FHIR-version-specific package exists + string packageName = pr.Name! + "." + config.FhirVersion.ToRLiteral().ToLowerInvariant(); + if (localPackages.Any(r => r.Name == packageName)) + { + continue; + } - return new LoadedPackageRec( - entry.resolvedDirective, - packageName, - packageVersion, - entry.directory, - supplementDir, - entry.fhirVersion); + _storesByController[tenantName].LoadPackage( + pr.Moniker, + contentDir, + GetSupplementDir(supplementalRoot, pr), + loadExamples); + } + } } - - //bool ShouldLoadPackage( - // LoadedPackageRec r, - // TenantConfiguration t, - // out string directive, - // out string directory, - // out string supplementalDirectory) - //{ - - - // if (!VersionsMatch(r, t)) - // { - // // check to see if we have a non-matching supplemental package - - // directive = string.Empty; - // directory = string.Empty; - // supplementalDirectory = string.Empty; - // return false; - // } - - - //} - } - /// Versions match. - /// A LoadedPackageRec to process. - /// A TenantConfiguration to process. - /// True if it succeeds, false if it fails. - static bool VersionsMatch(LoadedPackageRec r, TenantConfiguration t) => t.FhirVersion switch - { - TenantConfiguration.SupportedFhirVersions.R4 => r.fhirVersion == FhirPackageService.FhirSequenceEnum.R4, - TenantConfiguration.SupportedFhirVersions.R4B => r.fhirVersion == FhirPackageService.FhirSequenceEnum.R4B, - TenantConfiguration.SupportedFhirVersions.R5 => r.fhirVersion == FhirPackageService.FhirSequenceEnum.R5, - _ => false, - }; - /// Gets supplement dir. /// The supplemental root. /// The resolved directive entry. /// The supplement dir. - private string GetSupplementDir(string supplementalRoot, FhirPackageService.PackageCacheEntry entry) + private string GetSupplementDir(string supplementalRoot, PackageReference packageReference) { if (string.IsNullOrEmpty(supplementalRoot)) { @@ -735,31 +578,26 @@ private string GetSupplementDir(string supplementalRoot, FhirPackageService.Pack string dir; // check to see if we have an exact match - dir = Path.Combine(supplementalRoot, entry.resolvedDirective); - if (Directory.Exists(dir)) - { - return dir; - } - - // check for named package without version - dir = Path.Combine(supplementalRoot, entry.name); + dir = Path.Combine(supplementalRoot, packageReference.Moniker); if (Directory.Exists(dir)) { return dir; } - // check for umbrella package with version - dir = Path.Combine(supplementalRoot, entry.umbrellaPackageName + "#" + entry.version); + dir = Path.Combine(supplementalRoot, packageReference.Moniker.Replace('@', '#')); if (Directory.Exists(dir)) { return dir; } - // check for umbrella package without version - dir = Path.Combine(supplementalRoot, entry.umbrellaPackageName); - if (Directory.Exists(dir)) + // check for named package without version + if (!string.IsNullOrEmpty(packageReference.Name)) { - return dir; + dir = Path.Combine(supplementalRoot, packageReference.Name); + if (Directory.Exists(dir)) + { + return dir; + } } return string.Empty; diff --git a/src/fhir-candle/Services/IFhirPackageService.cs b/src/fhir-candle/Services/IFhirPackageService.cs index 272a0e7..26154a5 100644 --- a/src/fhir-candle/Services/IFhirPackageService.cs +++ b/src/fhir-candle/Services/IFhirPackageService.cs @@ -3,6 +3,9 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // +using FhirCandle.Utils; +using FhirCandle.Models; +using Firely.Fhir.Packages; using static fhir.candle.Services.FhirPackageService; namespace fhir.candle.Services; @@ -23,17 +26,29 @@ public interface IFhirPackageService : IHostedService /// The package directive. void DeletePackage(string packageDirective); - /// Attempts to find locally or download a given package. - /// The directive. - /// Name of the branch. - /// - /// True to enable offline mode, false to disable it. - /// True if it succeeds, false if it fails. - bool FindOrDownload( - string directive, - string branchName, - out IEnumerable packages, - bool offlineMode); + /// Installs packages based on directives or CI literals. + /// The package directives. + /// The ci literals. + /// The FHIR versions. + /// An asynchronous result that yields a List<PackageReference> + Task> InstallPackages( + string[]? packageDirectives, + string[]? ciLiterals, + List? fhirVersions); + + /// + /// Retrieves the FHIR versions supported by a package. + /// + /// The package reference. + /// A list of FHIR sequence codes representing the supported versions. + Task?> InstalledPackageFhirVersions(PackageReference packageReference); + + /// + /// Gets the content directory for a specific package. + /// + /// The package reference. + /// The content directory for the package, or null if the cache is not configured. + string? GetPackageContentDirectory(PackageReference packageReference); /// Initializes the FhirPackageService. void Init(); diff --git a/src/FhirStore.Common/Storage/IFhirStoreManager.cs b/src/fhir-candle/Services/IFhirStoreManager.cs similarity index 95% rename from src/FhirStore.Common/Storage/IFhirStoreManager.cs rename to src/fhir-candle/Services/IFhirStoreManager.cs index 780b5f6..cf43b01 100644 --- a/src/FhirStore.Common/Storage/IFhirStoreManager.cs +++ b/src/fhir-candle/Services/IFhirStoreManager.cs @@ -4,10 +4,11 @@ // using FhirCandle.Models; +using FhirCandle.Storage; using FhirStore.Smart; using Microsoft.Extensions.Hosting; -namespace FhirCandle.Storage; +namespace fhir.candle.Services; /// Interface for FHIR store manager. public interface IFhirStoreManager : IHostedService, IReadOnlyDictionary diff --git a/src/fhir-candle/Services/NotificationManager.cs b/src/fhir-candle/Services/NotificationManager.cs index e967c37..5b3e966 100644 --- a/src/fhir-candle/Services/NotificationManager.cs +++ b/src/fhir-candle/Services/NotificationManager.cs @@ -12,6 +12,7 @@ using MimeKit; using fhir.candle.Models; using System.Collections.Concurrent; +using FhirCandle.Configuration; namespace fhir.candle.Services; @@ -71,7 +72,7 @@ private record NotificationRequest /// Manager for FHIR store. /// The logger. public NotificationManager( - ServerConfiguration serverConfig, + CandleConfig serverConfig, Dictionary tenants, IFhirStoreManager fhirStoreManager, ILogger logger) @@ -79,9 +80,9 @@ public NotificationManager( _logger = logger; _storeManager = fhirStoreManager; - _zulipUrl = serverConfig.ZulipUrl; - _zulipEmail = serverConfig.ZulipEmail; - _zulipKey = serverConfig.ZulipKey; + _zulipUrl = serverConfig.ZulipUrl ?? string.Empty; + _zulipEmail = serverConfig.ZulipEmail ?? string.Empty; + _zulipKey = serverConfig.ZulipKey ?? string.Empty; if (string.IsNullOrEmpty(_zulipUrl) || string.IsNullOrEmpty(_zulipEmail) || @@ -95,10 +96,10 @@ public NotificationManager( ZulipClientPool.AddOrRegisterClient(_zulipUrl, _zulipEmail, _zulipKey); } - _smtpHost = serverConfig.SmtpHost; + _smtpHost = serverConfig.SmtpHost ?? string.Empty; _smtpPort = serverConfig.SmtpPort; - _smtpUser = serverConfig.SmtpUser; - _smtpPassword = serverConfig.SmtpPassword; + _smtpUser = serverConfig.SmtpUser ?? string.Empty; + _smtpPassword = serverConfig.SmtpPassword ?? string.Empty; if (string.IsNullOrEmpty(_smtpHost) || string.IsNullOrEmpty(_smtpUser) || diff --git a/src/fhir-candle/Services/SmartAuthManager.cs b/src/fhir-candle/Services/SmartAuthManager.cs index be161e9..ae49cb5 100644 --- a/src/fhir-candle/Services/SmartAuthManager.cs +++ b/src/fhir-candle/Services/SmartAuthManager.cs @@ -6,6 +6,7 @@ using fhir.candle.Models; using fhir.candle.Pages.Smart; using fhir.candle.Pages.Subscriptions; +using FhirCandle.Configuration; using FhirCandle.Models; using FhirCandle.Smart; using FhirCandle.Storage; @@ -48,7 +49,7 @@ public class SmartAuthManager : ISmartAuthManager, IDisposable private Dictionary _tenants; /// The server configuration. - private ServerConfiguration _serverConfig; + private CandleConfig _serverConfig; /// The smart configs. private Dictionary _smartConfigs = new(StringComparer.OrdinalIgnoreCase); @@ -67,7 +68,7 @@ public class SmartAuthManager : ISmartAuthManager, IDisposable /// The logger. public SmartAuthManager( Dictionary tenants, - ServerConfiguration serverConfiguration, + CandleConfig serverConfiguration, ILogger? logger) { _tenants = tenants; diff --git a/src/fhir-candle/_ForPackages/AuthorJsonConverter.cs b/src/fhir-candle/_ForPackages/AuthorJsonConverter.cs new file mode 100644 index 0000000..13e1a22 --- /dev/null +++ b/src/fhir-candle/_ForPackages/AuthorJsonConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace fhir.candle._ForPackages +{ + /// + /// Only does something custom for the author element, otherwise just do the regular serialization. + /// + internal class AuthorJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(AuthorInfo) || objectType == typeof(string); + } + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + + if (reader.TokenType == JsonToken.StartObject) + { + + if (objectType == typeof(AuthorInfo)) + { + // Use DummyDictionary to fool JsonSerializer into not using this converter recursively + var author = serializer.Deserialize(reader); + return author; + } + } + else if (reader.TokenType == JsonToken.String && reader.Path == "author") + { + if (reader.Value?.ToString() is { } value) + { + var author = AuthorSerializer.Deserialize(value); + return author; + } + } + return serializer.Deserialize(reader); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value is AuthorInfo author) + { + serializer.Serialize(writer, author.ParsedFromString ? AuthorSerializer.Serialize(author) : value); + } + else + { + serializer.Serialize(writer, value); + } + } + + /// + /// Dummy to fool JsonSerializer into not using this converter recursively + /// + private class DummyDictionary : AuthorInfo { } + } +} diff --git a/src/fhir-candle/_ForPackages/DiskPackageCache.cs b/src/fhir-candle/_ForPackages/DiskPackageCache.cs new file mode 100644 index 0000000..3adefa6 --- /dev/null +++ b/src/fhir-candle/_ForPackages/DiskPackageCache.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Firely.Fhir.Packages; +using Newtonsoft.Json; + +namespace fhir.candle._ForPackages; + +public class DiskPackageCache : Firely.Fhir.Packages.DiskPackageCache +{ + public DiskPackageCache(string? rootDirectory = null) + : base(rootDirectory) + { + } + + private static readonly JsonSerializerSettings SETTINGS = new() + { + MissingMemberHandling = MissingMemberHandling.Ignore, + //Converters = new List { new AuthorJsonConverter(), new ManifestDateJsonConverter() } + }; + + public Task ReadManifestEx(Firely.Fhir.Packages.PackageReference reference) + { + string folder = PackageContentFolder(reference); + + if (!Directory.Exists(folder)) + { + return Task.FromResult(null); + } + + string path = Path.Combine(folder, Firely.Fhir.Packages.PackageFileNames.MANIFEST); + if (!File.Exists(path)) + { + return Task.FromResult(null); + } + + string json = File.ReadAllText(path); + PackageManifest? manifest = JsonConvert.DeserializeObject(json, SETTINGS); + + return Task.FromResult(manifest); + } + + + /// + /// Deletes a package from the disk package cache. + /// + /// The package reference. +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public async Task Delete(PackageReference reference) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + var target = PackageContentFolder(reference); + if (!Directory.Exists(target)) + { + return; + } + + // delete the directory recursively + Directory.Delete(target, true); + } + +} diff --git a/src/fhir-candle/_ForPackages/FhirCiClient.cs b/src/fhir-candle/_ForPackages/FhirCiClient.cs new file mode 100644 index 0000000..c13fc23 --- /dev/null +++ b/src/fhir-candle/_ForPackages/FhirCiClient.cs @@ -0,0 +1,1089 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http.Headers; +using System.Text; +using Firely.Fhir.Packages; +using Newtonsoft.Json; + +#nullable enable + +namespace fhir.candle._ForPackages +{ + /// + /// Represents a client for interacting with a FHIR Continuous Integration (CI) server. + /// + public class FhirCiClient : IPackageServer, IDisposable + { + /// (Immutable) The ci scope. + public const string FhirCiScope = "build.fhir.org"; + + /// (Immutable) The default time to refresh the CI build listings, 0 for every request (no caching), -1 to never automatically refresh. + private const int _defaultInvalidationSeconds = -1; + + /// (Immutable) Literal used to designate the branch name in the version string. + private const string _ciBranchDelimiter = ".b-"; + + /// (Immutable) The CI version date format. + private const string _ciVersionDateFormat = "yyyyMMdd-HHmmssZ"; + + /// (Immutable) The default branch names. + private static readonly HashSet _defaultBranchNames = new() { "main", "master" }; + + /// (Immutable) URI of the FHIR CI server. + private static readonly Uri _ciUri = new("http://build.fhir.org/"); + + /// (Immutable) The ci URI using HTTPS. + private static readonly Uri _ciUriS = new("https://build.fhir.org/"); + + private static readonly Uri _ciBranchUri = new("https://build.fhir.org/branches/"); + + /// (Immutable) URI of the qas. + private static readonly Uri _qasUri = new("https://build.fhir.org/ig/qas.json"); + + /// (Immutable) URI of the ig list. + private static readonly Uri _igListUri = new("https://github.com/FHIR/ig-registry/blob/master/fhir-ig-list.json"); + + /// (Immutable) The HTTP client. + private readonly HttpClient _httpClient; + + /// The QAS records for the CI server, grouped by package id. + private Dictionary> _qasByPackageId = new(); + + /// The QAS records for the CI server. + private FhirCiQaRecord[] _qas = Array.Empty(); + + /// When the local copy of the CI QAS was last updated. + private DateTimeOffset _qasLastUpdated = DateTimeOffset.MinValue; + + /// The listing invalidation in seconds. + private int _listingInvalidationSeconds; + + /// Initializes a new instance of the class. + /// (Optional) The listing invalidation in seconds, 0 for + /// every request (no caching), -1 to never automatically refresh. + /// (Optional) The instance to + /// use. If null, a new instance will be created. + public FhirCiClient(int listingInvalidationSeconds = _defaultInvalidationSeconds, HttpClient? client = null) + { + _listingInvalidationSeconds = listingInvalidationSeconds; + _httpClient = client ?? new HttpClient(); + } + + /// Creates a new instance of the class. + /// (Optional) The listing invalidation in seconds, 0 for + /// every request (no caching), -1 to never automatically refresh. + /// (Optional) Specifies whether to create an insecure + /// client. + /// A new instance of the class. + public static FhirCiClient Create(int listingInvalidationSeconds = _defaultInvalidationSeconds, bool insecure = false) + { + var httpClient = new HttpClient(); + return new FhirCiClient(listingInvalidationSeconds, httpClient); + } + + /// + public override string? ToString() => _ciUriS.ToString(); + + /// Updates the ci listing cache. + /// An asynchronous result. + public async Task UpdateCiListingCache() + { + _ = await getQAs(true); + } + + /// Get the QA records and update the cache if necessary. + /// (Optional) True to force a refresh. + /// An asynchronous result that yields a list of. + private async Task<(FhirCiQaRecord[] qas, Dictionary> qasByPackageId)> getQAs(bool forceRefresh = false) + { + // check for having a cached copy and configuration to never refresh the cache + if (!forceRefresh && (_listingInvalidationSeconds == -1) && (_qas.Length != 0)) + { + // return what we have + return (_qas, _qasByPackageId); + } + + // check for having a cached copy and not needing to refresh + if (!forceRefresh && + (_listingInvalidationSeconds > 0) && + (_qasLastUpdated.AddSeconds(_listingInvalidationSeconds) >= DateTimeOffset.Now)) + { + // return what we have + return (_qas, _qasByPackageId); + } + + List? updatedGuideQas = await downloadGuideQAs(); + List? updatedCoreQas = await downloadCoreQAs(); + + // join our sets together + FhirCiQaRecord[] updatedQAs = (updatedCoreQas ?? Enumerable.Empty()).Concat(updatedGuideQas ?? Enumerable.Empty()).ToArray(); + if (updatedQAs.Length == 0) + { + return (_qas, _qasByPackageId); + } + + Dictionary> qasByPackageId = new(); + + // iterate over the QAS records and add them to a dictionary + foreach (FhirCiQaRecord qas in updatedQAs) + { + if (qas.PackageId == null) + { + continue; + } + + if (!qasByPackageId.TryGetValue(qas.PackageId, out List? qasRecs)) + { + qasRecs = new List(); + qasByPackageId.Add(qas.PackageId, qasRecs); + } + + qasRecs.Add(qas); + } + + // update our cache if necessary + if (forceRefresh || (_listingInvalidationSeconds != 0)) + { + _qas = updatedQAs; + _qasByPackageId = qasByPackageId; + _qasLastUpdated = DateTimeOffset.Now; + + // return our updated version + return (_qas, _qasByPackageId); + } + + // return what we downloaded + return (updatedQAs, qasByPackageId); + } + + /// + /// Downloads the current qas.json file from the build server and deserializes into an array of . + /// + /// An array of FhirCiQaRecord objects representing the current FhirCiQaRecords. + private async Task?> downloadGuideQAs() + { + // download the QA records from the build server + HttpRequestMessage request = new HttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = _qasUri, + Headers = + { + Accept = + { + new MediaTypeWithQualityHeaderValue("application/json"), + }, + }, + }; + + HttpResponseMessage response = await _httpClient.SendAsync(request); + System.Net.HttpStatusCode statusCode = response.StatusCode; + + if (statusCode != System.Net.HttpStatusCode.OK) + { + return null; + } + + string json = await response.Content.ReadAsStringAsync(); + if (string.IsNullOrEmpty(json)) + { + return null; + } + + List? qas = JsonConvert.DeserializeObject>(json); + + // if we do not have records, we are done + if (qas == null) + { + return null; + } + + return qas; + } + + private async Task> downloadCoreQAs() + { + List qas = new(); + + // download the branch list from the core build + HttpRequestMessage request = new HttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = _ciBranchUri, + Headers = + { + Accept = + { + new MediaTypeWithQualityHeaderValue("application/json"), + }, + }, + }; + + HttpResponseMessage response = await _httpClient.SendAsync(request); + System.Net.HttpStatusCode statusCode = response.StatusCode; + + if (statusCode != System.Net.HttpStatusCode.OK) + { + return qas; + } + + string json = await response.Content.ReadAsStringAsync(); + if (string.IsNullOrEmpty(json)) + { + return qas; + } + + CiBranchRecord[]? coreBranches = JsonConvert.DeserializeObject(json); + + if (coreBranches == null) + { + return qas; + } + + // traverse the core branches to build their records + foreach (CiBranchRecord ciBranchRec in coreBranches) + { + if ((ciBranchRec.Name == null) || + string.IsNullOrEmpty(ciBranchRec.Name) || + string.IsNullOrEmpty(ciBranchRec.Url)) + { + continue; + } + + // download the version.info for this branch + request = new HttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(_ciBranchUri, ciBranchRec.Url + "version.info"), + Headers = + { + Accept = + { + new MediaTypeWithQualityHeaderValue("text/plain"), + }, + }, + }; + + response = await _httpClient.SendAsync(request); + statusCode = response.StatusCode; + + if (statusCode != System.Net.HttpStatusCode.OK) + { + continue; + } + + string contents = await response.Content.ReadAsStringAsync(); + + // grab the contents we can out of the version.info file + parseVersionInfoIni( + contents, + out string ciFhirVersion, + out string ciVersion, + out string ciBuildId, + out DateTimeOffset? ciBuildDate); + + string packageId = $"hl7.fhir.r{ciFhirVersion.Split('.').First()}.core"; + + if (ciBranchRec.Name == "master/") + { + // build a QA record for the main branch + qas.Add(new() + { + Url = "https://build.fhir.org", + Name = "FHIR Core " + ciFhirVersion, + Title = "FHIR Core build", + Status = "draft", + PackageId = packageId, + PackageVersion = ciVersion, + BuildDate = ciBuildDate, + BuildDateIso = ciBuildDate, + FhirVersion = ciFhirVersion, + RepositoryUrl = "HL7/fhir/branches/master/qa.json" + }); + } + else + { + // build a QA record for this branch + qas.Add(new() + { + Url = "https://build.fhir.org/branches/" + ciBranchRec.Name.Substring(0, ciBranchRec.Name.Length - 1), + Name = "FHIR Core " + ciFhirVersion + " branch: " + ciBranchRec.Name, + Title = "FHIR Core build", + Status = "draft", + PackageId = packageId, + PackageVersion = ciVersion, + BuildDate = ciBuildDate, + BuildDateIso = ciBuildDate, + FhirVersion = ciFhirVersion, + RepositoryUrl = "HL7/fhir/branches/master/qa.json" + }); + } + } + + return qas; + } + + + /// Gets local version information. + /// Thrown when the requested file is not present. + /// The contents. + /// [out] The FHIR version. + /// [out] The version string (e.g., 4.0.1). + /// [out] Identifier for the build. + /// [out] The build date. + private static void parseVersionInfoIni( + string contents, + out string fhirVersion, + out string version, + out string buildId, + out DateTimeOffset? buildDate) + { + IEnumerable lines = contents.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); + + fhirVersion = string.Empty; + version = string.Empty; + buildId = string.Empty; + buildDate = null; + + foreach (string line in lines) + { + if (!line.Contains('=')) + { + continue; + } + + string[] kvp = line.Split('='); + + if (kvp.Length != 2) + { + continue; + } + + switch (kvp[0]) + { + case "FhirVersion": + fhirVersion = kvp[1]; + break; + + case "version": + version = kvp[1]; + break; + + case "buildId": + buildId = kvp[1]; + break; + + case "date": + { + if (DateTimeOffset.TryParseExact( + kvp[1], + "yyyyMMddHHmmss", + CultureInfo.InvariantCulture.DateTimeFormat, + DateTimeStyles.None, out DateTimeOffset dto)) + { + buildDate = dto; + } + } + break; + } + } + } + + /// + /// Builds the version string for a FhirCiQaRecord. + /// + /// The FhirCiQaRecord. + /// The version string. + private static string buildVersionString(FhirCiQaRecord qa) + { + string versionPrerelease = qa.PackageVersion?.Contains('-') ?? false + ? string.Empty + : "-cibuild"; + + // prefer the date of the build as the build metadata + string? buildMeta = qa.BuildDateIso?.ToUniversalTime().ToString(_ciVersionDateFormat) + ?? qa.BuildDate?.ToUniversalTime().ToString(_ciVersionDateFormat); + + // if we do not have a date, mangle the branch info + if (buildMeta == null) + { + (string? branchName, bool isDefaultBranch) = GetBranchNameRepoLiteral(qa.RepositoryUrl); + + versionPrerelease = isDefaultBranch || string.IsNullOrEmpty(branchName) + ? versionPrerelease + : (versionPrerelease + _ciBranchDelimiter + cleanForSemVer(branchName!)); + + // add the repo portions to the version + string[] repoComponents = qa.RepositoryUrl?.Split('/') ?? Array.Empty(); + if (repoComponents.Length > 2) + { + buildMeta = repoComponents[0] + "." + repoComponents[1]; + } + else + { + buildMeta = "ci"; + } + } + + string packageVersion = qa.PackageVersion ?? "0.0.0"; + + return $"{packageVersion}{versionPrerelease}+{cleanForSemVer(buildMeta)}"; + } + + /// + /// Generates a from a list of . + /// + /// The list of . + /// The generated . + private PackageListing? listingFromQaRecs(List qaRecs) + { + PackageListing? listing = null; + + // iterate over the records in our dictionary - sort by status so that 'active' comes before 'retired' + foreach (FhirCiQaRecord qa in qaRecs.OrderBy(qa => qa.Status)) + { + listing ??= new() + { + Id = qa.PackageId, + Name = qa.Name, + Description = $"CI Build of {qa.PackageId}", + DistTags = null, + Versions = new(), + }; + + int igUrlIndex = qa.Url?.IndexOf("/ImplementationGuide/", StringComparison.Ordinal) ?? -1; + string? qaUrl = igUrlIndex == -1 ? qa.Url : qa.Url!.Substring(0, igUrlIndex); + + string versionLiteral = buildVersionString(qa); + + if (listing.Versions!.ContainsKey(versionLiteral)) + { + continue; + } + + listing.Versions.Add(versionLiteral, new() + { + Name = qa.PackageId, + Version = versionLiteral, + Description = qa.Description ?? qa.Title ?? qa.RepositoryUrl, + FhirVersion = qa.FhirVersion, + Url = qaUrl, + Dist = new() + { + Tarball = qaUrl, + }, + }); + } + + if ((listing?.DistTags == null) && (listing?.Versions?.Count > 0)) + { + listing.DistTags ??= new(); + + foreach (FhirCiQaRecord qa in qaRecs.OrderBy(qa => qa.BuildDateIso ?? qa.BuildDate)) + { + (string? branchName, bool isDefaultBranch) = GetBranchNameRepoLiteral(qa.RepositoryUrl); + + string tag = branchName == null + ? "current" + : "current$" + branchName; + + string versionLiteral = buildVersionString(qa); + + // add the full tag if we have it + if (!listing.DistTags.ContainsKey(tag)) + { + listing.DistTags.Add(tag, versionLiteral); + } + + // check for default branches to add them as well + if (isDefaultBranch && + (!listing.DistTags.ContainsKey("current"))) + { + listing.DistTags.Add("current", versionLiteral); + } + } + } + + return listing; + } + + /// + /// Retrieve a package listing for a Package + /// + /// + /// Note that is function creates package listing records based on QA records from the build server. + /// + /// Name of the package. + /// Package listing. + public async ValueTask DownloadListingAsync(string pkgname) + { + (FhirCiQaRecord[] _, Dictionary> qasByPackageId) = await getQAs(); + + if (string.IsNullOrEmpty(pkgname) || + !qasByPackageId.TryGetValue(pkgname, out List? qaRecs) || + qaRecs.Count == 0) + { + return null; + } + + return listingFromQaRecs(qaRecs); + } + + /// + /// Extracts the branch name from a repository URL. + /// + /// The partial repository URL. + /// The branch name extracted from the repository URL, or null if the branch name cannot be determined. + public static (string? branchName, bool isDefaultBranch) GetBranchNameRepoLiteral(string? partialRepoLiteral) + { + if ((partialRepoLiteral == null) || + string.IsNullOrEmpty(partialRepoLiteral)) + { + return (null, false); + } + + int branchStart = partialRepoLiteral.IndexOf("branches/") + 9; + + if (branchStart == -1) + { + branchStart = partialRepoLiteral.IndexOf("tree/") + 5; + } + + if (branchStart == -1) + { + return (null, false); + } + + int branchEnd = partialRepoLiteral.IndexOf('/', branchStart); + + string branchName = branchEnd == -1 + ? partialRepoLiteral.Substring(branchStart) + : partialRepoLiteral.Substring(branchStart, branchEnd - branchStart); + + return (branchName, _defaultBranchNames.Contains(branchName)); + } + + /// Get a list of package catalogs, based on optional parameters. + /// (Optional) Name of the package. + /// (Optional) The FHIR version of a package. + /// (Optional) URL of the site. + /// (Optional) The repository. + /// (Optional) The branch. + /// (Optional) Allow for prelease packages. + /// A list of package catalogs that conform to the parameters. + public async ValueTask> CatalogPackagesAsync( + string? pkgname = null, + string? fhirversion = null, + string? site = null, + string? repo = null, + string? branch = null, + bool preview = true) + { + List entries = new(); + + (FhirCiQaRecord[] qas, Dictionary> qasByPackageId) = await getQAs(); + + HashSet usedIds = new(); + + // remove any trailing slashes from the site URL - QAs.json does not have them + if ((site != null) && site.EndsWith("/")) + { + site = site.Substring(0, site.Length - 1); + } + + // sanitize any repository URL - QAs.json does not repeat the GitHub URL portion + if ((repo != null) && repo.StartsWith("http://github.com/")) + { + repo = repo.Substring(18); + } + else if ((repo != null) && repo.StartsWith("https://github.com/")) + { + repo = repo.Substring(19); + } + + if ((branch != null) && (!branch.Contains('/'))) + { + branch = "/branches/" + branch + "/qa.json"; + } + + // if there was a package name provided, we can use our dictionary for lookup + if (pkgname != null) + { + if (!qasByPackageId.TryGetValue(pkgname, out List? qasRecs)) + { + return entries; + } + + // iterate over the records in our dictionary + foreach (FhirCiQaRecord qa in qasRecs) + { + // skip anything we have already added - duplicates are likely forks but we lose that granularity here + if (usedIds.Contains(qa.PackageId ?? string.Empty)) + { + continue; + } + + if ((fhirversion != null) && (qa.FhirVersion != fhirversion)) + { + continue; + } + + if ((site != null) && (qa.Url != site)) + { + continue; + } + + if ((repo != null) && (!qa.RepositoryUrl?.StartsWith(repo) ?? false)) + { + continue; + } + + if ((branch != null) && (!qa.RepositoryUrl?.EndsWith(branch) ?? false)) + { + continue; + } + + // only use this package if it passes all the other filters + usedIds.Add(qa.PackageId ?? string.Empty); + entries.Add(new PackageCatalogEntry() + { + Name = qa.PackageId, + Description = qa.RepositoryUrl, + FhirVersion = qa.FhirVersion, + }); + } + + return entries; + } + + // iterate over all the QA records + foreach (FhirCiQaRecord qa in qas) + { + // skip anything we have already added - duplicates are likely forks but we lose that granularity here + if (usedIds.Contains(qa.PackageId ?? string.Empty)) + { + continue; + } + + if ((fhirversion != null) && (qa.FhirVersion != fhirversion)) + { + continue; + } + + if ((site != null) && (qa.Url != site)) + { + continue; + } + + if ((repo != null) && (!qa.RepositoryUrl?.StartsWith(repo) ?? false)) + { + continue; + } + + if ((branch != null) && (!qa.RepositoryUrl?.EndsWith(branch) ?? false)) + { + continue; + } + + // only use this package if it passes all the other filters + usedIds.Add(qa.PackageId ?? string.Empty); + entries.Add(new PackageCatalogEntry() + { + Name = qa.PackageId, + Description = qa.RepositoryUrl, + FhirVersion = qa.FhirVersion, + }); + } + + return entries; + } + + /// + /// Cleans the input string to be usable in a semantic versioning component. + /// + /// The input string to be cleaned. + /// The cleaned string for semantic versioning. + private static string cleanForSemVer(string value) + { + List clean = new(value.Length); + + foreach (char c in value) + { + if (char.IsLetterOrDigit(c)) + { + clean.Add(c); + continue; + } + + clean.Add('-'); + } + + return new string(clean.ToArray()); + } + + /// + /// Retrieves the FhirCiQaRecord for the specified package name and version or tag. + /// + /// The name of the package. + /// The version, tag, or branch of the package. + /// The FhirCiQaRecord for the specified package name and version or tag, or null if not found. + private async ValueTask getQaRecord(string? name, string? versionDiscriminator) + { + if (string.IsNullOrEmpty(name)) + { + return null; + } + + (FhirCiQaRecord[] _, Dictionary> qasByPackageId) = await getQAs(); + + if (!qasByPackageId.TryGetValue(name!, out List? qaRecs)) + { + return null; + } + + // no version resolves to the 'current' tag by default + string requestedVersion = versionDiscriminator switch + { + null => "current", + _ => versionDiscriminator, + }; + + // check for using a tag and resolve it to a version + if (!requestedVersion.Contains('+')) + { + PackageListing? listing = listingFromQaRecs(qaRecs); + + if (listing != null) + { + // check for a tag + if ((listing.DistTags?.TryGetValue(requestedVersion, out string? tagVersion) == true) && + (listing.Versions?.TryGetValue(tagVersion, out PackageRelease? _) == true)) + { + requestedVersion = tagVersion; + } + // check for a branch name + else if ((listing.DistTags?.TryGetValue("current$" + requestedVersion, out tagVersion) == true) && + (listing.Versions?.TryGetValue(tagVersion, out PackageRelease? _) == true)) + { + requestedVersion = tagVersion; + } + } + } + + string[] rvComponents = requestedVersion.Split('+'); + + if ((rvComponents.Length > 1) && + DateTimeOffset.TryParseExact( + rvComponents.Last(), + _ciVersionDateFormat, + CultureInfo.InvariantCulture.DateTimeFormat, + DateTimeStyles.None, out DateTimeOffset requestedDto)) + { + // traverse the records in the package looking for a match + foreach (FhirCiQaRecord qa in qaRecs) + { + if ((qa.BuildDateIso == requestedDto) || + (qa.BuildDate == requestedDto)) + { + return qa; + } + } + } + + return null; + } + + /// + /// Downloads a package from the source. + /// + /// Package reference of the package to be downloaded. + /// Package content as a byte array. + private async ValueTask downloadPackage(PackageReference reference, FhirCiQaRecord? qa = null) + { + if (reference.Scope != FhirCiScope) + { + throw new Exception($"CI packages must be tagged with the Scope: {FhirCiScope}!"); + } + + // find our package in the QA listings + qa ??= await getQaRecord(reference.Name, reference.Version); + + if (qa == null) + { + throw new Exception($"Cannot resolve {reference.Moniker} in CI Build QA records!"); + } + + // extract the branch name + (string? branchName, bool _) = GetBranchNameRepoLiteral(qa.RepositoryUrl); + + // build the URL + int igUrlIndex = qa.RepositoryUrl?.IndexOf("/qa.json", StringComparison.Ordinal) ?? -1; + string url = igUrlIndex == -1 ? qa.RepositoryUrl! : qa.RepositoryUrl!.Substring(0, igUrlIndex); + + url += url.EndsWith('/') + ? "package.tgz" + : "/package.tgz"; + + if (!url.StartsWith("http")) + { + if (url.StartsWith("HL7/fhir/", StringComparison.OrdinalIgnoreCase)) + { + url = "https://build.fhir.org/" + url; + } + else + { + url = "https://build.fhir.org/ig/" + url; + } + } + + // download data + return await _httpClient.GetByteArrayAsync(url).ConfigureAwait(false); + } + + /// + /// Retrieves a tag-based package reference based on the provided name and version or tag. + /// + /// The name of the package. + /// The version, tag, or branch of the package. + /// (Optional) The FhirCiQaRecord. + /// The package reference. + public async ValueTask GetTagBasedReference(string? name, string? versionDiscriminator, FhirCiQaRecord? qa = null) + { + // find our package in the QA listings + qa ??= await getQaRecord(name, versionDiscriminator); + + if (qa == null) + { + return PackageReference.None; + } + + (string? branchName, bool isDefaultBranch) = GetBranchNameRepoLiteral(qa.RepositoryUrl); + + string tag; + if ((branchName == null) || + (isDefaultBranch && (!versionDiscriminator?.Contains(branchName) ?? true))) + { + tag = "current"; + } + else + { + tag = "current$" + branchName; + } + + return new PackageReference(FhirCiScope, qa.PackageId ?? name!, tag); + } + + /// + /// Retrieves the resolved-version reference for a package based on its name and version or tag. + /// + /// The name of the package. + /// The version, tag, or branch of the package. + /// (Optional) The FhirCiQaRecord. + /// The resolved package reference. + public async ValueTask GetResolvedReference(string? name, string? versionDiscriminator, FhirCiQaRecord? qa = null) + { + // find our package in the QA listings + qa ??= await getQaRecord(name, versionDiscriminator); + + if (qa == null) + { + return PackageReference.None; + } + + string version = buildVersionString(qa); + + return new PackageReference(FhirCiScope, qa.PackageId ?? name!, version); + } + + /// + /// Retrieves the tagged and resolved package references for a given package name and version or tag. + /// + /// The name of the package. + /// The version, tag, or branch of the package. + /// A tuple containing the tagged and resolved package references. + public async ValueTask<(PackageReference tagged, PackageReference resolved)> GetReferences(string name, string? versionDiscriminator) + { + // find our package in the QA listings + FhirCiQaRecord? qa = await getQaRecord(name, versionDiscriminator); + + return (await GetTagBasedReference(name, versionDiscriminator, qa), await GetResolvedReference(name, versionDiscriminator, qa)); + } + + /// + /// Retrieve a list of all versions of a package id on the build server. + /// + /// Package name. + /// List of versions. + public async Task GetVersions(string name) + { + PackageListing? listing = await DownloadListingAsync(name); + if (listing == null) + { + return new(); + } + + Versions v = listing.ToVersions(); + + if (v.Items.Count != listing.Versions!.Count) + { + throw new Exception("Version count mismatch!"); + } + + return v; + + //return listing is null ? new Versions() : listing.ToVersions(); + } + + /// + /// Download a package from the source. + /// + /// Package reference of the package to be downloaded. + /// Package content as byte array. + public async Task GetPackage(PackageReference reference) + { + return await downloadPackage(reference); + } + + /// + /// Checks if a package is outdated by comparing its build date with the build date from the QA listings. + /// + /// The package reference. + /// The package cache. + /// True if the package is outdated, false if it is up to date, or null if the package is not found in the QA listings. + public async Task IsPackageOutdated(PackageReference reference, IPackageCache cache) + { + if (cache is not DiskPackageCache diskCache) + { + throw new Exception("FhirCiClient requires a DiskPackageCache for IsPackageOutdated"); + } + + // find our package in the QA listings + FhirCiQaRecord? qa = await getQaRecord(reference.Name, reference.Version); + + if (qa == null) + { + return null; + } + + // make sure we have the tag and resolved version references for comparison + PackageReference taggedReference = await GetTagBasedReference(reference.Name, reference.Version, qa); + PackageReference resolvedReference = await GetResolvedReference(reference.Name, reference.Version, qa); + + // if either fails, we cannot proceed + if ((taggedReference == PackageReference.None) || + (resolvedReference == PackageReference.None)) + { + return null; + } + + // check for a local copy - CI builds are installed by tag + bool isInstalled = await cache.IsInstalled(taggedReference); + + if (!isInstalled) + { + return true; + } + + // get the manifest from the local copy + PackageManifest? installedManifest = await diskCache.ReadManifestEx(taggedReference); + + if (installedManifest == null) + { + return true; + } + + // compare the build date from QA and the installed manifest + return (installedManifest.Date ?? DateTimeOffset.MinValue) < (qa.BuildDateIso ?? qa.BuildDate ?? DateTimeOffset.MaxValue); + } + + /// + /// Installs or updates a package from the FHIR CI server. + /// + /// The package reference to install or update. + /// The package cache. + public async Task InstallOrUpdate(PackageReference reference, _ForPackages.DiskPackageCache cache) + { + if (cache is not DiskPackageCache diskCache) + { + throw new Exception("FhirCiClient requires a DiskPackageCache for IsPackageOutdated"); + } + + // find our package in the QA listings + FhirCiQaRecord? qa = await getQaRecord(reference.Name, reference.Version); + + if (qa == null) + { + throw new Exception($"Could not resolve {reference.Moniker} on the CI server"); + } + + // make sure we have the tag and resolved version references for comparison + PackageReference taggedReference = await GetTagBasedReference(reference.Name, reference.Version, qa); + PackageReference resolvedReference = await GetResolvedReference(reference.Name, reference.Version, qa); + + // if either fails, we cannot proceed + if ((taggedReference == PackageReference.None) || + (resolvedReference == PackageReference.None)) + { + throw new Exception($"Could not resolve version information for {reference.Moniker} on the CI server"); + } + + // check for a local copy - CI builds are installed by tag + bool isInstalled = await cache.IsInstalled(taggedReference); + + // get the manifest from the local copy + PackageManifest? installedManifest = isInstalled ? await diskCache.ReadManifestEx(taggedReference) : null; + + // compare the build date from QA and the installed manifest + bool shouldDownload = (installedManifest?.Date ?? DateTimeOffset.MinValue) < (qa.BuildDateIso ?? qa.BuildDate ?? DateTimeOffset.MaxValue); + + if (!shouldDownload) + { + return; + } + + if (isInstalled) + { + // need to delete the existing content + await cache.Delete(taggedReference); + } + + // download happens via the resolved version + byte[] data = await downloadPackage(resolvedReference, qa); + + // install happens via the tagged version + await cache.Install(taggedReference, data); + } + + #region IDisposable + + private bool _disposed; + + /// + void IDisposable.Dispose() => Dispose(true); + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// True to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // [WMR 20181102] HttpClient will dispose internal HttpClientHandler/WebRequestHandler + _httpClient?.Dispose(); + } + + // release any unmanaged objects + // set the object references to null + _disposed = true; + } + } + + #endregion + } +} + +#nullable restore diff --git a/src/fhir-candle/_ForPackages/JsonModels.cs b/src/fhir-candle/_ForPackages/JsonModels.cs new file mode 100644 index 0000000..9cbe707 --- /dev/null +++ b/src/fhir-candle/_ForPackages/JsonModels.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Firely.Fhir.Packages; +using Newtonsoft.Json; + +namespace fhir.candle._ForPackages +{ + public class PackageManifest + { + /// + /// Initialize a new package manifest + /// + /// Package name + /// Version of the package + [JsonConstructor] + public PackageManifest(string name, string version) + { + Name = name; + Version = version; + } + + /// + /// The globally unique name for the package. + /// + [JsonProperty(PropertyName = "name")] + public string Name; + + /// + /// Semver-based version for the package + /// + [JsonProperty(PropertyName = "version")] + public string Version; + + /// + /// Description of the package. + /// + [JsonProperty(PropertyName = "description")] + public string? Description; + + /// + /// Author of the package. + /// + [JsonIgnore] + public string? Author + { + get => (AuthorInformation != null ? AuthorSerializer.Serialize(AuthorInformation) : null); + set + { + if (value is not null) + AuthorInformation = AuthorSerializer.Deserialize(value); + + } + } + + [JsonConverter(typeof(AuthorJsonConverter))] + [JsonProperty(PropertyName = "author")] + public AuthorInfo? AuthorInformation; + + /// + /// Other packages that the contents of this packages depend on. + /// + /// + /// Keys are package names, values are package versions + /// + [JsonProperty(PropertyName = "dependencies")] + public Dictionary? Dependencies; + + /// + /// Other packages necessary during development of this package. + /// + [JsonProperty(PropertyName = "devDependencies")] + public Dictionary? DevDependencies; + + /// + /// List of keywords to help with discovery. + /// + [JsonProperty(PropertyName = "keywords")] + public List? Keywords; + + /// + /// SPDX-convention license name. + /// + [JsonProperty(PropertyName = "license")] + public string? License; + + /// + /// The url to the project homepage. + /// + [JsonProperty(PropertyName = "homepage")] + public string? Homepage; + + /// + /// Describes the structure of the package. + /// + /// Some of the common keys used are defined in . + [JsonProperty(PropertyName = "directories")] + public Dictionary? Directories; + + /// + /// String-based keys used in the dictionary. + /// + /// + /// Known directory keys are: + /// - lib: contains the package's definitions + /// - bin: contains any executables the package provides + /// - man: contains any man pages the package provides + /// - doc: contains documentation for the package + /// - example: contains example resource instances for the package + /// - test: contains test resources for the package. + /// Note that this list does NOT include: + /// - xml: XML schema definitions for the package (.sch) + /// - openapi: OpenAPI definitions for the package (.json) + /// - rdf: RDF definitions for the package (.ttl) + /// TODO: not documented at + /// https://confluence.hl7.org/pages/viewpage.action?pageId=35718629#NPMPackageSpecification-Packagemanifest. + /// + public class DirectoryKeys + { + /// + /// Where the bulk of the library is. + /// + public const string DIRECTORY_KEY_LIB = "lib"; + public const string DIRECTORY_KEY_BIN = "bin"; + public const string DIRECTORY_KEY_MAN = "man"; + public const string DIRECTORY_KEY_DOC = "doc"; + public const string DIRECTORY_KEY_EXAMPLE = "example"; + public const string DIRECTORY_KEY_TEST = "test"; + } + + /// + /// Title for the package. + /// + [JsonProperty(PropertyName = "title")] + public string? Title; + + /// + /// Versions of the FHIR standard used in artifacts within this package. + /// + /// Largely obsolete, and replaced by actual dependencies on the + /// core packages. + [JsonProperty(PropertyName = "fhirVersions")] + public List? FhirVersions; + + /// + /// Versions of the FHIR standard used in artifacts within this package. + /// + /// It seems this is mistakenly generated in the core packages + /// published by HL7 and should be the same as above. + [JsonProperty(PropertyName = "fhir-version-list")] + public List? FhirVersionList; + + /// Gets the FHIR versions from FhirVersions, FhirVersionList, or dependencies. + [JsonIgnore] + public List? AnyFhirVersions => + FhirVersions?.Count > 0 + ? FhirVersions + : FhirVersionList?.Count > 0 + ? FhirVersionList + : VersionExtensions.FhirVersionsFromPackages(Dependencies); + + /// + /// An optional value to indicate the type of package generated by the IG build tool + /// + /// + /// It's fairly random, so please do depend on it. Also note that the HL7 IG build tool + /// creates template and tool packages and publishes them as a FHIR package + /// even though they have little to do with FHIR packages. + /// Known types are (case-insensitive): + /// - Conformance: a set of Conformance Resources in the base package folder (/package) + /// - IG: a FHIR implementation guide package(has an ImplementationGuide resource in /package, + /// along with conformance resources, and also contains example resources in + /// /package/example) + /// - Core: contains the conformance related resources for the main FHIR + /// specification(effectively, this is a special type of "conformance" that marks it as + /// the core specification, which could also be inferred from it's name such as + /// hl7.fhir.r4.core, but other branches / ballots etc may vary, so this is simpler than + /// inferring from the name) + /// - fhir.core: older format of 'core' package (deprecated) + /// - Examples : contains the example resources found in the main FHIR specification in /package. + /// - Group: a package that only includes (e.g.depends on) other packages (won't contain FHIR + /// resources directly). The versions listed in this package must include all the + /// versions found in the included packages. Note that this is is used for the set of + /// packages that represent a full core specification + /// - Tool: A package that contains tool specific files to support specific tools (won't contain + /// FHIR resources or specify a FHIR version) + /// - IG-Template: an IG template for use by IG publishing tools (won't contain FHIR resources or + /// specify a version) + /// + [JsonProperty(PropertyName = "type")] + public string? Type; + + public class Maintainer + { + [JsonProperty(PropertyName = "name")] + public string? Name; + + [JsonProperty(PropertyName = "email")] + public string? Email; + + [JsonProperty(PropertyName = "url")] + public string? Url; + } + + /// + /// List of individual(s) responsible for maintaining the package. + /// + [JsonProperty(PropertyName = "maintainers")] + public List? Maintainers; + + /// + /// For IG packages: The canonical url of the IG (equivalent to ImplementationGuide.url). + /// + [JsonProperty(PropertyName = "canonical")] + public string? Canonical; + + /// + /// For IG packages: Where the human readable representation (e.g. IG) is published on the web. + /// + [JsonProperty(PropertyName = "url")] + public string? Url; + + /// + /// Country code for the jurisdiction under which this package is published. + /// + /// + /// Formatted as an urn specifying the code system and code, e.g. "urn:iso:std:iso:3166#US". + /// Typically from CommonJurisdictionCodes (http://hl7.org/fhir/ValueSet/jurisdiction-common) from the + /// fhir.tx.support.* package (fhir.tx.support.r3, fhir.tx.support.r4, ...) + /// + [JsonProperty(PropertyName = "jurisdiction")] + public string? Jurisdiction; + + /// The date the package was published. + /// TODO: not documented at https://confluence.hl7.org/pages/viewpage.action?pageId=35718629#NPMPackageSpecification-Packagemanifest + [JsonConverter(typeof(ManifestDateJsonConverter))] + [JsonProperty(PropertyName = "date")] + public DateTimeOffset? Date; + } + + + public class AuthorInfo : Firely.Fhir.Packages.AuthorInfo + { + /// + /// The npm specification allows author information to be serialized in json as a single string, or as a complex object. + /// This boolean keeps track of it was parsed from either one, so it can be serialized to the same output again. + /// + /// See issue: https://github.com/FirelyTeam/Firely.Fhir.Packages/issues/94 + [JsonIgnore] + internal bool ParsedFromString = false; + } + + /// + /// Parse AuthorInfo object based on the following pattern "name (url)" + /// + internal static class AuthorSerializer + { + + private const char EMAIL_START_CHAR = '<'; + private const char EMAIL_END_CHAR = '>'; + private const char URL_START_CHAR = '('; + private const char URL_END_CHAR = ')'; + + internal static AuthorInfo Deserialize(string authorString) + { + var authorInfo = new AuthorInfo(); + + // Extract name + authorInfo.Name = getName(authorString); + + // Extract email + authorInfo.Email = getStringBetweenCharacters(EMAIL_START_CHAR, EMAIL_END_CHAR, authorString); + + // Extract Url + authorInfo.Url = getStringBetweenCharacters(URL_START_CHAR, URL_END_CHAR, authorString); + + //If author was set using parsing of a string, we will think it should be deserialized as a string too. + authorInfo.ParsedFromString = true; + + return authorInfo; + } + + internal static string Serialize(AuthorInfo authorInfo) + { + var builder = new StringBuilder(); + if (authorInfo.Name != null) + { + builder.Append(authorInfo.Name); + } + if (authorInfo.Email != null) + { + builder.Append($" {EMAIL_START_CHAR}{authorInfo.Email}{EMAIL_END_CHAR}"); + } + if (authorInfo.Url != null) + { + builder.Append($" {URL_START_CHAR}{authorInfo.Url}{URL_END_CHAR}"); + } + return builder.ToString().TrimStart(); + } + + private static string? getStringBetweenCharacters(char start, char end, string input) + { + // Extract email + var urlStartIndex = input.IndexOf(start); + if (urlStartIndex != -1) + { + var urlEndIndex = input.IndexOf(end, urlStartIndex); + if (urlEndIndex != -1) + { + return input.Substring(urlStartIndex + 1, urlEndIndex - urlStartIndex - 1).Trim(); + } + } + return null; + } + + private static string? getName(string input) + { + if (input[0] == EMAIL_START_CHAR || input[0] == URL_START_CHAR) + return null; + + var nameStartIndex = 0; + + var nameEndIndex = input.IndexOf(EMAIL_START_CHAR, nameStartIndex); + if (nameEndIndex != -1) + { + return input.Substring(nameStartIndex, nameEndIndex - nameStartIndex).Trim(); + } + else + { + nameEndIndex = input.IndexOf(URL_START_CHAR, nameStartIndex); + return nameEndIndex != -1 + ? input.Substring(nameStartIndex, nameEndIndex - nameStartIndex).Trim() + : input.Substring(nameStartIndex).Trim(); + } + } + + } + + + /// Information about a CI branch, as returned from a branch query to the server. + public class CiBranchRecord + { + /// The relative name for this record. + [JsonProperty(PropertyName = "name")] + public string? Name; + + /// The size of the directory or file. + [JsonProperty(PropertyName = "size")] + public long? Size; + + /// URL of the resource, relative to the current URL. + [JsonProperty(PropertyName = "url")] + public string? Url; + + /// The file/directory mode. + /// This looks like a flag, but I cannot find documentation on values. + [JsonProperty(PropertyName = "mode")] + public long? ModeFlag; + + /// True if is directory, false if not. + [JsonProperty(PropertyName = "is_dir")] + public bool? IsDirectory; + + /// True if is symbolic link, false if not. + [JsonProperty(PropertyName = "is_symlink")] + public bool? IsSymbolicLink; + } + + /// FHIR QA record from the CI server. + public class FhirCiQaRecord + { + [JsonProperty(PropertyName = "url")] + public string? Url { get; set; } + + [JsonProperty(PropertyName = "name")] + public string? Name { get; set; } + + [JsonProperty(PropertyName = "title")] + public string? Title { get; set; } + + [JsonProperty(PropertyName = "description")] + public string? Description { get; set; } + + [JsonProperty(PropertyName = "status")] + public string? Status { get; set; } + + [JsonProperty(PropertyName = "package-id")] + public string? PackageId { get; set; } + + [JsonProperty(PropertyName = "ig-ver")] + public string? PackageVersion { get; set; } + + [JsonProperty(PropertyName = "date")] + public DateTimeOffset? BuildDate { get; set; } + + [JsonProperty(PropertyName = "dateISO8601")] + public DateTimeOffset? BuildDateIso { get; set; } + + [JsonProperty(PropertyName = "errs")] + public int? ErrorCount { get; set; } + + [JsonProperty(PropertyName = "warnings")] + public int? WarningCount { get; set; } + + [JsonProperty(PropertyName = "hints")] + public int? HintCount { get; set; } + + [JsonProperty(PropertyName = "suppressed-hints")] + public int? SuppressedHintCount { get; set; } + + [JsonProperty(PropertyName = "suppressed-warnings")] + public int? SuppressedWarningCount { get; set; } + + [JsonProperty(PropertyName = "version")] + public string? FhirVersion { get; set; } + + [JsonProperty(PropertyName = "tool")] + public string? ToolingVersion { get; set; } + + [JsonProperty(PropertyName = "maxMemory")] + public long? MaxMemoryUsedToBuild { get; set; } + + [JsonProperty(PropertyName = "repo")] + public string? RepositoryUrl { get; set; } + } +} diff --git a/src/fhir-candle/_ForPackages/ManifestDateJsonConverter.cs b/src/fhir-candle/_ForPackages/ManifestDateJsonConverter.cs new file mode 100644 index 0000000..f18a8cb --- /dev/null +++ b/src/fhir-candle/_ForPackages/ManifestDateJsonConverter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Newtonsoft.Json; + +namespace fhir.candle._ForPackages +{ + internal class ManifestDateJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(DateTimeOffset)) || + (objectType == typeof(string)); + } + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + string? dateString = reader.Value?.ToString(); + + if (string.IsNullOrEmpty(dateString)) + { + return null; + } + + if (DateTimeOffset.TryParseExact( + dateString, + "yyyyMMddHHmmss", + CultureInfo.InvariantCulture.DateTimeFormat, + DateTimeStyles.None, out DateTimeOffset dto)) + { + return dto; + } + + return null; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value is DateTimeOffset dto) + { + serializer.Serialize(writer, dto.ToUniversalTime().ToString("yyyyMMddHHmmss")); + } + else if (value != null) + { + serializer.Serialize(writer, value); + } + } + } +} diff --git a/src/fhir-candle/_ForPackages/VersionExtensions.cs b/src/fhir-candle/_ForPackages/VersionExtensions.cs new file mode 100644 index 0000000..746e4e9 --- /dev/null +++ b/src/fhir-candle/_ForPackages/VersionExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +#if NETSTANDARD2_0 +using FhirCandle.Polyfill; +#endif + +namespace fhir.candle._ForPackages +{ + internal static class VersionExtensions + { + /// + /// Gets the regular expression for matching known core package names. + /// + /// A regular expression. + private static readonly Regex _matchCorePackageOnly = new Regex("^hl7\\.fhir\\.(r\\d+[A-Za-z]?)\\.core$", RegexOptions.Compiled); + + /// + /// Determines whether the specified package ID belongs to the FHIR core package. + /// + /// The package ID to check. + /// true if the package ID belongs to the FHIR core package; otherwise, false. + public static bool PackageIsFhirCore(string packageId) + { + return _matchCorePackageOnly.IsMatch(packageId); + } + + /// + /// Retrieves the FHIR versions from a dictionary of package IDs and versions. + /// + /// The dictionary of package IDs and versions. + /// A list of FHIR version numbers if provided (e.g., 4.0.1), R-literals if not (e.g., R4). + public static List FhirVersionsFromPackages(Dictionary? packages) + { + List fhirVersions = new(); + + if (packages == null) + { + return fhirVersions; + } + + foreach ((string packageId, string? version) in packages) + { + Match match = _matchCorePackageOnly.Match(packageId); + if (!match.Success) + { + continue; + } + + if (string.IsNullOrEmpty(version)) + { + fhirVersions.Add(match.Groups[0].Value.ToUpperInvariant()); + } + else + { + fhirVersions.Add(version!); + } + } + + return fhirVersions; + } + } +} diff --git a/src/fhir-candle/_Imports.razor b/src/fhir-candle/_Imports.razor index be3df67..6873a87 100644 --- a/src/fhir-candle/_Imports.razor +++ b/src/fhir-candle/_Imports.razor @@ -7,6 +7,7 @@ @using FhirCandle.Ui; @using FhirCandle.Models; @using FhirCandle.Storage; +@using FhirCandle.Utils; @* @using Microsoft.FluentUI.AspNetCore.Components; *@ @* @using MudBlazor; *@ @using static Microsoft.AspNetCore.Components.Web.RenderMode diff --git a/src/fhir-candle/fhir-candle.csproj b/src/fhir-candle/fhir-candle.csproj index e4f1ba3..396dea9 100644 --- a/src/fhir-candle/fhir-candle.csproj +++ b/src/fhir-candle/fhir-candle.csproj @@ -72,15 +72,16 @@ + - + - + - + From a39a0b9061738278916af5eb1386090aa63f57f2 Mon Sep 17 00:00:00 2001 From: Gino Canessa Date: Wed, 28 Aug 2024 12:53:18 -0500 Subject: [PATCH 3/5] Updated subscription variable resolution to use the added Envrionment setting from Firely instead of a custom resolver. --- .../Components/FhirEditor.razor | 7 ++- .../FhirStore.CommonVersioned.projitems | 1 - .../Models/FhirPathVariableResolver.cs | 44 ---------------- .../Storage/ResourceStore.cs | 51 +++++++------------ .../Storage/VersionedFhirStore.cs | 12 ++--- src/fhir-candle/Program.cs | 3 -- 6 files changed, 30 insertions(+), 88 deletions(-) delete mode 100644 src/FhirStore.CommonVersioned/Models/FhirPathVariableResolver.cs diff --git a/src/FhirCandle.Ui.Common/Components/FhirEditor.razor b/src/FhirCandle.Ui.Common/Components/FhirEditor.razor index b63618f..9cf06a5 100644 --- a/src/FhirCandle.Ui.Common/Components/FhirEditor.razor +++ b/src/FhirCandle.Ui.Common/Components/FhirEditor.razor @@ -11,6 +11,8 @@ OnDidInit="EditorOnDidInit"/> @code { +#pragma warning disable BL0007 + private string _language = "json"; /// Gets or sets the language. @@ -48,7 +50,6 @@ } } - private string _editorContent = ""; /// Gets or sets the editor content. @@ -71,6 +72,9 @@ /// The editor. private StandaloneCodeEditor? _editor = null; +#pragma warning restore BL0007 + + /// Resource construction options. /// The editor. /// The StandaloneEditorConstructionOptions. @@ -147,3 +151,4 @@ { } } + diff --git a/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems b/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems index ee22430..5a0b2b7 100644 --- a/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems +++ b/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems @@ -12,7 +12,6 @@ - diff --git a/src/FhirStore.CommonVersioned/Models/FhirPathVariableResolver.cs b/src/FhirStore.CommonVersioned/Models/FhirPathVariableResolver.cs deleted file mode 100644 index 343db13..0000000 --- a/src/FhirStore.CommonVersioned/Models/FhirPathVariableResolver.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// - -using Hl7.Fhir.ElementModel; - -namespace FhirCandle.Models; - -/// A FHIR path variable resolver. -public class FhirPathVariableResolver -{ - /// (Immutable) The FHIR path prefix. - public const string _fhirPathPrefix = "/_fpvar/"; - - /// (Immutable) Length of the FHIR path prefix. - private const int _fhirPathPrefixLength = 8; - - /// Gets the versioned FHIR store. - public required Func NextResolver { get; init; } - - /// Gets or initializes the variables. - public Dictionary Variables { get; init; } = new(); - - /// Resolves the given document. - /// URI of the resource. - /// An ITypedElement. - public ITypedElement Resolve(string uri) - { - if (uri.StartsWith(_fhirPathPrefix, StringComparison.Ordinal)) - { - string name = uri.Substring(_fhirPathPrefixLength); - - if (Variables.ContainsKey(name)) - { - return Variables[name]; - } - - return null!; - } - - return NextResolver(uri); - } -} diff --git a/src/FhirStore.CommonVersioned/Storage/ResourceStore.cs b/src/FhirStore.CommonVersioned/Storage/ResourceStore.cs index 82c8741..a17857f 100644 --- a/src/FhirStore.CommonVersioned/Storage/ResourceStore.cs +++ b/src/FhirStore.CommonVersioned/Storage/ResourceStore.cs @@ -1445,20 +1445,15 @@ public void TestCreateAgainstSubscriptions(T current) { ITypedElement currentTE = current.ToTypedElement(); - FhirPathVariableResolver resolver = new FhirPathVariableResolver() - { - NextResolver = _store.Resolve, - Variables = new() - { - { "current", currentTE }, - //{ "previous", Enumerable.Empty() }, - }, - }; - FhirEvaluationContext fpContext = new FhirEvaluationContext(currentTE.ToScopedNode()) { TerminologyService = _store.Terminology, - ElementResolver = resolver.Resolve, + ElementResolver = _store.Resolve, + Environment = new Dictionary>() + { + { "current", [currentTE] }, + { "previous", [] }, + }, }; PerformSubscriptionTest( @@ -1496,20 +1491,15 @@ public void TestUpdateAgainstSubscriptions(T current, T previous) ITypedElement currentTE = current.ToTypedElement(); ITypedElement previousTE = previous.ToTypedElement(); - FhirPathVariableResolver resolver = new FhirPathVariableResolver() - { - NextResolver = _store.Resolve, - Variables = new() - { - { "current", currentTE }, - { "previous", previousTE }, - }, - }; - FhirEvaluationContext fpContext = new FhirEvaluationContext(currentTE.ToScopedNode()) { TerminologyService = _store.Terminology, - ElementResolver = resolver.Resolve, + ElementResolver = _store.Resolve, + Environment = new Dictionary>() + { + { "current", [currentTE] }, + { "previous", [previousTE] }, + }, }; //string test = "meta.tag.memberOf('http://hl7.org/fhir/us/davinci-cdex/ValueSet/cdex-work-queue')"; @@ -1547,20 +1537,15 @@ public void TestDeleteAgainstSubscriptions(T previous) { ITypedElement previousTE = previous.ToTypedElement(); - FhirPathVariableResolver resolver = new FhirPathVariableResolver() - { - NextResolver = _store.Resolve, - Variables = new() - { - //{ "current", currentTE }, - { "previous", previousTE }, - }, - }; - FhirEvaluationContext fpContext = new FhirEvaluationContext(previousTE.ToScopedNode()) { TerminologyService = _store.Terminology, - ElementResolver = resolver.Resolve, + ElementResolver = _store.Resolve, + Environment = new Dictionary>() + { + { "current", [] }, + { "previous", [ previousTE ] }, + }, }; PerformSubscriptionTest( diff --git a/src/FhirStore.CommonVersioned/Storage/VersionedFhirStore.cs b/src/FhirStore.CommonVersioned/Storage/VersionedFhirStore.cs index 5bb2b62..896eef8 100644 --- a/src/FhirStore.CommonVersioned/Storage/VersionedFhirStore.cs +++ b/src/FhirStore.CommonVersioned/Storage/VersionedFhirStore.cs @@ -2507,13 +2507,13 @@ public bool TryGetSearchParamDefinition(string resourceName, string spName, out /// A CompiledExpression. public static CompiledExpression CompileFhirPathCriteria(string fpc) { - MatchCollection matches = _fhirpathVarMatcher().Matches(fpc); + //MatchCollection matches = _fhirpathVarMatcher().Matches(fpc); - // replace the variable with a resolve call - foreach (string matchValue in matches.Select(m => m.Value).Distinct()) - { - fpc = fpc.Replace(matchValue, $"'{FhirPathVariableResolver._fhirPathPrefix}{matchValue.Substring(1)}'.resolve()"); - } + //// replace the variable with a resolve call + //foreach (string matchValue in matches.Select(m => m.Value).Distinct()) + //{ + // fpc = fpc.Replace(matchValue, $"'{FhirPathVariableResolver._fhirPathPrefix}{matchValue.Substring(1)}'.resolve()"); + //} return _compiler.Compile(fpc); } diff --git a/src/fhir-candle/Program.cs b/src/fhir-candle/Program.cs index 9a13d9e..3953390 100644 --- a/src/fhir-candle/Program.cs +++ b/src/fhir-candle/Program.cs @@ -48,9 +48,6 @@ public static partial class Program [GeneratedRegex("(http[s]*:\\/\\/.*(:\\d+)*)")] private static partial Regex InputUrlFormatRegex(); - /// (Immutable) The default subscription expiration. - private static readonly int DefaultSubscriptionExpirationMinutes = 30; - /// Main entry-point for this application. /// An array of command-line argument strings. public static async Task Main(string[] args) From 245a8b71d7ccc953100292d7c83c8a5bf7811274 Mon Sep 17 00:00:00 2001 From: Gino Canessa Date: Wed, 28 Aug 2024 13:03:08 -0500 Subject: [PATCH 4/5] Subscription RI page content update for clarity. --- .../Components/RI/subscriptions/IndexContentR4.razor | 5 ----- .../Components/RI/subscriptions/IndexContentR4B.razor | 6 ------ .../Components/RI/subscriptions/IndexContentR5.razor | 5 ----- .../Pages/Subscriptions/SubscriptionsLocal.razor | 2 +- 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/fhir-candle/Components/RI/subscriptions/IndexContentR4.razor b/src/fhir-candle/Components/RI/subscriptions/IndexContentR4.razor index b04b644..a285637 100644 --- a/src/fhir-candle/Components/RI/subscriptions/IndexContentR4.razor +++ b/src/fhir-candle/Components/RI/subscriptions/IndexContentR4.razor @@ -25,11 +25,6 @@ or updated from any other status to finished. - - - If you are getting started with topic-based subscriptions, there is a detailed tour available - here. - @* diff --git a/src/fhir-candle/Components/RI/subscriptions/IndexContentR4B.razor b/src/fhir-candle/Components/RI/subscriptions/IndexContentR4B.razor index fcd73c3..b61ce63 100644 --- a/src/fhir-candle/Components/RI/subscriptions/IndexContentR4B.razor +++ b/src/fhir-candle/Components/RI/subscriptions/IndexContentR4B.razor @@ -24,12 +24,6 @@ or updated from any other status to finished. - - - If you are getting started with topic-based subscriptions, there is a detailed tour available - here. - - @* diff --git a/src/fhir-candle/Components/RI/subscriptions/IndexContentR5.razor b/src/fhir-candle/Components/RI/subscriptions/IndexContentR5.razor index 3583755..241e3f9 100644 --- a/src/fhir-candle/Components/RI/subscriptions/IndexContentR5.razor +++ b/src/fhir-candle/Components/RI/subscriptions/IndexContentR5.razor @@ -24,11 +24,6 @@ or updated from any other status to complete. - - - If you are getting started with topic-based subscriptions, there is a detailed tour available - here. - @* diff --git a/src/fhir-candle/Pages/Subscriptions/SubscriptionsLocal.razor b/src/fhir-candle/Pages/Subscriptions/SubscriptionsLocal.razor index f1aef82..e722791 100644 --- a/src/fhir-candle/Pages/Subscriptions/SubscriptionsLocal.razor +++ b/src/fhir-candle/Pages/Subscriptions/SubscriptionsLocal.razor @@ -19,7 +19,7 @@ else @foreach (ParsedSubscription sub in _store!.CurrentSubscriptions) { - + Subscription From a44d06eaea4ead3318049375648a144680912852 Mon Sep 17 00:00:00 2001 From: Gino Canessa Date: Wed, 28 Aug 2024 13:06:04 -0500 Subject: [PATCH 5/5] Remove unused code from CLI option building. --- src/fhir-candle/Program.cs | 134 ++----------------------------------- 1 file changed, 7 insertions(+), 127 deletions(-) diff --git a/src/fhir-candle/Program.cs b/src/fhir-candle/Program.cs index 3953390..93c71a9 100644 --- a/src/fhir-candle/Program.cs +++ b/src/fhir-candle/Program.cs @@ -58,12 +58,6 @@ public static async Task Main(string[] args) .AddEnvironmentVariables() .Build(); - //// in order to process help correctly we have to build a parser independent of the command - //SCL.Parsing.Parser parser = BuildParser(envConfig); - - //// attempt a parse - //SCL.Parsing.ParseResult pr = parser.Parse(args); - SCL.RootCommand rootCommand = new("A lightweight in-memory FHIR server, for when a small FHIR will do."); foreach (SCL.Option option in BuildCliOptions(typeof(CandleConfig), envConfig: envConfig)) { @@ -73,129 +67,15 @@ public static async Task Main(string[] args) rootCommand.SetHandler(async (context) => await RunServer(context.ParseResult, context.GetCancellationToken())); return await rootCommand.InvokeAsync(args); - - //// check for invalid arguments, help, a generate command with no subcommand, or a generate with no packages to trigger the nicely formatted help - //if (pr.UnmatchedTokens.Any() || - // !pr.Tokens.Any() || - // (!pr.CommandResult.Command.Parents?.Any() ?? false) || - // pr.Tokens.Any(t => t.Value.Equals("-?", StringComparison.Ordinal)) || - // pr.Tokens.Any(t => t.Value.Equals("-h", StringComparison.Ordinal)) || - // pr.Tokens.Any(t => t.Value.Equals("--help", StringComparison.Ordinal)) || - // pr.Tokens.Any(t => t.Value.Equals("help", StringComparison.Ordinal))) - - //{ - // return await parser.InvokeAsync(args); - //} - - - //return await RunServer(pr); - - - - //// in order to process help correctly we have to build a parser independent of the command - //SCL.Parsing.Parser parser = BuildParser(envConfig); - - //// attempt a parse - //SCL.Parsing.ParseResult pr = parser.Parse(args); - - //return await parser.InvokeAsync(args); - - ////System.CommandLine.Parsing.Parser clParser = new System.CommandLine.Builder.CommandLineBuilder(_rootCommand).Build(); - - //return await rootCommand.InvokeAsync(args); - } - - - private static SCL.Parsing.Parser BuildParser(IConfiguration envConfig) - { - SCL.RootCommand command = new("A lightweight in-memory FHIR server, for when a small FHIR will do."); - foreach (SCL.Option option in BuildCliOptions(typeof(CandleConfig), envConfig: envConfig)) - { - // note that 'global' here is just recursive DOWNWARD - command.AddGlobalOption(option); - TrackIfEnum(option); - } - - //command.SetHandler(async (context) => await RunServer(context.ParseResult, context.GetCancellationToken())); - - SCL.Parsing.Parser parser = new CommandLineBuilder(command) - .UseExceptionHandler((ex, ctx) => - { - Console.WriteLine($"Error: {ex.Message}"); - ctx.ExitCode = 1; - }) - .UseDefaults() - .UseHelp(ctx => - { - foreach (SCL.Option option in _optsWithEnums) - { - StringBuilder sb = new(); - if (option.Aliases.Count != 0) - { - sb.AppendLine(string.Join(", ", option.Aliases)); - } - else - { - sb.AppendLine(option.Name); - } - - Type et = option.ValueType; - - if (option.ValueType.IsGenericType) - { - et = option.ValueType.GenericTypeArguments.First(); - } - - if (option.ValueType.IsArray) - { - et = option.ValueType.GetElementType()!; - } - - foreach (MemberInfo mem in et.GetMembers(BindingFlags.Public | BindingFlags.Static).Where(m => m.DeclaringType == et).OrderBy(m => m.Name)) - { - sb.AppendLine($" opt: {mem.Name}"); - } - - ctx.HelpBuilder.CustomizeSymbol( - option, - firstColumnText: (ctx) => sb.ToString()); - //secondColumnText: (ctx) => option.Description); - } - }) - .Build(); - - return parser; - - void TrackIfEnum(SCL.Option option) - { - if (option.ValueType.IsEnum) - { - _optsWithEnums.Add(option); - return; - } - - if (option.ValueType.IsGenericType) - { - if (option.ValueType.GenericTypeArguments.First().IsEnum) - { - _optsWithEnums.Add(option); - } - - return; - } - - if (option.ValueType.IsArray) - { - if (option.ValueType.GetElementType()!.IsEnum) - { - _optsWithEnums.Add(option); - } - - return; - } - } } + /// + /// Builds the command line options for the specified type. + /// + /// The type for which to build the command line options. + /// The type to exclude from the command line options. + /// The environment configuration. + /// An enumerable collection of command line options. private static IEnumerable BuildCliOptions( Type forType, Type? excludeFromType = null,