fhirpath-rb 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +25 -0
- data/CLAUDE.md +59 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/ext/fhir_path_parser/extconf.rb +40 -0
- data/lib/fhirpath/engine/evaluators/expressions.rb +59 -0
- data/lib/fhirpath/engine/evaluators/literals.rb +70 -0
- data/lib/fhirpath/engine/evaluators/member_invocation.rb +109 -0
- data/lib/fhirpath/engine/evaluators.rb +130 -0
- data/lib/fhirpath/engine/invocations/aggregate.rb +48 -0
- data/lib/fhirpath/engine/invocations/collections.rb +32 -0
- data/lib/fhirpath/engine/invocations/combining.rb +30 -0
- data/lib/fhirpath/engine/invocations/datetime.rb +34 -0
- data/lib/fhirpath/engine/invocations/equality.rb +95 -0
- data/lib/fhirpath/engine/invocations/equivalence.rb +138 -0
- data/lib/fhirpath/engine/invocations/existence.rb +117 -0
- data/lib/fhirpath/engine/invocations/filtering.rb +107 -0
- data/lib/fhirpath/engine/invocations/inequality.rb +92 -0
- data/lib/fhirpath/engine/invocations/logic.rb +74 -0
- data/lib/fhirpath/engine/invocations/math.rb +109 -0
- data/lib/fhirpath/engine/invocations/math_functions.rb +121 -0
- data/lib/fhirpath/engine/invocations/misc.rb +144 -0
- data/lib/fhirpath/engine/invocations/misc_converts_to.rb +91 -0
- data/lib/fhirpath/engine/invocations/navigation.rb +102 -0
- data/lib/fhirpath/engine/invocations/registry_strings_and_math.rb +48 -0
- data/lib/fhirpath/engine/invocations/strings.rb +139 -0
- data/lib/fhirpath/engine/invocations/strings_encoding.rb +68 -0
- data/lib/fhirpath/engine/invocations/subsetting.rb +15 -0
- data/lib/fhirpath/engine/invocations/types.rb +36 -0
- data/lib/fhirpath/engine/invocations.rb +118 -0
- data/lib/fhirpath/engine/nodes/fp_date_time.rb +140 -0
- data/lib/fhirpath/engine/nodes/fp_date_time_arithmetic.rb +112 -0
- data/lib/fhirpath/engine/nodes/fp_quantity.rb +142 -0
- data/lib/fhirpath/engine/nodes/fp_quantity_deep_equal.rb +67 -0
- data/lib/fhirpath/engine/nodes/fp_time.rb +139 -0
- data/lib/fhirpath/engine/nodes/fp_time_arithmetic.rb +111 -0
- data/lib/fhirpath/engine/nodes/resource_node.rb +66 -0
- data/lib/fhirpath/engine/nodes/type_info.rb +92 -0
- data/lib/fhirpath/engine/param_resolution.rb +74 -0
- data/lib/fhirpath/engine/util.rb +117 -0
- data/lib/fhirpath/engine.rb +100 -0
- data/lib/fhirpath/models/dstu2/choiceTypePaths.json +1041 -0
- data/lib/fhirpath/models/dstu2/path2Type.json +4728 -0
- data/lib/fhirpath/models/dstu2/pathsDefinedElsewhere.json +21 -0
- data/lib/fhirpath/models/dstu2/type2Parent.json +141 -0
- data/lib/fhirpath/models/r4/choiceTypePaths.json +1365 -0
- data/lib/fhirpath/models/r4/path2Type.json +7707 -0
- data/lib/fhirpath/models/r4/pathsDefinedElsewhere.json +57 -0
- data/lib/fhirpath/models/r4/type2Parent.json +211 -0
- data/lib/fhirpath/models/r5/choiceTypePaths.json +1883 -0
- data/lib/fhirpath/models/r5/path2Type.json +9653 -0
- data/lib/fhirpath/models/r5/pathsDefinedElsewhere.json +80 -0
- data/lib/fhirpath/models/r5/type2Parent.json +234 -0
- data/lib/fhirpath/models/stu3/choiceTypePaths.json +991 -0
- data/lib/fhirpath/models/stu3/path2Type.json +5729 -0
- data/lib/fhirpath/models/stu3/pathsDefinedElsewhere.json +37 -0
- data/lib/fhirpath/models/stu3/type2Parent.json +173 -0
- data/lib/fhirpath/models.rb +25 -0
- data/lib/fhirpath/parser/FHIRPath.g4 +172 -0
- data/lib/fhirpath/parser/ast_builder.rb +51 -0
- data/lib/fhirpath/parser.rb +21 -0
- data/lib/fhirpath/version.rb +5 -0
- data/lib/fhirpath.rb +117 -0
- data/rakelib/parser.rake +146 -0
- data/sig/fhirpath.rbs +14 -0
- metadata +168 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Bundle.entry.link": "Bundle.link",
|
|
3
|
+
"CapabilityStatement.rest.searchParam": "CapabilityStatement.rest.resource.searchParam",
|
|
4
|
+
"ClaimResponse.addItem.adjudication": "ClaimResponse.item.adjudication",
|
|
5
|
+
"ClaimResponse.addItem.detail.adjudication": "ClaimResponse.item.adjudication",
|
|
6
|
+
"ClaimResponse.item.detail.adjudication": "ClaimResponse.item.adjudication",
|
|
7
|
+
"ClaimResponse.item.detail.subDetail.adjudication": "ClaimResponse.item.adjudication",
|
|
8
|
+
"CodeSystem.concept.concept": "CodeSystem.concept",
|
|
9
|
+
"Composition.section.section": "Composition.section",
|
|
10
|
+
"ConceptMap.group.element.target.product": "ConceptMap.group.element.target.dependsOn",
|
|
11
|
+
"Contract.term.group": "Contract.term",
|
|
12
|
+
"ExplanationOfBenefit.addItem.adjudication": "ExplanationOfBenefit.item.adjudication",
|
|
13
|
+
"ExplanationOfBenefit.addItem.detail.adjudication": "ExplanationOfBenefit.item.adjudication",
|
|
14
|
+
"ExplanationOfBenefit.item.detail.adjudication": "ExplanationOfBenefit.item.adjudication",
|
|
15
|
+
"ExplanationOfBenefit.item.detail.subDetail.adjudication": "ExplanationOfBenefit.item.adjudication",
|
|
16
|
+
"GraphDefinition.link.target.link": "GraphDefinition.link",
|
|
17
|
+
"ImplementationGuide.page.page": "ImplementationGuide.page",
|
|
18
|
+
"Observation.component.referenceRange": "Observation.referenceRange",
|
|
19
|
+
"OperationDefinition.parameter.part": "OperationDefinition.parameter",
|
|
20
|
+
"Parameters.parameter.part": "Parameters.parameter",
|
|
21
|
+
"PlanDefinition.action.action": "PlanDefinition.action",
|
|
22
|
+
"Provenance.entity.agent": "Provenance.agent",
|
|
23
|
+
"Questionnaire.item.item": "Questionnaire.item",
|
|
24
|
+
"QuestionnaireResponse.item.answer.item": "QuestionnaireResponse.item",
|
|
25
|
+
"QuestionnaireResponse.item.item": "QuestionnaireResponse.item",
|
|
26
|
+
"RequestGroup.action.action": "RequestGroup.action",
|
|
27
|
+
"StructureMap.group.rule.rule": "StructureMap.group.rule",
|
|
28
|
+
"TestReport.teardown.action.operation": "TestReport.setup.action.operation",
|
|
29
|
+
"TestReport.test.action.assert": "TestReport.setup.action.assert",
|
|
30
|
+
"TestReport.test.action.operation": "TestReport.setup.action.operation",
|
|
31
|
+
"TestScript.teardown.action.operation": "TestScript.setup.action.operation",
|
|
32
|
+
"TestScript.test.action.assert": "TestScript.setup.action.assert",
|
|
33
|
+
"TestScript.test.action.operation": "TestScript.setup.action.operation",
|
|
34
|
+
"ValueSet.compose.exclude": "ValueSet.compose.include",
|
|
35
|
+
"ValueSet.expansion.contains.contains": "ValueSet.expansion.contains",
|
|
36
|
+
"ValueSet.expansion.contains.designation": "ValueSet.compose.include.concept.designation"
|
|
37
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Account": "DomainResource",
|
|
3
|
+
"ActivityDefinition": "DomainResource",
|
|
4
|
+
"Address": "Element",
|
|
5
|
+
"AdverseEvent": "DomainResource",
|
|
6
|
+
"Age": "Quantity",
|
|
7
|
+
"AllergyIntolerance": "DomainResource",
|
|
8
|
+
"Annotation": "Element",
|
|
9
|
+
"Appointment": "DomainResource",
|
|
10
|
+
"AppointmentResponse": "DomainResource",
|
|
11
|
+
"Attachment": "Element",
|
|
12
|
+
"AuditEvent": "DomainResource",
|
|
13
|
+
"BackboneElement": "Element",
|
|
14
|
+
"Basic": "DomainResource",
|
|
15
|
+
"Binary": "Resource",
|
|
16
|
+
"BodySite": "DomainResource",
|
|
17
|
+
"Bundle": "Resource",
|
|
18
|
+
"CapabilityStatement": "DomainResource",
|
|
19
|
+
"CarePlan": "DomainResource",
|
|
20
|
+
"CareTeam": "DomainResource",
|
|
21
|
+
"ChargeItem": "DomainResource",
|
|
22
|
+
"Claim": "DomainResource",
|
|
23
|
+
"ClaimResponse": "DomainResource",
|
|
24
|
+
"ClinicalImpression": "DomainResource",
|
|
25
|
+
"CodeSystem": "DomainResource",
|
|
26
|
+
"CodeableConcept": "Element",
|
|
27
|
+
"Coding": "Element",
|
|
28
|
+
"Communication": "DomainResource",
|
|
29
|
+
"CommunicationRequest": "DomainResource",
|
|
30
|
+
"CompartmentDefinition": "DomainResource",
|
|
31
|
+
"Composition": "DomainResource",
|
|
32
|
+
"ConceptMap": "DomainResource",
|
|
33
|
+
"Condition": "DomainResource",
|
|
34
|
+
"Consent": "DomainResource",
|
|
35
|
+
"ContactDetail": "Element",
|
|
36
|
+
"ContactPoint": "Element",
|
|
37
|
+
"Contract": "DomainResource",
|
|
38
|
+
"Contributor": "Element",
|
|
39
|
+
"Count": "Quantity",
|
|
40
|
+
"Coverage": "DomainResource",
|
|
41
|
+
"DataElement": "DomainResource",
|
|
42
|
+
"DataRequirement": "Element",
|
|
43
|
+
"DetectedIssue": "DomainResource",
|
|
44
|
+
"Device": "DomainResource",
|
|
45
|
+
"DeviceComponent": "DomainResource",
|
|
46
|
+
"DeviceMetric": "DomainResource",
|
|
47
|
+
"DeviceRequest": "DomainResource",
|
|
48
|
+
"DeviceUseStatement": "DomainResource",
|
|
49
|
+
"DiagnosticReport": "DomainResource",
|
|
50
|
+
"Distance": "Quantity",
|
|
51
|
+
"DocumentManifest": "DomainResource",
|
|
52
|
+
"DocumentReference": "DomainResource",
|
|
53
|
+
"DomainResource": "Resource",
|
|
54
|
+
"Dosage": "Element",
|
|
55
|
+
"Duration": "Quantity",
|
|
56
|
+
"ElementDefinition": "Element",
|
|
57
|
+
"EligibilityRequest": "DomainResource",
|
|
58
|
+
"EligibilityResponse": "DomainResource",
|
|
59
|
+
"Encounter": "DomainResource",
|
|
60
|
+
"Endpoint": "DomainResource",
|
|
61
|
+
"EnrollmentRequest": "DomainResource",
|
|
62
|
+
"EnrollmentResponse": "DomainResource",
|
|
63
|
+
"EpisodeOfCare": "DomainResource",
|
|
64
|
+
"ExpansionProfile": "DomainResource",
|
|
65
|
+
"ExplanationOfBenefit": "DomainResource",
|
|
66
|
+
"Extension": "Element",
|
|
67
|
+
"FamilyMemberHistory": "DomainResource",
|
|
68
|
+
"Flag": "DomainResource",
|
|
69
|
+
"Goal": "DomainResource",
|
|
70
|
+
"GraphDefinition": "DomainResource",
|
|
71
|
+
"Group": "DomainResource",
|
|
72
|
+
"GuidanceResponse": "DomainResource",
|
|
73
|
+
"HealthcareService": "DomainResource",
|
|
74
|
+
"HumanName": "Element",
|
|
75
|
+
"Identifier": "Element",
|
|
76
|
+
"ImagingManifest": "DomainResource",
|
|
77
|
+
"ImagingStudy": "DomainResource",
|
|
78
|
+
"Immunization": "DomainResource",
|
|
79
|
+
"ImmunizationRecommendation": "DomainResource",
|
|
80
|
+
"ImplementationGuide": "DomainResource",
|
|
81
|
+
"Library": "DomainResource",
|
|
82
|
+
"Linkage": "DomainResource",
|
|
83
|
+
"List": "DomainResource",
|
|
84
|
+
"Location": "DomainResource",
|
|
85
|
+
"Measure": "DomainResource",
|
|
86
|
+
"MeasureReport": "DomainResource",
|
|
87
|
+
"Media": "DomainResource",
|
|
88
|
+
"Medication": "DomainResource",
|
|
89
|
+
"MedicationAdministration": "DomainResource",
|
|
90
|
+
"MedicationDispense": "DomainResource",
|
|
91
|
+
"MedicationRequest": "DomainResource",
|
|
92
|
+
"MedicationStatement": "DomainResource",
|
|
93
|
+
"MessageDefinition": "DomainResource",
|
|
94
|
+
"MessageHeader": "DomainResource",
|
|
95
|
+
"Meta": "Element",
|
|
96
|
+
"Money": "Quantity",
|
|
97
|
+
"NamingSystem": "DomainResource",
|
|
98
|
+
"Narrative": "Element",
|
|
99
|
+
"NutritionOrder": "DomainResource",
|
|
100
|
+
"Observation": "DomainResource",
|
|
101
|
+
"OperationDefinition": "DomainResource",
|
|
102
|
+
"OperationOutcome": "DomainResource",
|
|
103
|
+
"Organization": "DomainResource",
|
|
104
|
+
"ParameterDefinition": "Element",
|
|
105
|
+
"Parameters": "Resource",
|
|
106
|
+
"Patient": "DomainResource",
|
|
107
|
+
"PaymentNotice": "DomainResource",
|
|
108
|
+
"PaymentReconciliation": "DomainResource",
|
|
109
|
+
"Period": "Element",
|
|
110
|
+
"Person": "DomainResource",
|
|
111
|
+
"PlanDefinition": "DomainResource",
|
|
112
|
+
"Practitioner": "DomainResource",
|
|
113
|
+
"PractitionerRole": "DomainResource",
|
|
114
|
+
"Procedure": "DomainResource",
|
|
115
|
+
"ProcedureRequest": "DomainResource",
|
|
116
|
+
"ProcessRequest": "DomainResource",
|
|
117
|
+
"ProcessResponse": "DomainResource",
|
|
118
|
+
"Provenance": "DomainResource",
|
|
119
|
+
"Quantity": "Element",
|
|
120
|
+
"Questionnaire": "DomainResource",
|
|
121
|
+
"QuestionnaireResponse": "DomainResource",
|
|
122
|
+
"Range": "Element",
|
|
123
|
+
"Ratio": "Element",
|
|
124
|
+
"Reference": "Element",
|
|
125
|
+
"ReferralRequest": "DomainResource",
|
|
126
|
+
"RelatedArtifact": "Element",
|
|
127
|
+
"RelatedPerson": "DomainResource",
|
|
128
|
+
"RequestGroup": "DomainResource",
|
|
129
|
+
"ResearchStudy": "DomainResource",
|
|
130
|
+
"ResearchSubject": "DomainResource",
|
|
131
|
+
"RiskAssessment": "DomainResource",
|
|
132
|
+
"SampledData": "Element",
|
|
133
|
+
"Schedule": "DomainResource",
|
|
134
|
+
"SearchParameter": "DomainResource",
|
|
135
|
+
"Sequence": "DomainResource",
|
|
136
|
+
"ServiceDefinition": "DomainResource",
|
|
137
|
+
"Signature": "Element",
|
|
138
|
+
"SimpleQuantity": "Quantity",
|
|
139
|
+
"Slot": "DomainResource",
|
|
140
|
+
"Specimen": "DomainResource",
|
|
141
|
+
"StructureDefinition": "DomainResource",
|
|
142
|
+
"StructureMap": "DomainResource",
|
|
143
|
+
"Subscription": "DomainResource",
|
|
144
|
+
"Substance": "DomainResource",
|
|
145
|
+
"SupplyDelivery": "DomainResource",
|
|
146
|
+
"SupplyRequest": "DomainResource",
|
|
147
|
+
"Task": "DomainResource",
|
|
148
|
+
"TestReport": "DomainResource",
|
|
149
|
+
"TestScript": "DomainResource",
|
|
150
|
+
"Timing": "Element",
|
|
151
|
+
"TriggerDefinition": "Element",
|
|
152
|
+
"UsageContext": "Element",
|
|
153
|
+
"ValueSet": "DomainResource",
|
|
154
|
+
"VisionPrescription": "DomainResource",
|
|
155
|
+
"base64Binary": "Element",
|
|
156
|
+
"boolean": "Element",
|
|
157
|
+
"code": "string",
|
|
158
|
+
"date": "Element",
|
|
159
|
+
"dateTime": "Element",
|
|
160
|
+
"decimal": "Element",
|
|
161
|
+
"id": "string",
|
|
162
|
+
"instant": "Element",
|
|
163
|
+
"integer": "Element",
|
|
164
|
+
"markdown": "string",
|
|
165
|
+
"oid": "uri",
|
|
166
|
+
"positiveInt": "integer",
|
|
167
|
+
"string": "Element",
|
|
168
|
+
"time": "Element",
|
|
169
|
+
"unsignedInt": "integer",
|
|
170
|
+
"uri": "Element",
|
|
171
|
+
"uuid": "uri",
|
|
172
|
+
"xhtml": "Element"
|
|
173
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Fhirpath
|
|
6
|
+
# Ruby port of fhirpath-py's fhirpathpy/models/__init__.py: loads the per-FHIR-version model
|
|
7
|
+
# data (choice-type expansion, path-to-type mapping, ...) used to disambiguate polymorphic
|
|
8
|
+
# fields like `Patient.deceased[x]`. Each subdirectory here (r4, r5, stu3, dstu2) holds the
|
|
9
|
+
# JSON files copied verbatim from fhirpath-py's model data for that version.
|
|
10
|
+
module Models
|
|
11
|
+
DIR = File.join(__dir__, "models")
|
|
12
|
+
|
|
13
|
+
REGISTRY = Dir.children(DIR).sort.each_with_object({}) do |version, models|
|
|
14
|
+
next unless File.directory?(File.join(DIR, version))
|
|
15
|
+
|
|
16
|
+
models[version] = Dir.glob(File.join(DIR, version, "*.json")).each_with_object({}) do |path, data|
|
|
17
|
+
data[File.basename(path, ".json")] = JSON.parse(File.read(path))
|
|
18
|
+
end
|
|
19
|
+
end.freeze
|
|
20
|
+
|
|
21
|
+
def self.[](name)
|
|
22
|
+
REGISTRY[name]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
grammar FHIRPath;
|
|
2
|
+
|
|
3
|
+
// Grammar rules
|
|
4
|
+
// [FHIRPath](http://hl7.org/fhirpath/N1) Normative Release
|
|
5
|
+
|
|
6
|
+
//prog: line (line)*;
|
|
7
|
+
//line: ID ( '(' expr ')') ':' expr '\r'? '\n';
|
|
8
|
+
entireExpression
|
|
9
|
+
: expression EOF
|
|
10
|
+
;
|
|
11
|
+
|
|
12
|
+
expression
|
|
13
|
+
: term #termExpression
|
|
14
|
+
| expression '.' invocation #invocationExpression
|
|
15
|
+
| expression '[' expression ']' #indexerExpression
|
|
16
|
+
| ('+' | '-') expression #polarityExpression
|
|
17
|
+
| expression ('*' | '/' | 'div' | 'mod') expression #multiplicativeExpression
|
|
18
|
+
| expression ('+' | '-' | '&') expression #additiveExpression
|
|
19
|
+
| expression '|' expression #unionExpression
|
|
20
|
+
| expression ('<=' | '<' | '>' | '>=') expression #inequalityExpression
|
|
21
|
+
| expression ('is' | 'as') typeSpecifier #typeExpression
|
|
22
|
+
| expression ('=' | '~' | '!=' | '!~') expression #equalityExpression
|
|
23
|
+
| expression ('in' | 'contains') expression #membershipExpression
|
|
24
|
+
| expression 'and' expression #andExpression
|
|
25
|
+
| expression ('or' | 'xor') expression #orExpression
|
|
26
|
+
| expression 'implies' expression #impliesExpression
|
|
27
|
+
//| (IDENTIFIER)? '=>' expression #lambdaExpression
|
|
28
|
+
;
|
|
29
|
+
|
|
30
|
+
term
|
|
31
|
+
: invocation #invocationTerm
|
|
32
|
+
| literal #literalTerm
|
|
33
|
+
| externalConstant #externalConstantTerm
|
|
34
|
+
| '(' expression ')' #parenthesizedTerm
|
|
35
|
+
;
|
|
36
|
+
|
|
37
|
+
literal
|
|
38
|
+
: '{' '}' #nullLiteral
|
|
39
|
+
| ('true' | 'false') #booleanLiteral
|
|
40
|
+
| STRING #stringLiteral
|
|
41
|
+
| NUMBER #numberLiteral
|
|
42
|
+
| DATETIME #dateTimeLiteral
|
|
43
|
+
| TIME #timeLiteral
|
|
44
|
+
| quantity #quantityLiteral
|
|
45
|
+
;
|
|
46
|
+
|
|
47
|
+
externalConstant
|
|
48
|
+
: '%' ( identifier | STRING )
|
|
49
|
+
;
|
|
50
|
+
|
|
51
|
+
invocation // Terms that can be used after the function/member invocation '.'
|
|
52
|
+
: identifier #memberInvocation
|
|
53
|
+
| functn #functionInvocation
|
|
54
|
+
| '$this' #thisInvocation
|
|
55
|
+
| '$index' #indexInvocation
|
|
56
|
+
| '$total' #totalInvocation
|
|
57
|
+
;
|
|
58
|
+
|
|
59
|
+
functn
|
|
60
|
+
: identifier '(' paramList? ')'
|
|
61
|
+
;
|
|
62
|
+
|
|
63
|
+
paramList
|
|
64
|
+
: expression (',' expression)*
|
|
65
|
+
;
|
|
66
|
+
|
|
67
|
+
quantity
|
|
68
|
+
: NUMBER unit?
|
|
69
|
+
;
|
|
70
|
+
|
|
71
|
+
unit
|
|
72
|
+
: dateTimePrecision
|
|
73
|
+
| pluralDateTimePrecision
|
|
74
|
+
| STRING // UCUM syntax for units of measure
|
|
75
|
+
;
|
|
76
|
+
|
|
77
|
+
dateTimePrecision
|
|
78
|
+
: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond'
|
|
79
|
+
;
|
|
80
|
+
|
|
81
|
+
pluralDateTimePrecision
|
|
82
|
+
: 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds'
|
|
83
|
+
;
|
|
84
|
+
|
|
85
|
+
typeSpecifier
|
|
86
|
+
: qualifiedIdentifier
|
|
87
|
+
;
|
|
88
|
+
|
|
89
|
+
qualifiedIdentifier
|
|
90
|
+
: identifier ('.' identifier)*
|
|
91
|
+
;
|
|
92
|
+
|
|
93
|
+
identifier
|
|
94
|
+
: IDENTIFIER
|
|
95
|
+
| DELIMITEDIDENTIFIER
|
|
96
|
+
| 'as'
|
|
97
|
+
| 'is'
|
|
98
|
+
| 'contains'
|
|
99
|
+
| 'in'
|
|
100
|
+
;
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/****************************************************************
|
|
104
|
+
Lexical rules
|
|
105
|
+
*****************************************************************/
|
|
106
|
+
|
|
107
|
+
DATETIME
|
|
108
|
+
: '@'
|
|
109
|
+
[0-9][0-9][0-9][0-9] // year
|
|
110
|
+
(
|
|
111
|
+
'-'[0-9][0-9] // month
|
|
112
|
+
(
|
|
113
|
+
'-'[0-9][0-9] // day
|
|
114
|
+
(
|
|
115
|
+
'T' TIMEFORMAT
|
|
116
|
+
)?
|
|
117
|
+
)?
|
|
118
|
+
)?
|
|
119
|
+
'Z'? // UTC specifier
|
|
120
|
+
;
|
|
121
|
+
|
|
122
|
+
TIME
|
|
123
|
+
: '@' 'T' TIMEFORMAT
|
|
124
|
+
;
|
|
125
|
+
|
|
126
|
+
fragment TIMEFORMAT
|
|
127
|
+
:
|
|
128
|
+
[0-9][0-9] (':'[0-9][0-9] (':'[0-9][0-9] ('.'[0-9]+)?)?)?
|
|
129
|
+
('Z' | ('+' | '-') [0-9][0-9]':'[0-9][0-9])? // timezone
|
|
130
|
+
;
|
|
131
|
+
|
|
132
|
+
IDENTIFIER
|
|
133
|
+
: ([A-Za-z] | '_')([A-Za-z0-9] | '_')* // Added _ to support CQL (FHIR could constrain it out)
|
|
134
|
+
;
|
|
135
|
+
|
|
136
|
+
DELIMITEDIDENTIFIER
|
|
137
|
+
: '`' (ESC | ~[\\`])* '`'
|
|
138
|
+
;
|
|
139
|
+
|
|
140
|
+
STRING
|
|
141
|
+
: '\'' (ESC | ~['])* '\''
|
|
142
|
+
;
|
|
143
|
+
|
|
144
|
+
// Also allows leading zeroes now (just like CQL and XSD)
|
|
145
|
+
NUMBER
|
|
146
|
+
: [0-9]+('.' [0-9]+)?
|
|
147
|
+
;
|
|
148
|
+
|
|
149
|
+
// Pipe whitespace to the HIDDEN channel to support retrieving source text through the parser.
|
|
150
|
+
WS
|
|
151
|
+
: [ \r\n\t]+ -> channel(HIDDEN)
|
|
152
|
+
;
|
|
153
|
+
|
|
154
|
+
COMMENT
|
|
155
|
+
: '/*' .*? '*/' -> channel(HIDDEN)
|
|
156
|
+
;
|
|
157
|
+
|
|
158
|
+
LINE_COMMENT
|
|
159
|
+
: '//' ~[\r\n]* -> channel(HIDDEN)
|
|
160
|
+
;
|
|
161
|
+
|
|
162
|
+
fragment ESC
|
|
163
|
+
: '\\' ([`'\\/fnrt] | UNICODE) // allow \`, \', \\, \/, \f, etc. and \uXXX
|
|
164
|
+
;
|
|
165
|
+
|
|
166
|
+
fragment UNICODE
|
|
167
|
+
: 'u' HEX HEX HEX HEX
|
|
168
|
+
;
|
|
169
|
+
|
|
170
|
+
fragment HEX
|
|
171
|
+
: [0-9a-fA-F]
|
|
172
|
+
;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fhirpath
|
|
4
|
+
module Parser
|
|
5
|
+
# Walks the native ANTLR4 parse tree (see Fhirpath::Parser.parse) and builds the same
|
|
6
|
+
# generic AST shape as fhirpath-py's ASTPathListener: each node is a Hash with "type",
|
|
7
|
+
# optional "text", "terminalNodeText" (direct terminal children's text), and optional
|
|
8
|
+
# "children" (present only when there's at least one non-terminal child).
|
|
9
|
+
module AstBuilder
|
|
10
|
+
# Port of ASTPathListener.py's has_node_type_text: which node types carry source text.
|
|
11
|
+
TEXT_NODE_TYPES = %w[
|
|
12
|
+
LiteralTerm
|
|
13
|
+
Identifier
|
|
14
|
+
TypeSpecifier
|
|
15
|
+
InvocationExpression
|
|
16
|
+
TermExpression
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def build(ctx)
|
|
21
|
+
type = node_type(ctx)
|
|
22
|
+
node = { "type" => type, "terminalNodeText" => terminal_text(ctx) }
|
|
23
|
+
node["text"] = ctx.text if text_node?(type)
|
|
24
|
+
|
|
25
|
+
children = rule_children(ctx).map { |child| build(child) }
|
|
26
|
+
node["children"] = children unless children.empty?
|
|
27
|
+
|
|
28
|
+
node
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def terminal_text(ctx)
|
|
34
|
+
ctx.children.select { |child| child.is_a?(::FHIRPathParser::TerminalNodeImpl) }.map(&:text)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rule_children(ctx)
|
|
38
|
+
ctx.children.reject { |child| child.is_a?(::FHIRPathParser::TerminalNodeImpl) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def node_type(ctx)
|
|
42
|
+
ctx.type_name.split("::").last.delete_suffix("Context")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def text_node?(type)
|
|
46
|
+
type.end_with?("Literal") || TEXT_NODE_TYPES.include?(type)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../ext/fhir_path_parser/fhir_path_parser"
|
|
4
|
+
require_relative "parser/ast_builder"
|
|
5
|
+
|
|
6
|
+
module Fhirpath
|
|
7
|
+
# Ruby port of fhirpath-py's fhirpathpy/parser package. Parses a FHIRPath expression string
|
|
8
|
+
# into the same generic AST shape fhirpath-py produces (see AstBuilder), using a native
|
|
9
|
+
# ANTLR4 parser generated from FHIRPath.g4 via the antlr4-native gem — run
|
|
10
|
+
# `rake parser:setup compile` once before requiring this file (see rakelib/parser.rake).
|
|
11
|
+
module Parser
|
|
12
|
+
def self.parse(expression)
|
|
13
|
+
parser = FHIRPathParser::Parser.parse(expression)
|
|
14
|
+
root = parser.expression
|
|
15
|
+
|
|
16
|
+
raise Fhirpath::Error, "Failed to parse #{expression.inspect}" if parser.syntax_error_count.positive?
|
|
17
|
+
|
|
18
|
+
{ "children" => [AstBuilder.build(root)] }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/fhirpath.rb
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fhir_models"
|
|
4
|
+
|
|
5
|
+
require_relative "fhirpath/version"
|
|
6
|
+
require_relative "fhirpath/parser"
|
|
7
|
+
require_relative "fhirpath/engine"
|
|
8
|
+
require_relative "fhirpath/models"
|
|
9
|
+
|
|
10
|
+
# Ruby implementation of FHIRPath (https://hl7.org/fhirpath/), a port of fhirpath-py
|
|
11
|
+
# (https://github.com/beda-software/fhirpath-py).
|
|
12
|
+
module Fhirpath
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
# `path` may be a plain FHIRPath expression string, or (mirroring fhirpath-py's evaluate) a
|
|
16
|
+
# {"expression" => ..., "base" => ...} Hash: "base" tags the resource with the FHIRPath it
|
|
17
|
+
# would have been reached at (e.g. "QuestionnaireResponse.item") without actually navigating
|
|
18
|
+
# there, so model-based navigation (e.g. choice-type resolution) below that point works when
|
|
19
|
+
# evaluating just a fragment of a resource.
|
|
20
|
+
def self.evaluate(resource, path, context = {}, model = nil, options = {})
|
|
21
|
+
if path.is_a?(::Hash)
|
|
22
|
+
node = Parser.parse(path["expression"])
|
|
23
|
+
resource = Engine::Nodes::ResourceNode.create_node(resource, path["base"]) if path["base"]
|
|
24
|
+
else
|
|
25
|
+
node = Parser.parse(path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
apply_parsed_path(resource, node, context, model, options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.compile(path, model = nil, options = {})
|
|
32
|
+
node = Parser.parse(path)
|
|
33
|
+
->(resource, context = nil) { apply_parsed_path(resource, node, context || {}, model, options) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Ruby port of fhirpath-py's compile_as_array (fhirpathpy/__init__.py): like `compile`, but
|
|
37
|
+
# the returned callable accepts a fhir_models `input_type` instance instead of a plain Hash,
|
|
38
|
+
# and wraps each result item as an `output_type` instance instead of returning raw data.
|
|
39
|
+
def self.compile_as_array(expression, input_type, output_type, model = nil)
|
|
40
|
+
path_fn = compile(expression, model)
|
|
41
|
+
lambda do |resource, context = nil|
|
|
42
|
+
data = path_fn.call(prepare_data(resource, input_type), context)
|
|
43
|
+
format_result(data, output_type, array: true)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.compile_as_first(expression, input_type, output_type, model = nil)
|
|
48
|
+
path_fn = compile(expression, model)
|
|
49
|
+
lambda do |resource, context = nil|
|
|
50
|
+
data = path_fn.call(prepare_data(resource, input_type), context)
|
|
51
|
+
format_result(data, output_type, array: false)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.prepare_data(resource, input_type)
|
|
56
|
+
raise Error, "Resource type is #{resource.class}, expected #{input_type}" unless resource.is_a?(input_type)
|
|
57
|
+
|
|
58
|
+
return resource if resource.is_a?(::Hash)
|
|
59
|
+
return resource.to_hash if resource.respond_to?(:to_hash)
|
|
60
|
+
|
|
61
|
+
raise Error, "Don't know how to work with type #{resource.class}"
|
|
62
|
+
end
|
|
63
|
+
private_class_method :prepare_data
|
|
64
|
+
|
|
65
|
+
def self.format_result(result, output_type, array:)
|
|
66
|
+
formatted = result.map { |item| format_item(item, output_type) }
|
|
67
|
+
return formatted if array
|
|
68
|
+
|
|
69
|
+
formatted.first
|
|
70
|
+
end
|
|
71
|
+
private_class_method :format_result
|
|
72
|
+
|
|
73
|
+
def self.format_item(item, output_type)
|
|
74
|
+
return item if item.is_a?(output_type)
|
|
75
|
+
return output_type.new(item) if item.is_a?(::Hash) && output_type.is_a?(::Class) && output_type <= ::FHIR::Model
|
|
76
|
+
|
|
77
|
+
raise Error, "Expected result to be #{output_type}, but got #{item.class}"
|
|
78
|
+
end
|
|
79
|
+
private_class_method :format_item
|
|
80
|
+
|
|
81
|
+
# Ruby port of fhirpath-py's apply_parsed_path (fhirpathpy/__init__.py): builds the
|
|
82
|
+
# evaluation context and resolves the AST, then unwraps internal ResourceNode wrappers back
|
|
83
|
+
# into plain data.
|
|
84
|
+
def self.apply_parsed_path(resource, node, context, model, _options)
|
|
85
|
+
data_root = Engine::Util.arraify(resource)
|
|
86
|
+
ctx = {
|
|
87
|
+
root: data_root,
|
|
88
|
+
vars: { "context" => resource, "resource" => resource,
|
|
89
|
+
"ucum" => "http://unitsofmeasure.org" }.merge(context || {}),
|
|
90
|
+
model: model.is_a?(::String) ? Models[model] : model,
|
|
91
|
+
user_invocation_table: {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
visit(Engine.do_eval(ctx, data_root, node["children"][0]))
|
|
95
|
+
end
|
|
96
|
+
private_class_method :apply_parsed_path
|
|
97
|
+
|
|
98
|
+
# Resolves ResourceNode instances back into plain data, dropping internal
|
|
99
|
+
# primitive-extension-only placeholders (`{"extension" => [...]}`) along the way.
|
|
100
|
+
def self.visit(node)
|
|
101
|
+
data = Engine::Util.get_data(node)
|
|
102
|
+
|
|
103
|
+
return visit_array(data) if data.is_a?(::Array)
|
|
104
|
+
return data.transform_values { |value| visit(value) } if data.is_a?(::Hash)
|
|
105
|
+
|
|
106
|
+
data
|
|
107
|
+
end
|
|
108
|
+
private_class_method :visit
|
|
109
|
+
|
|
110
|
+
def self.visit_array(items)
|
|
111
|
+
items.each_with_object([]) do |item, acc|
|
|
112
|
+
visited = visit(item)
|
|
113
|
+
acc << visited unless visited.is_a?(::Hash) && visited.keys == ["extension"]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
private_class_method :visit_array
|
|
117
|
+
end
|