Skip to content

Commit

Permalink
Merge pull request #212 from microsoft/dotliquid
Browse files Browse the repository at this point in the history
Update version to v3.4
  • Loading branch information
qiwjin authored Mar 3, 2021
2 parents b7001db + c3ef964 commit 9ac2c36
Show file tree
Hide file tree
Showing 101 changed files with 2,584 additions and 972 deletions.
77 changes: 72 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,90 @@ More details of usage are given in [Template Management CLI tool](docs/TemplateM
Besides current version of [templates](data/Templates) given in our project, other versions that released by Microsoft are stored in a public ACR: healthplatformregistry.azurecr.io, users can directly pull templates from ``` healthplatformregistry.azurecr.io/hl7v2defaulttemplates:<version> ``` without authentication.
>Note!: Template version is aligned with the version of FHIR Converter.
### A note on Resource ID generation
## Usage Notes

The default templates provided with the Converter computes resource ids using the fields present in the input data. In order to preserve the generated resource ids, the converter generates PUT calls, instead of POST calls.
### Resource ID generation

There are a set of ID generation [templates](data/Templates/Hl7v2/ID) to help generate FHIR resource IDs from HL7 v2 messages.
The default templates provided with the Converter computes resource ids using the fields present in the input data. In order to preserve the generated resource ids, the converter created PUT requests, instead of POST requests in the generated bundles.

An ID generation template does 3 things: 1) extract identifiers from input segment or field; 2) combine the identifers with resource type and base ID (optional) as hash seed; 3) compute hash as output ID.
A set of [templates](data/Templates/Hl7v2/ID) help generate FHIR resource IDs from HL7 v2 messages. An ID generation template does 3 things: 1) extract identifiers from input segment or field; 2) combine the identifers with resource type and base ID (optional) as hash seed; 3) compute hash as output ID.

The Converter introduces a concept of "base resource/base ID". Base resources are independent entities, like Patient, Organization, Device, etc, whose IDs are defined as base ID. Base IDs could be used to generate IDs for other resources that relate to them. It helps enrich the input for hash and thus reduce ID collision.
For example, a Patient ID is used as part of hash input for an AllergyIntolerance ID, as this resource is closely related with a specific patient.

Below is an example where an AllergyIntolerance ID is generated, using ID/AllergyIntolerance template, AL1 segment and patient ID as its base ID.
The syntax is `{% evaluate [id] using [template] [variables] -%}`.
```

```liquid
{% evaluate allergyIntoleranceId using 'ID/AllergyIntolerance' AL1: al1Segment, baseId: patientId -%}
```

### Resource validation and post-processing

Real world HL7 messages vary in richness and level of conformance with the spec. The output of converter depends on the templates as well as the quality and richness of input messages. Therefore, it is important that you review and validate the Converter output before using those in production.

In general, you can use [HL7 FHIR validator](https://wiki.hl7.org/Using_the_FHIR_Validator) to validate a FHIR resource. You may be able to fix some of the conversion issues by appropriately changing the templates. For other issues, you may need to have a post-processing step in your pipeline.

In some cases, due to lack of field level data in the incoming messages, the Converter may produce resources without useful information or even without ID. You can use `Hl7.Fhir.R4` .NET library to filter such resources in your pipeline. Here is the sample code for such purpose.

```C#
using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;

public class PostProcessor
{
private readonly FhirJsonParser _parser = new FhirJsonParser();

public IEnumerable<Resource> FilterResources(IEnumerable<string> fhirResources)
{
return fhirResources
.Select(fhirResource => _parser.Parse<Resource>(fhirResource))
.Where(resource => !IsEmptyResource(resource))
.Where(resource => !IsIdAbsentResource(resource));
}

public bool IsEmptyResource(Resource resource)
{
try
{
var fhirResource = resource.ToJObject();
var properties = fhirResource.Properties().Select(property => property.Name);
// an empty resource contains no properties other than "resourceType" and "id"
return !properties
.Where(property => !property.Equals("resourceType"))
.Where(property => !property.Equals("id"))
.Any();
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message);
// deal with the exception...
}

return false;
}

