Skip to content

Commit

Permalink
Merge branch 'develop' into feature/try-nullability
Browse files Browse the repository at this point in the history
  • Loading branch information
ewoutkramer authored Apr 4, 2024
2 parents 761f656 + 2ad3c09 commit d4cbbeb
Show file tree
Hide file tree
Showing 23 changed files with 591 additions and 262 deletions.
3 changes: 1 addition & 2 deletions src/Hl7.Fhir.Base/ElementModel/MaskingNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ namespace Hl7.Fhir.ElementModel
{
public class MaskingNode : ITypedElement, IAnnotated, IExceptionSource
{

/// <summary>
/// Set to true when a complex type property is mandatory so all its children need to be included
/// </summary>
Expand Down Expand Up @@ -59,7 +58,7 @@ public static MaskingNode ForCount(ITypedElement node) =>
new MaskingNode(node, new MaskingNodeSettings
{
IncludeMandatory = true,
IncludeElements = new[] { "id", "total" },
IncludeElements = new[] { "id", "total", "link" },
});

public MaskingNode(ITypedElement source, MaskingNodeSettings settings = null)
Expand Down
119 changes: 82 additions & 37 deletions src/Hl7.Fhir.Base/Rest/BaseFhirClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Hl7.Fhir.Utility;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.Http;
Expand Down Expand Up @@ -438,7 +439,6 @@ public virtual async Task ConditionalDeleteMultipleAsync(SearchParams condition,
{
if (id == null) throw Error.ArgumentNull(nameof(id));


var tx = new TransactionBuilder(Endpoint);
var resourceType = typeNameOrDie<TResource>();

Expand All @@ -450,6 +450,26 @@ public virtual async Task ConditionalDeleteMultipleAsync(SearchParams condition,
return executeAsync<TResource>(tx.ToBundle(), new[] { HttpStatusCode.Created, HttpStatusCode.OK }, ct);
}

public virtual Task<TResource?> PatchAsync<TResource>(string id, string patchDocument, ResourceFormat format, CancellationToken? ct = null) where TResource : Resource
{
if (id == null) throw Error.ArgumentNull(nameof(id));

var resourceType = typeNameOrDie<TResource>();
var url = new RestUrl(Endpoint).AddPath(resourceType, id);

var request = new HttpRequestMessage(new("PATCH"), url.Uri).WithFormatParameter(format);

request.Content = new StringContent(patchDocument);
request.Content.Headers.ContentType = new MediaTypeHeaderValue(format switch
{
ResourceFormat.Json => "application/json-patch+json",
ResourceFormat.Xml => "application/xml-patch+xml",
_ => throw Error.Argument(nameof(format), "Unsupported format")
});

return executeAsync<TResource>(request, new[] { HttpStatusCode.Created, HttpStatusCode.OK }, ct);
}

/// <summary>
/// Conditionally patch a resource on a FHIR Endpoint
/// </summary>
Expand Down Expand Up @@ -787,6 +807,56 @@ public async Task DeleteHistoryVersionAsync(string location, CancellationToken?

using var responseMessage = await Requester.ExecuteAsync(requestMessage, cancellation).ConfigureAwait(false);

return await extractResourceFromHttpResponse<TResource>(expect, responseMessage, entryComponent: request);
}

private async Task<TResource?> executeAsync<TResource>(HttpRequestMessage request, IEnumerable<HttpStatusCode> expect, CancellationToken? ct) where TResource : Resource
{
var cancellation = ct ?? CancellationToken.None;

cancellation.ThrowIfCancellationRequested();

using var responseMessage = await Requester.ExecuteAsync(request, cancellation).ConfigureAwait(false);

return await extractResourceFromHttpResponse<TResource>(expect, responseMessage, request);
}

#endregion

#region Utilities

// Create our own and add decompression strategy in default handler.
private static HttpClientHandler makeDefaultHandler() =>
new()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};

private static Uri getValidatedEndpoint(Uri endpoint)
{
if (endpoint == null) throw new ArgumentNullException(nameof(endpoint));

endpoint = new Uri(endpoint.OriginalString.EnsureEndsWith("/"));

if (!endpoint.IsAbsoluteUri) throw new ArgumentException("Endpoint must be absolute", nameof(endpoint));

return endpoint;
}
private static ResourceIdentity verifyResourceIdentity(Uri location, bool needId, bool needVid)
{
var result = new ResourceIdentity(location);

if (result.ResourceType == null) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the resource type in its path");
if (needId && result.Id == null) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the logical id in its path");
if (needVid && !result.HasVersion) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the version id in its path");

return result;
}

// either msg or entryComponent should be set
private async Task<TResource?> extractResourceFromHttpResponse<TResource>(IEnumerable<HttpStatusCode> expect, HttpResponseMessage responseMessage, HttpRequestMessage? msg = null, Bundle.EntryComponent? entryComponent = null) where TResource : Resource
{
if (msg is null && entryComponent is null) throw new ArgumentException("Either msg or entryComponent should be set");
// Validate the response and throw the appropriate exceptions. Also, if we have *not* verified the FHIR version
// of the server, add a suggestion about this in the (legacy) parsing exception.
var suggestedVersionOnParseError = !Settings.VerifyFhirVersion ? fhirVersion : null;
Expand All @@ -809,7 +879,7 @@ await ValidateResponse(responseMessage, expect, getSerializationEngine(), sugges
// the full body of the altered resource.
var noRealBody = LastBodyAsResource is null || (LastBodyAsResource is OperationOutcome && string.IsNullOrEmpty(LastBodyAsResource.Id));
var shouldFetchFullRepresentation = noRealBody
&& isPostOrPutOrPatch(request)
&& (msg is not null ? isPostOrPutOrPatch(msg.Method) : isPostOrPutOrPatch(entryComponent!))
&& Settings.ReturnPreference == ReturnPreference.Representation
&& LastResult.Location is { } fetchLocation
&& new ResourceIdentity(fetchLocation).IsRestResourceIdentity(); // Check that it isn't an operation too
Expand All @@ -833,43 +903,14 @@ await ValidateResponse(responseMessage, expect, getSerializationEngine(), sugges
null => null,

// Unexpected response type in the body, throw.
_ => throw new FhirOperationException(unexpectedBodyType(request.Request), responseMessage.StatusCode)
_ => throw new FhirOperationException(entryComponent is not null ? unexpectedBodyTypeForBundle(entryComponent.Request) : unexpectedBodyTypeForMessage(msg!), responseMessage.StatusCode)
};

static string unexpectedBodyType(Bundle.RequestComponent rc) => $"Operation {rc.Method} on {rc.Url} " +

static string unexpectedBodyTypeForBundle(Bundle.RequestComponent rc) => $"Operation {rc.Method} on {rc.Url} " +
$"expected a body of type {typeof(TResource).Name} but a {typeof(TResource).Name} was returned.";

static string unexpectedBodyTypeForMessage(HttpRequestMessage msg) => $"Operation {msg.Method} on {msg.RequestUri} " +
$"expected a body of type {typeof(TResource).Name} but a {typeof(TResource).Name} was returned.";
}

#endregion

#region Utilities

// Create our own and add decompression strategy in default handler.
private static HttpClientHandler makeDefaultHandler() =>
new()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};

private static Uri getValidatedEndpoint(Uri endpoint)
{
if (endpoint == null) throw new ArgumentNullException(nameof(endpoint));

endpoint = new Uri(endpoint.OriginalString.EnsureEndsWith("/"));

if (!endpoint.IsAbsoluteUri) throw new ArgumentException("Endpoint must be absolute", nameof(endpoint));

return endpoint;
}
private static ResourceIdentity verifyResourceIdentity(Uri location, bool needId, bool needVid)
{
var result = new ResourceIdentity(location);

if (result.ResourceType == null) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the resource type in its path");
if (needId && result.Id == null) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the logical id in its path");
if (needVid && !result.HasVersion) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the version id in its path");

return result;
}

/// <summary>
Expand Down Expand Up @@ -913,9 +954,13 @@ internal static async Task<ResponseData> ValidateResponse(HttpResponseMessage re

private static bool isPostOrPutOrPatch(Bundle.EntryComponent interaction) =>
interaction.Request.Method is Bundle.HTTPVerb.POST or Bundle.HTTPVerb.PUT or Bundle.HTTPVerb.PATCH;

private static bool isPostOrPutOrPatch(HttpMethod method) =>
method == HttpMethod.Post || method == HttpMethod.Put || method == new HttpMethod("PATCH");

private bool _versionChecked = false;


private IFhirSerializationEngine getSerializationEngine()
{
return Settings.SerializationEngine ?? FhirSerializationEngineFactory.Legacy.FromParserSettings(Inspector, Settings.ParserSettings ?? new());
Expand Down
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/Rest/TransactionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ public TransactionBuilder ConditionalPatch(string resourceType, SearchParams con

return this;
}

#endregion

#region Delete
Expand Down
3 changes: 0 additions & 3 deletions src/Hl7.Fhir.Base/Rest/TransactionBuilder_obsolete.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@

#nullable enable

using Hl7.Fhir.Introspection;
using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Utility;
using System;

namespace Hl7.Fhir.Rest;
Expand Down
7 changes: 7 additions & 0 deletions src/Hl7.Fhir.Base/Serialization/SerializationFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ public abstract class SerializationFilter
IncludeMandatory = true
}));

public static SerializationFilter ForCount() => new BundleFilter(new TopLevelFilter(
new ElementMetadataFilter()
{
IncludeMandatory = true,
IncludeNames = new[] { "id", "total", "link" }
}));

/// <summary>
/// Construct a new filter that conforms to the `_summary=data` summarized form.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,15 @@ private static IAsyncResourceResolver verifySource(IAsyncResourceResolver resolv
/// <param name="structure">A <see cref="StructureDefinition"/> instance.</param>
public async T.Task UpdateAsync(StructureDefinition structure)
{
structure.Snapshot = new StructureDefinition.SnapshotComponent()
{
Element = await GenerateAsync(structure).ConfigureAwait(false)
};
structure.Snapshot.SetCreatedBySnapshotGenerator();
var result = await GenerateAsync(structure).ConfigureAwait(false);

if (result == null && structure.Snapshot?.Element != null) return;

structure.Snapshot = new StructureDefinition.SnapshotComponent { Element = result };
structure.Snapshot.SetCreatedBySnapshotGenerator();

// [WMR 20170209] TODO: also merge global StructureDefinition.Mapping components
// structure.Mappings = ElementDefnMerger.Merge(...)
// [WMR 20170209] TODO: also merge global StructureDefinition.Mapping components
// structure.Mappings = ElementDefnMerger.Merge(...)
}

/// <inheritdoc cref="UpdateAsync(StructureDefinition)"/>
Expand Down
14 changes: 13 additions & 1 deletion src/Hl7.Fhir.Serialization.Shared.Tests/SummaryTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Hl7.Fhir.ElementModel;
using FluentAssertions;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Specification;
using Hl7.Fhir.Specification.Source;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -74,5 +75,16 @@ public void SummaryCountUsingStructureDefinitionSummaryProvider()
ITypedElement getXmlNodeSDSP(string xml, FhirXmlParsingSettings s = null) =>
XmlParsingHelpers.ParseToTypedElement(xml, new StructureDefinitionSummaryProvider(ZipSource.CreateValidationSource()), s);
}

[TestMethod]
public void TestSummaryCountSelfLinks()
{
var tpXml = File.ReadAllText(Path.Combine("TestData", "no-namespace.xml"));

var nav = new ScopedNode(getXmlNode(tpXml));
var masker = MaskingNode.ForCount(nav);

masker.Children("link").Children("relation").First().Value.Should().BeEquivalentTo("self");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<meta>
<lastUpdated value="2014-08-18T01:43:30Z"/>
</meta>
<link>
<relation value="self"/>
<url value="https://example.com/base/Observation?subject=Patient/347"/>
</link>
<type value="searchset"/>
<total value="3"/>
<entry>
Expand Down
28 changes: 28 additions & 0 deletions src/Hl7.Fhir.Shims.Base/Model/ValueSetExpansionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ public static class ValueSetExpansionExtensions
return null;
}

internal static List<CSDC> RemoveCode(this ICollection<CSDC> concepts, string code)
{
return concepts.removeCodeByPredicate(c => c.Code == code).ToList();
}

private static ICollection<CSDC> removeCodeByPredicate(this ICollection<CSDC> concepts, Predicate<CSDC> predicate)
{
foreach (var concept in concepts)
{
var result = concept.removeCodeByPredicate(concepts, predicate);
if (result != null) return result;
}
return concepts;
}
private static ICollection<CSDC>? removeCodeByPredicate(this CSDC concept, ICollection<CSDC> concepts, Predicate<CSDC> predicate)
{
// Direct hit
if (predicate(concept))
concepts.Remove(concept);
else if (concept.Concept.Any())
{
removeCodeByPredicate(concept.Concept, predicate);
}
return concepts;
}



/// <summary>
/// Loops through all concepts and descendants and returns a flat list of concepts, without a nested hierarchy
/// </summary>
Expand Down
Loading

0 comments on commit d4cbbeb

Please sign in to comment.