diff --git a/.deployments/k8-argo-run-ris.yaml b/.deployments/k8-argo-run-ris.yaml
new file mode 100644
index 0000000..ac9920e
--- /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-prod
+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
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/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..5a0b2b7 100644
--- a/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems
+++ b/src/FhirStore.CommonVersioned/FhirStore.CommonVersioned.projitems
@@ -12,7 +12,6 @@
-
@@ -29,7 +28,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/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/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..a17857f 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;
}
@@ -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 50ea504..896eef8 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,
@@ -2492,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);
}
@@ -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/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/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:
-
+
Subscription
diff --git a/src/fhir-candle/Program.cs b/src/fhir-candle/Program.cs
index bfb8575..93c71a9 100644
--- a/src/fhir-candle/Program.cs
+++ b/src/fhir-candle/Program.cs
@@ -3,8 +3,8 @@
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
//
+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,255 +29,112 @@
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;
-
/// Main entry-point for this application.
/// An array of command-line argument strings.
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()
+ 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);
+ }
+
+ ///
+ /// 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,
+ IConfiguration? envConfig = null)
+ {
+ HashSet inheritedPropNames = [];
+
+ if (excludeFromType != null)
{
- ServerConfiguration config = new()
+ PropertyInfo[] exProps = excludeFromType.GetProperties();
+ foreach (PropertyInfo exProp in exProps)
{
- 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();
+ inheritedPropNames.Add(exProp.Name);
+ }
+ }
- return await rootCommand.InvokeAsync(args);
+ 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 +149,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 +159,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 +288,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 +310,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 +355,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 +366,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 +383,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 +400,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 @@
+
-
+
-
+
-
+