Skip to content

Commit 446cd4f

Browse files
committed
Move the Current Canonical functionality out into its own class to be more compliant - not having the op available on non conformance resources
and easier referencing as an example implementation of the validation too.
1 parent e65a03f commit 446cd4f

File tree

4 files changed

+246
-147
lines changed

4 files changed

+246
-147
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,4 @@ specification.zip
168168
dist/
169169
/src/infer-out
170170
/src/.infersharpconfig
171+
/src/Hl7.Fhir.VersionConverters
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
using Hl7.Fhir.Model;
2+
using Hl7.Fhir.Rest;
3+
using Hl7.Fhir.Specification.Source;
4+
using Hl7.Fhir.Support;
5+
using Hl7.Fhir.Utility;
6+
using Hl7.Fhir.WebApi;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Net;
10+
using System.Threading.Tasks;
11+
12+
namespace Hl7.Fhir.DemoFileSystemFhirServer
13+
{
14+
public class CanonicalResourceService<TSP> : DirectoryResourceService<TSP>
15+
where TSP : class
16+
{
17+
public CanonicalResourceService(ModelBaseInputs<TSP> requestDetails, string resourceName, string directory, IResourceResolver source, IAsyncResourceResolver asyncSource)
18+
: base(requestDetails, resourceName, directory, source, asyncSource)
19+
{
20+
}
21+
22+
public CanonicalResourceService(ModelBaseInputs<TSP> requestDetails, string resourceName, string directory, IResourceResolver source, IAsyncResourceResolver asyncSource, SearchIndexer indexer)
23+
: base(requestDetails, resourceName, directory, source, asyncSource, indexer)
24+
{
25+
}
26+
27+
override async public Task<OperationOutcome> ValidateResource(Resource resource, ResourceValidationMode mode, string[] profiles)
28+
{
29+
var outcome = await base.ValidateResource(resource, mode, profiles);
30+
31+
if (outcome.Success && resource is IVersionableConformanceResource icr)
32+
{
33+
// This validation was successful, check that the canonical URL/Version/algorithm doesn't
34+
// create some invalid state for the server to be in
35+
var kvps = new List<KeyValuePair<string, string>>();
36+
kvps.Add(new KeyValuePair<string, string>("url", icr.Url));
37+
var bundle = await Search(kvps, null, SummaryType.True, null);
38+
39+
var ivrs = bundle.Entry
40+
.Select(e => e.Resource as IVersionableConformanceResource)
41+
.Where(e => (e as Resource).Id != resource.Id)
42+
.ToList(); // exclude itself (for updates)
43+
44+
// Verify that this version doesn't already exist too.
45+
if (ivrs.Any(v => v.Version == icr.Version))
46+
{
47+
outcome.Issue.Insert(0, new OperationOutcome.IssueComponent
48+
{
49+
Code = OperationOutcome.IssueType.Duplicate,
50+
Severity = OperationOutcome.IssueSeverity.Error,
51+
Details = new CodeableConcept(null, null, $"Version {icr.Version} already exists")
52+
});
53+
}
54+
55+
// Verify version algorithm isn't defined differently
56+
bool conflictingAlgorithm = false;
57+
var existingFhirpathAlg = ivrs.Where(e => e.versionAlgorithFhirPathExpression() != null).Select(e => e.versionAlgorithFhirPathExpression());
58+
var existingVerAlg = ivrs.Where(e => e.versionAlgorithCoded() != null).Select(e => e.versionAlgorithCoded().Code);
59+
if (icr.versionAlgorithFhirPathExpression() != null
60+
&& (existingVerAlg.Any() || existingFhirpathAlg.Any(e => e != icr.versionAlgorithFhirPathExpression())))
61+
conflictingAlgorithm = true;
62+
63+
if (icr.versionAlgorithCoded() != null
64+
&& (existingFhirpathAlg.Any() || existingVerAlg.Any(e => e != icr.versionAlgorithCoded().Code)))
65+
conflictingAlgorithm = true;
66+
67+
if (conflictingAlgorithm == true)
68+
{
69+
outcome.Issue.Insert(0, new OperationOutcome.IssueComponent
70+
{
71+
Code = OperationOutcome.IssueType.BusinessRule,
72+
Severity = OperationOutcome.IssueSeverity.Error,
73+
Details = new CodeableConcept(null, null, $"Ambiguous version algorithms would result: {string.Join(",", existingVerAlg.Union(existingFhirpathAlg))} is the existing algorithm")
74+
});
75+
}
76+
}
77+
78+
return outcome;
79+
}
80+
81+
override public async Task<Resource> PerformOperation(string operation, Parameters operationParameters, SummaryType summary)
82+
{
83+
switch (operation.ToLower())
84+
{
85+
case "current-canonical":
86+
return await PerformOperation_CurrentCanonical(operationParameters, summary);
87+
}
88+
89+
return await base.PerformOperation(operation, operationParameters, summary);
90+
}
91+
92+
private async Task<Resource> PerformOperation_CurrentCanonical(Parameters operationParameters, SummaryType summary)
93+
{
94+
var outcome = new OperationOutcome();
95+
string url = null;
96+
var statuses = new List<string>();
97+
98+
// read the URL parameter
99+
var urlParams = operationParameters.Parameter.Where(p => p.Name?.ToLower() == "url");
100+
if (urlParams.Any())
101+
{
102+
if (urlParams.Count() > 1)
103+
{
104+
outcome.Issue.Add(new OperationOutcome.IssueComponent()
105+
{
106+
Code = OperationOutcome.IssueType.Informational,
107+
Severity = OperationOutcome.IssueSeverity.Information,
108+
Details = new CodeableConcept(null, null, "Multiple 'url' parameters provided, using the first one")
109+
});
110+
}
111+
var val = urlParams.FirstOrDefault().Value;
112+
if (val == null)
113+
{
114+
outcome.Issue.Add(new OperationOutcome.IssueComponent()
115+
{
116+
Code = OperationOutcome.IssueType.Required,
117+
Severity = OperationOutcome.IssueSeverity.Error,
118+
Details = new CodeableConcept(null, null, "Parameter 'url' value is missing")
119+
});
120+
}
121+
else
122+
{
123+
if (!(val is FhirString || val is FhirUri))
124+
{
125+
outcome.Issue.Add(new OperationOutcome.IssueComponent()
126+
{
127+
Code = OperationOutcome.IssueType.Informational,
128+
Severity = OperationOutcome.IssueSeverity.Information,
129+
Details = new CodeableConcept(null, null, $"'url' parameters provided as {val.TypeName}, uri is defined in the specification")
130+
});
131+
}
132+
if (val is PrimitiveType p)
133+
url = p.ToString();
134+
}
135+
}
136+
if (!urlParams.Any())
137+
{
138+
outcome.Issue.Add(new OperationOutcome.IssueComponent()
139+
{
140+
Code = OperationOutcome.IssueType.Required,
141+
Severity = OperationOutcome.IssueSeverity.Error,
142+
Details = new CodeableConcept(null, null, "Required parmeter 'url' missing")
143+
});
144+
}
145+
146+
// read the status parameter(s)
147+
var statusParams = operationParameters.Parameter.Where(p => p.Name?.ToLower() == "status");
148+
if (statusParams.Any())
149+
{
150+
foreach (var value in statusParams.Select(p => p.Value))
151+
{
152+
string psv = value.ToString();
153+
if (value is FhirUri code)
154+
psv = code.Value;
155+
else if (value is FhirString str)
156+
psv = str.Value;
157+
if (!string.IsNullOrEmpty(psv))
158+
{
159+
statuses.AddRange(psv.Split(','));
160+
}
161+
}
162+
// validate status value is in enumeration
163+
foreach (var psv in statuses)
164+
{
165+
PublicationStatus? ps = EnumUtility.ParseLiteral<PublicationStatus>(psv);
166+
if (!ps.HasValue)
167+
{
168+
outcome.Issue.Add(new OperationOutcome.IssueComponent()
169+
{
170+
Code = OperationOutcome.IssueType.Invalid,
171+
Severity = OperationOutcome.IssueSeverity.Error,
172+
Details = new CodeableConcept(null, null, $"Invalid 'status' parameter value [{psv}]")
173+
});
174+
}
175+
}
176+
}
177+
178+
// return the error if one was detected
179+
if (!outcome.Success)
180+
{
181+
outcome.SetAnnotation<HttpStatusCode>(HttpStatusCode.BadRequest);
182+
return outcome;
183+
}
184+
185+
// Search for the resources using this canonical URL
186+
var kvps = new List<KeyValuePair<string, string>>();
187+
kvps.Add(new KeyValuePair<string, string>("url", url));
188+
if (statuses.Any())
189+
kvps.Add(new KeyValuePair<string, string>("status", string.Join(",", statuses)));
190+
var bundle = await Search(kvps, null, summary, null);
191+
if (!bundle.Entry.Any())
192+
{
193+
outcome.Issue.Insert(0, new OperationOutcome.IssueComponent
194+
{
195+
Code = OperationOutcome.IssueType.NotFound,
196+
Severity = OperationOutcome.IssueSeverity.Error,
197+
Details = new CodeableConcept(null, null, $"Canonical URL '{url}' was not found")
198+
});
199+
outcome.SetAnnotation(HttpStatusCode.NotFound);
200+
return outcome;
201+
}
202+
203+
// use the Canonical helper function to locate the current one
204+
var ivrs = bundle.Entry.Select(e => e.Resource as IVersionableConformanceResource).Where(e => e != null);
205+
var result = CurrentCanonical.Current(ivrs);
206+
if (result != null)
207+
{
208+
return result as Resource;
209+
}
210+
211+
// Could not evaluate the current version
212+
outcome.Issue.Insert(0, new OperationOutcome.IssueComponent
213+
{
214+
Code = OperationOutcome.IssueType.Processing,
215+
Severity = OperationOutcome.IssueSeverity.Error,
216+
Details = new CodeableConcept(null, null, $"Canonical URL '{url}' could not be calculated between versions {string.Join(",", ivrs.Select(i => i.Version))}")
217+
});
218+
outcome.SetAnnotation(HttpStatusCode.Ambiguous);
219+
return outcome;
220+
}
221+
}
222+
}

0 commit comments

Comments
 (0)