Skip to content

Commit

Permalink
Merge branch 'develop' into devops/re-enable-r4-tests-for-PRs
Browse files Browse the repository at this point in the history
  • Loading branch information
ewoutkramer authored Feb 16, 2024
2 parents 4f942a9 + 2a6c939 commit cd4c32a
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 136 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#nullable enable

using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using T = System.Threading.Tasks;

namespace Hl7.Fhir.Specification.Terminology
{
/// <summary>
/// Base class for checking Code terminology
/// </summary>
public abstract class CodeSystemTerminologyService : ITerminologyService
{
private readonly string _terminologyType;
private readonly string _codeSystem;
private readonly string[] _codeValueSets;

/// <summary>
/// Base class for checking Code terminology.
/// </summary>
/// <param name="terminologyType">String representation of the code type which is being checked. Exclusively used for error messages</param>
/// <param name="codeSystem">Name of the specification defining the members of the value set</param>
/// <param name="codeValueSets">uri's of the definitions of the code system. This can be multiple, if a FHIR version has changed this at some point.</param>
protected CodeSystemTerminologyService(string terminologyType, string codeSystem, string[] codeValueSets)
{
_terminologyType = terminologyType;
_codeSystem = codeSystem;
_codeValueSets = codeValueSets;
}

///<inheritdoc />
public T.Task<Resource> Closure(Parameters parameters, bool useGet = false) =>
throw new NotImplementedException();

///<inheritdoc />
public T.Task<Parameters>
CodeSystemValidateCode(Parameters parameters, string? id = null, bool useGet = false) =>
throw new NotImplementedException();

///<inheritdoc />
public T.Task<Resource> Expand(Parameters parameters, string? id = null, bool useGet = false) =>
throw new NotImplementedException();

///<inheritdoc />
public T.Task<Parameters> Lookup(Parameters parameters, bool useGet = false) =>
throw new NotImplementedException();

///<inheritdoc />
public T.Task<Parameters> Subsumes(Parameters parameters, string? id = null, bool useGet = false) =>
throw new NotImplementedException();

///<inheritdoc />
public T.Task<Parameters> Translate(Parameters parameters, string? id = null, bool useGet = false) =>
throw new NotImplementedException();

///<inheritdoc />
public async T.Task<Parameters> ValueSetValidateCode(Parameters parameters, string? id = null,
bool useGet = false)
{
parameters.CheckForValidityOfValidateCodeParams();

var validCodeParams = new ValidateCodeParameters(parameters);
var valueSetUri = validCodeParams?.Url?.Value != null
? new Canonical(validCodeParams?.Url?.Value).Uri
: null;

if (_codeValueSets.All(valueSet => valueSet != valueSetUri))
{
// 404 not found
throw new FhirOperationException($"Cannot find valueset '{validCodeParams!.Url?.Value}'",
HttpStatusCode.NotFound);
}

try
{
if (validCodeParams!.CodeableConcept is { })
return await ValidateCodeVS(validCodeParams.CodeableConcept).ConfigureAwait(false);
else if (validCodeParams.Coding is { })
return await ValidateCodeVS(validCodeParams.Coding).ConfigureAwait(false);
else
return await ValidateCodeVS(validCodeParams.Code?.Value, validCodeParams.System?.Value)
.ConfigureAwait(false);
}
catch (Exception e)
{
//500 internal server error
throw new FhirOperationException(e.Message, (HttpStatusCode)500);
}

}

private async Task<Parameters> ValidateCodeVS(Coding coding)
{
return await ValidateCodeVS(coding.Code, coding.System).ConfigureAwait(false);
}

private async Task<Parameters> ValidateCodeVS(CodeableConcept cc)
{
var result = new Parameters();

// Maybe just a text, but if there are no codings, that's a positive result
if (!cc.Coding.Any())
{
result.Add("result", new FhirBoolean(true));
return result;
}

// If we have just 1 coding, we better handle this using the simpler version of ValidateBinding
if (cc.Coding.Count == 1)
return await ValidateCodeVS(cc.Coding.Single()).ConfigureAwait(false);


// Else, look for one succesful match in any of the codes in the CodeableConcept
var callResults = await Task.WhenAll(cc.Coding.Select(coding => ValidateCodeVS(coding))).ConfigureAwait(false);
var anySuccesful = callResults.Any(p => p.GetSingleValue<FhirBoolean>("result")?.Value == true);

if (anySuccesful == false)
{
var messages = new StringBuilder();
messages.AppendLine("None of the Codings in the CodeableConcept were valid for the binding. Details follow.");

// gathering the messages of all calls
foreach (var msg in callResults.Select(cr => cr.GetSingleValue<FhirString>("message")?.Value).Where(m => m is { }))
{
messages.AppendLine(msg);
}

result.Add("message", new FhirString(messages.ToString()));
result.Add("result", new FhirBoolean(false));
}
else
{
result.Add("result", new FhirBoolean(true));
}

return result;
}

private Task<Parameters> ValidateCodeVS(string? code, string? system)
{
var result = new Parameters();
var systemUri = system != null ? new Canonical(system).Uri : null;


if (systemUri == _codeSystem || systemUri == null)
{
if (code is null)
{
result.Add("message", new FhirString("No code supplied."))
.Add("result", new FhirBoolean(false));
}
else
{
var success = ValidateCodeType(code);

if (success)
{
result.Add("result", new FhirBoolean(true));
}
else
{
result.Add("result", new FhirBoolean(false))
.Add("message", new FhirString($"'{code}' is not a valid {_terminologyType}."));
}
}
}
else
{
throw new FhirOperationException($"Unknown system '{systemUri}'", HttpStatusCode.NotFound);
}
return Task.FromResult(result);
}
abstract protected bool ValidateCodeType(string code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#nullable enable

using System.Text.RegularExpressions;

namespace Hl7.Fhir.Specification.Terminology
{
/// <summary>
/// Checks if codes are valid language types
/// </summary>
public class LanguageTerminologyService : CodeSystemTerminologyService
{
private const string LANGUAGE_SYSTEM = "urn:ietf:bcp:47";
public const string LANGUAGE_VALUESET = "http://hl7.org/fhir/ValueSet/all-languages";

public LanguageTerminologyService() : base("language", LANGUAGE_SYSTEM, [LANGUAGE_VALUESET])
{
}

override protected bool ValidateCodeType(string code)
{
var regex = new Regex("^[a-z]{2}(-[A-Z]{2})?$"); // matches either two lowercase letters OR 2 lowercase letters followed by a dash and two uppercase letters
return regex.IsMatch(code);
}
}
}

#nullable restore
Original file line number Diff line number Diff line change
@@ -1,154 +1,24 @@
#nullable enable

using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using System;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using T = System.Threading.Tasks;

namespace Hl7.Fhir.Specification.Terminology
{
/// <summary>
/// Checks if codes are valid Mime-Types (urn:ietf:bcp:13)
/// </summary>
public class MimeTypeTerminologyService : ITerminologyService
public sealed class MimeTypeTerminologyService : CodeSystemTerminologyService
{
private const string MIMETYPE_SYSTEM = "urn:ietf:bcp:13";
public const string MIMETYPE_VALUESET_R4_AND_UP = "http://hl7.org/fhir/ValueSet/mimetypes";
public const string MIMETYPE_VALUESET_STU3 = "http://www.rfc-editor.org/bcp/bcp13.txt";

///<inheritdoc />
public T.Task<Resource> Closure(Parameters parameters, bool useGet = false) => throw new NotImplementedException();
///<inheritdoc />
public T.Task<Parameters> CodeSystemValidateCode(Parameters parameters, string? id = null, bool useGet = false) => throw new NotImplementedException();
///<inheritdoc />
public T.Task<Resource> Expand(Parameters parameters, string? id = null, bool useGet = false) => throw new NotImplementedException();
///<inheritdoc />
public T.Task<Parameters> Lookup(Parameters parameters, bool useGet = false) => throw new NotImplementedException();
///<inheritdoc />
public T.Task<Parameters> Subsumes(Parameters parameters, string? id = null, bool useGet = false) => throw new NotImplementedException();

///<inheritdoc />
public T.Task<Parameters> Translate(Parameters parameters, string? id = null, bool useGet = false) => throw new NotImplementedException();

///<inheritdoc />
public async T.Task<Parameters> ValueSetValidateCode(Parameters parameters, string? id = null, bool useGet = false)
{

parameters.CheckForValidityOfValidateCodeParams();

var validCodeParams = new ValidateCodeParameters(parameters);
var valueSetUri = validCodeParams?.Url?.Value != null ? new Canonical(validCodeParams?.Url?.Value).Uri : null;

if (valueSetUri != MIMETYPE_VALUESET_R4_AND_UP && valueSetUri != MIMETYPE_VALUESET_STU3)
{ // 404 not found
throw new FhirOperationException($"Cannot find valueset '{validCodeParams!.Url?.Value}'", HttpStatusCode.NotFound);
}

try
{
if (validCodeParams!.CodeableConcept is { })
return await validateCodeVS(validCodeParams.CodeableConcept).ConfigureAwait(false);
else if (validCodeParams.Coding is { })
return await validateCodeVS(validCodeParams.Coding).ConfigureAwait(false);
else
return await validateCodeVS(validCodeParams.Code?.Value, validCodeParams.System?.Value).ConfigureAwait(false);
}
catch (Exception e)
{
//500 internal server error
throw new FhirOperationException(e.Message, (HttpStatusCode)500);
}

}

private async Task<Parameters> validateCodeVS(Coding coding)
{
return await validateCodeVS(coding.Code, coding.System).ConfigureAwait(false);
}

private async Task<Parameters> validateCodeVS(CodeableConcept cc)
{
var result = new Parameters();

// Maybe just a text, but if there are no codings, that's a positive result
if (!cc.Coding.Any())
{
result.Add("result", new FhirBoolean(true));
return result;
}

// If we have just 1 coding, we better handle this using the simpler version of ValidateBinding
if (cc.Coding.Count == 1)
return await validateCodeVS(cc.Coding.Single()).ConfigureAwait(false);


// Else, look for one succesful match in any of the codes in the CodeableConcept
var callResults = await Task.WhenAll(cc.Coding.Select(coding => validateCodeVS(coding))).ConfigureAwait(false);
var anySuccesful = callResults.Any(p => p.GetSingleValue<FhirBoolean>("result")?.Value == true);

if (anySuccesful == false)
{
var messages = new StringBuilder();
messages.AppendLine("None of the Codings in the CodeableConcept were valid for the binding. Details follow.");

// gathering the messages of all calls
foreach (var msg in callResults.Select(cr => cr.GetSingleValue<FhirString>("message")?.Value).Where(m => m is { }))
{
messages.AppendLine(msg);
}

result.Add("message", new FhirString(messages.ToString()));
result.Add("result", new FhirBoolean(false));
}
else
{
result.Add("result", new FhirBoolean(true));
}

return result;
}

private static Task<Parameters> validateCodeVS(string? code, string? system)
public MimeTypeTerminologyService() : base("MIME type", MIMETYPE_SYSTEM, [MIMETYPE_VALUESET_STU3, MIMETYPE_VALUESET_R4_AND_UP])
{
var result = new Parameters();
var systemUri = system != null ? new Canonical(system).Uri : null;


if (systemUri == MIMETYPE_SYSTEM || systemUri == null)
{
if (code is null)
{
result.Add("message", new FhirString("No code supplied."))
.Add("result", new FhirBoolean(false));
}
else
{
var success = validateMimeType(code);

if (success)
{
result.Add("result", new FhirBoolean(true));
}
else
{
result.Add("result", new FhirBoolean(false))
.Add("message", new FhirString($"'{code}' is not a valid MIME type."));
}
}
}
else
{
throw new FhirOperationException($"Unknown system '{systemUri}'", HttpStatusCode.NotFound);
}
return Task.FromResult(result);
}

//mime-type format: type "/" [tree "."] subtype ["+" suffix]* [";" parameter];
private static bool validateMimeType(string code)
override protected bool ValidateCodeType(string code)
{
var entries = code.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
return entries.Length == 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ public static MultiTerminologyService CreateDefaultForCore(IAsyncResourceResolve
MimeTypeTerminologyService.MIMETYPE_VALUESET_STU3
#else
MimeTypeTerminologyService.MIMETYPE_VALUESET_R4_AND_UP
#endif
#endif
}
};

var languageRoutingSettings = new TerminologyServiceRoutingSettings(new LanguageTerminologyService())
{
PreferredValueSets = [LanguageTerminologyService.LANGUAGE_VALUESET]
};

var localTermRoutingSettings = new TerminologyServiceRoutingSettings(new LocalTerminologyService(coreResourceResolver, expanderSettings))
{
PreferredValueSets = new string[]
Expand Down
Loading

0 comments on commit cd4c32a

Please sign in to comment.