Skip to content

Commit 9ac2c36

Browse files
authored
Merge pull request #212 from microsoft/dotliquid
Update version to v3.4
2 parents b7001db + c3ef964 commit 9ac2c36

File tree

101 files changed

+2584
-972
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+2584
-972
lines changed

README.md

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,90 @@ More details of usage are given in [Template Management CLI tool](docs/TemplateM
8080
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.
8181
>Note!: Template version is aligned with the version of FHIR Converter.
8282
83-
### A note on Resource ID generation
83+
## Usage Notes
8484

85-
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.
85+
### Resource ID generation
8686

87-
There are a set of ID generation [templates](data/Templates/Hl7v2/ID) to help generate FHIR resource IDs from HL7 v2 messages.
87+
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.
8888

89-
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.
89+
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.
9090

9191
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.
9292
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.
9393

9494
Below is an example where an AllergyIntolerance ID is generated, using ID/AllergyIntolerance template, AL1 segment and patient ID as its base ID.
9595
The syntax is `{% evaluate [id] using [template] [variables] -%}`.
96-
```
96+
97+
```liquid
9798
{% evaluate allergyIntoleranceId using 'ID/AllergyIntolerance' AL1: al1Segment, baseId: patientId -%}
9899
```
99100

101+
### Resource validation and post-processing
102+
103+
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.
104+
105+
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.
106+
107+
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.
108+
109+
```C#
110+
using Hl7.Fhir.Model;
111+
using Hl7.Fhir.Serialization;
112+
using System;
113+
using System.Collections.Generic;
114+
using System.Linq;
115+
116+
public class PostProcessor
117+
{
118+
private readonly FhirJsonParser _parser = new FhirJsonParser();
119+
120+
public IEnumerable<Resource> FilterResources(IEnumerable<string> fhirResources)
121+
{
122+
return fhirResources
123+
.Select(fhirResource => _parser.Parse<Resource>(fhirResource))
124+
.Where(resource => !IsEmptyResource(resource))
125+
.Where(resource => !IsIdAbsentResource(resource));
126+
}
127+
128+
public bool IsEmptyResource(Resource resource)
129+
{
130+
try
131+
{
132+
var fhirResource = resource.ToJObject();
133+
var properties = fhirResource.Properties().Select(property => property.Name);
134+
// an empty resource contains no properties other than "resourceType" and "id"
135+
return !properties
136+
.Where(property => !property.Equals("resourceType"))
137+
.Where(property => !property.Equals("id"))
138+
.Any();
139+
}
140+
catch (Exception e)
141+
{
142+
Console.Error.WriteLine(e.Message);
143+
// deal with the exception...
144+
}
145+
146+
return false;
147+
}
148+
149+
public bool IsIdAbsentResource(Resource resource)
150+
{
151+
try
152+
{
153+
return string.IsNullOrWhiteSpace(resource.Id);
154+
}
155+
catch (Exception e)
156+
{
157+
Console.Error.WriteLine(e.Message);
158+
// deal with the exception...
159+
}
160+
return false;
161+
}
162+
}
163+
```
164+
165+
166+
100167
## Reference documentation
101168
- [Filters summary](docs/FiltersSummary.md)
102169
- [Snippet concept](docs/SnippetConcept.md)
Lines changed: 83 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,119 @@
1+
{% assign firstSegments = hl7v2Data | get_first_segments: 'PID|PD1|PV1|PV2|AVR|MSH' -%}
2+
{% assign pr1SegmentLists = hl7v2Data | get_segment_lists: 'PR1' -%}
3+
{% assign nk1SegmentLists = hl7v2Data | get_segment_lists: 'NK1' -%}
4+
{% assign obxSegmentLists = hl7v2Data | get_segment_lists: 'OBX' -%}
5+
{% assign al1SegmentLists = hl7v2Data | get_segment_lists: 'AL1' -%}
6+
{% assign dg1SegmentLists = hl7v2Data | get_segment_lists: 'DG1' -%}
7+
18
{
29
"resourceType": "Bundle",
310
"type": "batch",
11+
{% if firstSegments.MSH.7 -%}
12+
"timestamp":"{{ firstSegments.MSH.7.Value | format_as_date_time }}",
13+
{% endif -%}
14+
"identifier":
15+
{
16+
"value":"{{ firstSegments.MSH.10.Value }}",
17+
},
418
"entry": [
5-
{% assign firstSegments = hl7v2Data | get_first_segments: 'PID|PD1|PV1|PV2|AVR|MSH' -%}
6-
719
{% evaluate messageHeaderId using 'ID/MessageHeader' MSH: firstSegments.MSH -%}
8-
9-
{% if messageHeaderId -%}
10-
{% include 'Resource/MessageHeader' MSH: firstSegments.MSH, ID: messageHeaderID -%}
11-
{% endif -%}
20+
{% include 'Resource/MessageHeader' MSH: firstSegments.MSH, ID: messageHeaderID -%}
1221

1322
{% evaluate patientId using 'ID/Patient' PID: firstSegments.PID, type: 'First' -%}
14-
{% if patientId -%}
15-
{% assign fullPatientId = patientId | prepend: 'Patient/' -%}
16-
{% include 'Resource/Patient' PID: firstSegments.PID, PD1: firstSegments.PD1, ID: patientId -%}
17-
{% endif -%}
18-
19-
{% evaluate provenanceId using 'ID/Provenance' MSH: firstSegments.MSH, baseId: patientId -%}
20-
{% evaluate practitionerId10 using 'ID/Practitioner' XCN: firstSegments.ORC.10 -%}
21-
{% evaluate practitionerId11 using 'ID/Practitioner' XCN: firstSegments.ORC.11 -%}
22-
{% evaluate practitionerId12 using 'ID/Practitioner' XCN: firstSegments.ORC.12 -%}
23-
{% evaluate locationId using 'ID/Location' XON: firstSegments.ORC.21 -%}
24-
25-
{% if practitionerId10 -%}
26-
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId10 -%}
27-
{% endif %}
28-
29-
{% if practitionerId11 -%}
30-
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId11 -%}
31-
{% endif %}
32-
33-
{% if practitionerId12 -%}
34-
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId12 -%}
35-
{% endif %}
36-
37-
{% if locationId -%}
38-
{% include 'Resource/Location' ORC: firstSegments.ORC, ID: locationId -%}
39-
{% endif -%}
40-
41-
{% if provenanceId -%}
42-
{% 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 -%}
43-
{% endif -%}
23+
{% assign fullPatientId = patientId | prepend: 'Patient/' -%}
24+
{% include 'Resource/Patient' PID: firstSegments.PID, PD1: firstSegments.PD1, ID: patientId -%}
25+
26+
{% evaluate practitionerId_ORC_10 using 'ID/Practitioner' XCN: firstSegments.ORC.10 -%}
27+
{% evaluate practitionerId_ORC_11 using 'ID/Practitioner' XCN: firstSegments.ORC.11 -%}
28+
{% evaluate practitionerId_ORC_12 using 'ID/Practitioner' XCN: firstSegments.ORC.12 -%}
29+
{% evaluate practitionerId_PV1_7 using 'ID/Practitioner' XCN: firstSegments.PV1.7 -%}
30+
{% evaluate practitionerId_PV1_8 using 'ID/Practitioner' XCN: firstSegments.PV1.8 -%}
31+
{% evaluate practitionerId_PV1_9 using 'ID/Practitioner' XCN: firstSegments.PV1.9 -%}
32+
{% evaluate practitionerId_PV1_17 using 'ID/Practitioner' XCN: firstSegments.PV1.17 -%}
33+
{% evaluate practitionerId_PV1_52 using 'ID/Practitioner' XCN: firstSegments.PV1.52 -%}
34+
35+
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId_ORC_10 -%}
36+
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId_ORC_11 -%}
37+
{% include 'Resource/Practitioner' ORC: firstSegments.ORC, ID: practitionerId_ORC_12 -%}
38+
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_7 -%}
39+
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_8 -%}
40+
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_9 -%}
41+
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_17 -%}
42+
{% include 'Resource/Practitioner' PV1: firstSegments.PV1, ID: practitionerId_PV1_52 -%}
4443

4544
{% evaluate accountId using 'ID/Account' CX: firstSegments.PID.3 -%}
46-
{% if accountId -%}
47-
{% include 'Resource/Account' PID: firstSegments.PID, ID: accountId -%}
48-
{% endif -%}
45+
{% include 'Resource/Account' PID: firstSegments.PID, ID: accountId -%}
46+
{% include 'Reference/Account/Subject' ID: accountId, REF: fullPatientId -%}
4947

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

54-
{% evaluate locationId3 using 'ID/Location' PL: firstSegments.PV1.3 -%}
55-
{% if locationId3 -%}
56-
{% include 'Resource/Location' PL: firstSegments.PV1.3, ID: locationId3 -%}
57-
{% endif -%}
52+
{% include 'Resource/Location' ORC: firstSegments.ORC, ID: locationId_ORC_21 -%}
53+
{% include 'Resource/Location' PL: firstSegments.PV1.3, ID: locationId_PV1_3 -%}
54+
{% include 'Resource/Location' PL: firstSegments.PV1.6, ID: locationId_PV1_6 -%}
5855

59-
{% evaluate locationId6 using 'ID/Location' PL: firstSegments.PV1.6 -%}
60-
{% if locationId6 -%}
61-
{% include 'Resource/Location' PL: firstSegments.PV1.6, ID: locationId6 -%}
62-
{% endif -%}
63-
64-
{% include 'Resource/Encounter' PV1: firstSegments.PV1, Location_ID_PV1_3: locationId3, Location_ID_PV1_6: locationId6, ID: encounterId -%}
56+
{% evaluate provenanceId using 'ID/Provenance' MSH: firstSegments.MSH, baseId: patientId -%}
57+
{% 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 -%}
6558

66-
{% if patientId -%}
67-
{% include 'Reference/Encounter/Subject' ID: encounterId, REF: fullPatientId -%}
68-
{% endif -%}
69-
{% endif -%}
59+
{% evaluate encounterId using 'ID/Encounter' PV1: firstSegments.PV1, baseId: patientId -%}
60+
{% 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 -%}
61+
{% include 'Reference/Encounter/Subject' ID: encounterId, REF: fullPatientId -%}
7062

71-
{% assign pr1SegmentLists = hl7v2Data | get_segment_lists: 'PR1' -%}
7263
{% for pr1Segment in pr1SegmentLists.PR1 -%}
64+
{% evaluate locationId_PR1_23 using 'ID/Location' PL: pr1Segment.23 -%}
65+
{% include 'Resource/Location' PL: pr1Segment.23, ID: locationId_PR1_23 -%}
66+
7367
{% evaluate procedureId using 'ID/Procedure' PR1: pr1Segment, baseId: patientId -%}
74-
{% if procedureId -%}
75-
{% include 'Resource/Procedure' PR1: pr1Segment, ID: procedureId -%}
76-
77-
{% evaluate locationId using 'ID/Location' PL: pr1Segment.23 -%}
78-
{% if locationId -%}
79-
{% include 'Resource/Location' PL: pr1Segment.23, ID: locationId -%}
80-
{% endif -%}
81-
82-
{% if patientId -%}
83-
{% include 'Reference/Procedure/Subject' ID: procedureId, REF: fullPatientId -%}
84-
{% endif -%}
85-
{% endif -%}
68+
{% include 'Resource/Procedure' PR1: pr1Segment, ID: procedureId -%}
69+
{% include 'Reference/Procedure/Subject' ID: procedureId, REF: fullPatientId -%}
8670
{% endfor -%}
87-
88-
{% assign nk1SegmentLists = hl7v2Data | get_segment_lists: 'NK1' -%}
71+
8972
{% for nk1Segment in nk1SegmentLists.NK1 -%}
90-
{% include 'Resource/Patient' NK1: nk1Segment, ID: patientId -%}
73+
{% evaluate organizationId_NK1_13 using 'ID/Organization' XON: nk1Segment.13 -%}
74+
{% include 'Resource/Organization' NK1: nk1Segment, ID: organizationId_NK1_13 -%}
75+
76+
{% include 'Resource/Patient' NK1: nk1Segment, Organization_ID_NK1_13: organizationId_NK1_13, ID: patientId -%}
9177

9278
{% evaluate relatedPersonId using 'ID/RelatedPerson' NK1: nk1Segment, baseId: patientId -%}
93-
{% if relatedPersonId -%}
94-
{% include 'Resource/RelatedPerson' NK1: nk1Segment, ID: relatedPersonId -%}
95-
{% if patientId -%}
96-
{% include 'Reference/RelatedPerson/Patient' ID: relatedPersonId, REF: fullPatientId -%}
97-
{% endif -%}
98-
{% endif -%}
79+
{% include 'Resource/RelatedPerson' NK1: nk1Segment, ID: relatedPersonId -%}
80+
{% include 'Reference/RelatedPerson/Patient' ID: relatedPersonId, REF: fullPatientId -%}
9981
{% endfor -%}
10082

101-
{% assign obxSegmentLists = hl7v2Data | get_segment_lists: 'OBX' -%}
10283
{% for obxSegment in obxSegmentLists.OBX -%}
84+
{% evaluate organizationId_OBX_23 using 'ID/Organization' XON: obxSegment.23 -%}
85+
{% include 'Resource/Organization' OBX: obxSegment, ID: organizationId_OBX_23 -%}
86+
87+
{% evaluate practitionerId_OBX_16 using 'ID/Practitioner' XCN: obxSegment.16 -%}
88+
{% include 'Resource/Practitioner' OBX: obxSegment, ID: practitionerId_OBX_16 -%}
89+
90+
{% evaluate practitionerRoleId_OBX_25 using 'ID/PractitionerRole' XCN: obxSegment.25 -%}
91+
{% include 'Resource/PractitionerRole' OBX: obxSegment, Practitioner_ID_OBX_16: practitionerId_OBX_16, Organization_ID_OBX_23: organizationId_OBX_23, ID: practitionerRoleId_OBX_25 -%}
92+
93+
{% evaluate deviceId_OBX_18 using 'ID/Device' HD: obxSegment.18 -%}
94+
{% include 'Resource/Device' OBX: obxSegment, ID: deviceId_OBX_18 -%}
95+
10396
{% evaluate observationId using 'ID/Observation' OBX: obxSegment, baseId: patientId -%}
104-
{% evaluate practitionerId using 'ID/Practitioner' XCN: obxSegment.16 -%}
105-
{% evaluate practitionerRoleId using 'ID/PractitionerRole' XCN: obxSegment.25 -%}
106-
107-
{% if practitionerId -%}
108-
{% include 'Resource/Practitioner' OBX: obxSegment, ID: practitionerId -%}
109-
{% endif -%}
110-
111-
{% if practitionerRoleId -%}
112-
{% include 'Resource/PractitionerRole' OBX: obxSegment, ID: practitionerRoleId -%}
113-
{% endif %}
114-
115-
{% if observationId -%}
116-
{% include 'Resource/Observation' OBX: obxSegment, Practitioner_ID_OBX_16: practitionerId, PractitionerRole_ID_OBX_25: practitionerRoleId, ID: observationId -%}
117-
{% if patientId -%}
118-
{% include 'Reference/Observation/Subject' ID: observationId, REF: fullPatientId -%}
119-
{% endif -%}
120-
{% endif -%}
97+
{% 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 -%}
98+
{% include 'Reference/Observation/Subject' ID: observationId, REF: fullPatientId -%}
12199
{% endfor -%}
122100

123-
{% assign al1SegmentLists = hl7v2Data | get_segment_lists: 'AL1' -%}
124101
{% for al1Segment in al1SegmentLists.AL1 -%}
125102
{% evaluate allergyIntoleranceId using 'ID/AllergyIntolerance' AL1: al1Segment, baseId: patientId -%}
126-
{% if allergyIntoleranceId -%}
127-
{% include 'Resource/AllergyIntolerance' AL1: al1Segment, ID: allergyIntoleranceId -%}
128-
{% if patientId -%}
129-
{% include 'Reference/AllergyIntolerance/Patient' ID: allergyIntoleranceId, REF: fullPatientId -%}
130-
{% endif -%}
131-
{% endif -%}
103+
{% include 'Resource/AllergyIntolerance' AL1: al1Segment, ID: allergyIntoleranceId -%}
104+
{% include 'Reference/AllergyIntolerance/Patient' ID: allergyIntoleranceId, REF: fullPatientId -%}
132105
{% endfor -%}
133106

134-
{% assign dg1SegmentLists = hl7v2Data | get_segment_lists: 'DG1' -%}
135107
{% for dg1Segment in dg1SegmentLists.DG1 -%}
136-
{% evaluate practitionerId using 'ID/Practitioner' XCN: dg1Segment.16 -%}
137-
{% if practitionerId -%}
138-
{% include 'Resource/Practitioner' DG1: dg1Segment, ID: practitionerId -%}
139-
{% endif -%}
108+
{% evaluate practitionerId_DG1_16 using 'ID/Practitioner' XCN: dg1Segment.16 -%}
109+
{% include 'Resource/Practitioner' DG1: dg1Segment, ID: practitionerId_DG1_16 -%}
140110

141111
{% evaluate conditionId using 'ID/Condition' DG1: dg1Segment, baseId: patientId -%}
142-
{% if conditionId -%}
143-
{% assign fullConditionId = conditionId | prepend: 'Condition/' -%}
144-
{% include 'Resource/Condition' DG1: dg1Segment, Practitioner_ID_DG1_16: practitionerId, ID: conditionId -%}
145-
{% if patientId -%}
146-
{% include 'Reference/Condition/Subject' ID: conditionId, REF: fullPatientId -%}
147-
{% endif -%}
148-
{% if encounterId -%}
149-
{% include 'Reference/Encounter/Diagnosis_Condition' ID: encounterId, REF: fullConditionId -%}
150-
{% endif -%}
151-
{% endif -%}
112+
{% include 'Resource/Condition' DG1: dg1Segment, Practitioner_ID_DG1_16: practitionerId_DG1_16, ID: conditionId -%}
113+
{% include 'Reference/Condition/Subject' ID: conditionId, REF: fullPatientId -%}
114+
115+
{% assign fullConditionId = conditionId | prepend: 'Condition/' -%}
116+
{% include 'Reference/Encounter/Diagnosis_Condition' ID: encounterId, REF: fullConditionId -%}
152117
{% endfor -%}
153118
]
154119
}

0 commit comments

Comments
 (0)