public bool IsIdAbsentResource(Resource resource)
{
try
{
return string.IsNullOrWhiteSpace(resource.Id);
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message);
// deal with the exception...
}
return false;
}
}
```



## Reference documentation
- [Filters summary](docs/FiltersSummary.md)
- [Snippet concept](docs/SnippetConcept.md)
Expand Down
201 changes: 83 additions & 118 deletions data/Templates/Hl7v2/ADT_A01.liquid
Original file line number Diff line number Diff line change
@@ -1,154 +1,119 @@
{% assign firstSegments = hl7v2Data | get_first_segments: 'PID|PD1|PV1|PV2|AVR|MSH' -%}
{% assign pr1SegmentLists = hl7v2Data | get_segment_lists: 'PR1' -%}
{% assign nk1SegmentLists = hl7v2Data | get_segment_lists: 'NK1' -%}
{% assign obxSegmentLists = hl7v2Data | get_segment_lists: 'OBX' -%}
{% assign al1SegmentLists = hl7v2Data | get_segment_lists: 'AL1' -%}
{% assign dg1SegmentLists = hl7v2Data | get_segment_lists: 'DG1' -%}

{
"resourceType": "Bundle",
"type": "batch",
{% if firstSegments.MSH.7 -%}
"timestamp":"{{ firstSegments.MSH.7.Value | format_as_date_time }}",
{% endif -%}
"identifier":
{
"value":"{{ firstSegments.MSH.10.Value }}",
},
"entry": [
{% assign firstSegments = hl7v2Data | get_first_segments: 'PID|PD1|PV1|PV2|AVR|MSH' -%}

{% evaluate messageHeaderId using 'ID/MessageHeader' MSH: firstSegments.MSH -%}

{% if messageHeaderId -%}
{% include 'Resource/MessageHeader' MSH: firstSegments.MSH, ID: messageHeaderID -%}
{% endif -%}
{% include 'Resource/MessageHeader' MSH: firstSegments.MSH, ID: messageHeaderID -%}

{% evaluate patientId using 'ID/Patient' PID: firstSegments.PID, type: 'First' -%}
{% if patientId -%}
{% assign fullPatientId = patientId | prepend: 'Patient/' -%}
{% include 'Resource/Patient' PID: firstSegments.PID, PD1: firstSegments.PD1, ID: patientId -%}
{% endif -%}

{% evaluate provenanceId using 'ID/Provenance' MSH: firstSegments.MSH, baseId: patientId -%}
{% evaluate practitionerId10 using 'ID/Practitioner' XCN: firstSegments.ORC.10 -%}
{% evaluate practitionerId11 using 'ID/Practitioner' XCN: firstSegments.ORC.11 -%}
{% evaluate practitionerId12 using 'ID/Practitioner' XCN: firstSegments.ORC.12 -%}
{% evaluate locationId using 'ID/Location' XON: firstSegments.ORC.21 -%}

{% if practitionerId10 -%}
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId10 -%}
{% endif %}

{% if practitionerId11 -%}
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId11 -%}
{% endif %}

{% if practitionerId12 -%}
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId12 -%}
{% endif %}

{% if locationId -%}
{% include 'Resource/Location' ORC: firstSegments.ORC, ID: locationId -%}
{% endif -%}

{% if provenanceId -%}
{% include 'Resource/Provenance' MSH: firstSegments.MSH, ORC: firstSegments.ORC, Practitioner_ID_ORC_10: practitionerId10, Practitioner_ID_ORC_11: practitionerId11, Practitioner_ID_ORC_12: practitionerId12, Location_ID_ORC_21: locationId, ID: provenanceId -%}
{% endif -%}
{% assign fullPatientId = patientId | prepend: 'Patient/' -%}
{% include 'Resource/Patient' PID: firstSegments.PID, PD1: firstSegments.PD1, ID: patientId -%}

{% evaluate practitionerId_ORC_10 using 'ID/Practitioner' XCN: firstSegments.ORC.10 -%}
{% evaluate practitionerId_ORC_11 using 'ID/Practitioner' XCN: firstSegments.ORC.11 -%}
{% evaluate practitionerId_ORC_12 using 'ID/Practitioner' XCN: firstSegments.ORC.12 -%}
{% evaluate practitionerId_PV1_7 using 'ID/Practitioner' XCN: firstSegments.PV1.7 -%}
{% evaluate practitionerId_PV1_8 using 'ID/Practitioner' XCN: firstSegments.PV1.8 -%}
{% evaluate practitionerId_PV1_9 using 'ID/Practitioner' XCN: firstSegments.PV1.9 -%}
{% evaluate practitionerId_PV1_17 using 'ID/Practitioner' XCN: firstSegments.PV1.17 -%}
{% evaluate practitionerId_PV1_52 using 'ID/Practitioner' XCN: firstSegments.PV1.52 -%}

{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId_ORC_10 -%}
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId_ORC_11 -%}
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId_ORC_12 -%}
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_7 -%}
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_8 -%}
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_9 -%}
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_17 -%}
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_52 -%}

{% evaluate accountId using 'ID/Account' CX: firstSegments.PID.3 -%}
{% if accountId -%}
{% include 'Resource/Account' PID: firstSegments.PID, ID: accountId -%}
{% endif -%}
{% include 'Resource/Account' PID: firstSegments.PID, ID: accountId -%}
{% include 'Reference/Account/Subject' ID: accountId, REF: fullPatientId -%}

{% evaluate encounterId using 'ID/Encounter' PV1: firstSegments.PV1, baseId: patientId -%}
{% if encounterId -%}
{% include 'Resource/Encounter' PV1: firstSegments.PV1, PV2: firstSegments.PV2, ID: encounterId -%}
{% evaluate locationId_ORC_21 using 'ID/Location' XON: firstSegments.ORC.21 -%}
{% evaluate locationId_PV1_3 using 'ID/Location' PL: firstSegments.PV1.3 -%}
{% evaluate locationId_PV1_6 using 'ID/Location' PL: firstSegments.PV1.6 -%}

{% evaluate locationId3 using 'ID/Location' PL: firstSegments.PV1.3 -%}
{% if locationId3 -%}
{% include 'Resource/Location' PL: firstSegments.PV1.3, ID: locationId3 -%}
{% endif -%}
{% include 'Resource/Location' ORC: firstSegments.ORC, ID: locationId_ORC_21 -%}
{% include 'Resource/Location' PL: firstSegments.PV1.3, ID: locationId_PV1_3 -%}
{% include 'Resource/Location' PL: firstSegments.PV1.6, ID: locationId_PV1_6 -%}

{% evaluate locationId6 using 'ID/Location' PL: firstSegments.PV1.6 -%}
{% if locationId6 -%}
{% include 'Resource/Location' PL: firstSegments.PV1.6, ID: locationId6 -%}
{% endif -%}

{% include 'Resource/Encounter' PV1: firstSegments.PV1, Location_ID_PV1_3: locationId3, Location_ID_PV1_6: locationId6, ID: encounterId -%}
{% evaluate provenanceId using 'ID/Provenance' MSH: firstSegments.MSH, baseId: patientId -%}
{% include 'Resource/Provenance' MSH: firstSegments.MSH, ORC: firstSegments.ORC, Practitioner_ID_ORC_10: practitionerId_ORC_10, Practitioner_ID_ORC_11: practitionerId_ORC_11, Practitioner_ID_ORC_12: practitionerId_ORC_12, Location_ID_ORC_21: locationId_ORC_21, ID: provenanceId -%}

{% if patientId -%}
{% include 'Reference/Encounter/Subject' ID: encounterId, REF: fullPatientId -%}
{% endif -%}
{% endif -%}
{% evaluate encounterId using 'ID/Encounter' PV1: firstSegments.PV1, baseId: patientId -%}
{% include 'Resource/Encounter' PV1: firstSegments.PV1, PV2: firstSegments.PV2, Location_ID_PV1_3: locationId_PV1_3, Location_ID_PV1_6: locationId_PV1_6, Practitioner_ID_PV1_7: practitionerId_PV1_7, Practitioner_ID_PV1_8: practitionerId_PV1_8, Practitioner_ID_PV1_9: practitionerId_PV1_9, Practitioner_ID_PV1_17: practitionerId_PV1_17, Practitioner_ID_PV1_52: practitionerId_PV1_52, ID: encounterId -%}
{% include 'Reference/Encounter/Subject' ID: encounterId, REF: fullPatientId -%}

{% assign pr1SegmentLists = hl7v2Data | get_segment_lists: 'PR1' -%}
{% for pr1Segment in pr1SegmentLists.PR1 -%}
{% evaluate locationId_PR1_23 using 'ID/Location' PL: pr1Segment.23 -%}
{% include 'Resource/Location' PL: pr1Segment.23, ID: locationId_PR1_23 -%}

{% evaluate procedureId using 'ID/Procedure' PR1: pr1Segment, baseId: patientId -%}
{% if procedureId -%}
{% include 'Resource/Procedure' PR1: pr1Segment, ID: procedureId -%}

{% evaluate locationId using 'ID/Location' PL: pr1Segment.23 -%}
{% if locationId -%}
{% include 'Resource/Location' PL: pr1Segment.23, ID: locationId -%}
{% endif -%}

{% if patientId -%}
{% include 'Reference/Procedure/Subject' ID: procedureId, REF: fullPatientId -%}
{% endif -%}
{% endif -%}
{% include 'Resource/Procedure' PR1: pr1Segment, ID: procedureId -%}
{% include 'Reference/Procedure/Subject' ID: procedureId, REF: fullPatientId -%}
{% endfor -%}

{% assign nk1SegmentLists = hl7v2Data | get_segment_lists: 'NK1' -%}

{% for nk1Segment in nk1SegmentLists.NK1 -%}
{% include 'Resource/Patient' NK1: nk1Segment, ID: patientId -%}
{% evaluate organizationId_NK1_13 using 'ID/Organization' XON: nk1Segment.13 -%}
{% include 'Resource/Organization' NK1: nk1Segment, ID: organizationId_NK1_13 -%}

{% include 'Resource/Patient' NK1: nk1Segment, Organization_ID_NK1_13: organizationId_NK1_13, ID: patientId -%}

{% evaluate relatedPersonId using 'ID/RelatedPerson' NK1: nk1Segment, baseId: patientId -%}
{% if relatedPersonId -%}
{% include 'Resource/RelatedPerson' NK1: nk1Segment, ID: relatedPersonId -%}
{% if patientId -%}
{% include 'Reference/RelatedPerson/Patient' ID: relatedPersonId, REF: fullPatientId -%}
{% endif -%}
{% endif -%}
{% include 'Resource/RelatedPerson' NK1: nk1Segment, ID: relatedPersonId -%}
{% include 'Reference/RelatedPerson/Patient' ID: relatedPersonId, REF: fullPatientId -%}
{% endfor -%}

{% assign obxSegmentLists = hl7v2Data | get_segment_lists: 'OBX' -%}
{% for obxSegment in obxSegmentLists.OBX -%}
{% evaluate organizationId_OBX_23 using 'ID/Organization' XON: obxSegment.23 -%}
{% include 'Resource/Organization' OBX: obxSegment, ID: organizationId_OBX_23 -%}

{% evaluate practitionerId_OBX_16 using 'ID/Practitioner' XCN: obxSegment.16 -%}
{% include 'Resource/Practitioner' OBX: obxSegment, ID: practitionerId_OBX_16 -%}

{% evaluate practitionerRoleId_OBX_25 using 'ID/PractitionerRole' XCN: obxSegment.25 -%}
{% include 'Resource/PractitionerRole' OBX: obxSegment, Practitioner_ID_OBX_16: practitionerId_OBX_16, Organization_ID_OBX_23: organizationId_OBX_23, ID: practitionerRoleId_OBX_25 -%}

{% evaluate deviceId_OBX_18 using 'ID/Device' HD: obxSegment.18 -%}
{% include 'Resource/Device' OBX: obxSegment, ID: deviceId_OBX_18 -%}

{% evaluate observationId using 'ID/Observation' OBX: obxSegment, baseId: patientId -%}
{% evaluate practitionerId using 'ID/Practitioner' XCN: obxSegment.16 -%}
{% evaluate practitionerRoleId using 'ID/PractitionerRole' XCN: obxSegment.25 -%}

{% if practitionerId -%}
{% include 'Resource/Practitioner' OBX: obxSegment, ID: practitionerId -%}
{% endif -%}

{% if practitionerRoleId -%}
{% include 'Resource/PractitionerRole' OBX: obxSegment, ID: practitionerRoleId -%}
{% endif %}

{% if observationId -%}
{% include 'Resource/Observation' OBX: obxSegment, Practitioner_ID_OBX_16: practitionerId, PractitionerRole_ID_OBX_25: practitionerRoleId, ID: observationId -%}
{% if patientId -%}
{% include 'Reference/Observation/Subject' ID: observationId, REF: fullPatientId -%}
{% endif -%}
{% endif -%}
{% include 'Resource/Observation' OBX: obxSegment, Practitioner_ID_OBX_16: practitionerId_OBX_16, PractitionerRole_ID_OBX_25: practitionerRoleId_OBX_25, Organization_ID_OBX_23: organizationId_OBX_23, ID: observationId -%}
{% include 'Reference/Observation/Subject' ID: observationId, REF: fullPatientId -%}
{% endfor -%}

{% assign al1SegmentLists = hl7v2Data | get_segment_lists: 'AL1' -%}
{% for al1Segment in al1SegmentLists.AL1 -%}
{% evaluate allergyIntoleranceId using 'ID/AllergyIntolerance' AL1: al1Segment, baseId: patientId -%}
{% if allergyIntoleranceId -%}
{% include 'Resource/AllergyIntolerance' AL1: al1Segment, ID: allergyIntoleranceId -%}
{% if patientId -%}
{% include 'Reference/AllergyIntolerance/Patient' ID: allergyIntoleranceId, REF: fullPatientId -%}
{% endif -%}
{% endif -%}
{% include 'Resource/AllergyIntolerance' AL1: al1Segment, ID: allergyIntoleranceId -%}
{% include 'Reference/AllergyIntolerance/Patient' ID: allergyIntoleranceId, REF: fullPatientId -%}
{% endfor -%}

{% assign dg1SegmentLists = hl7v2Data | get_segment_lists: 'DG1' -%}
{% for dg1Segment in dg1SegmentLists.DG1 -%}
{% evaluate practitionerId using 'ID/Practitioner' XCN: dg1Segment.16 -%}
{% if practitionerId -%}
{% include 'Resource/Practitioner' DG1: dg1Segment, ID: practitionerId -%}
{% endif -%}
{% evaluate practitionerId_DG1_16 using 'ID/Practitioner' XCN: dg1Segment.16 -%}
{% include 'Resource/Practitioner' DG1: dg1Segment, ID: practitionerId_DG1_16 -%}

{% evaluate conditionId using 'ID/Condition' DG1: dg1Segment, baseId: patientId -%}
{% if conditionId -%}
{% assign fullConditionId = conditionId | prepend: 'Condition/' -%}
{% include 'Resource/Condition' DG1: dg1Segment, Practitioner_ID_DG1_16: practitionerId, ID: conditionId -%}
{% if patientId -%}
{% include 'Reference/Condition/Subject' ID: conditionId, REF: fullPatientId -%}
{% endif -%}
{% if encounterId -%}
{% include 'Reference/Encounter/Diagnosis_Condition' ID: encounterId, REF: fullConditionId -%}
{% endif -%}
{% endif -%}
{% include 'Resource/Condition' DG1: dg1Segment, Practitioner_ID_DG1_16: practitionerId_DG1_16, ID: conditionId -%}
{% include 'Reference/Condition/Subject' ID: conditionId, REF: fullPatientId -%}

{% assign fullConditionId = conditionId | prepend: 'Condition/' -%}
{% include 'Reference/Encounter/Diagnosis_Condition' ID: encounterId, REF: fullConditionId -%}
{% endfor -%}
]
}
Loading

0 comments on commit 9ac2c36

Please sign in to comment.