openehr 1.3.0 → 2.0.0
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 +4 -4
- data/README.rdoc +93 -12
- data/lib/openehr/am/archetype/constraint_model/primitive.rb +204 -6
- data/lib/openehr/am/archetype/constraint_model.rb +228 -4
- data/lib/openehr/am/archetype/ontology.rb +2 -2
- data/lib/openehr/am/archetype.rb +101 -2
- data/lib/openehr/am/openehr_profile/data_types/basic.rb +28 -0
- data/lib/openehr/am/openehr_profile/data_types/quantity.rb +88 -0
- data/lib/openehr/am/openehr_profile/data_types/text.rb +23 -0
- data/lib/openehr/am/template.rb +25 -3
- data/lib/openehr/aql/engine/binding.rb +13 -0
- data/lib/openehr/aql/engine/contains_resolver.rb +161 -0
- data/lib/openehr/aql/engine/dataset.rb +122 -0
- data/lib/openehr/aql/engine/path_evaluator.rb +137 -0
- data/lib/openehr/aql/engine/predicate_evaluator.rb +65 -0
- data/lib/openehr/aql/engine.rb +119 -0
- data/lib/openehr/aql/errors.rb +27 -0
- data/lib/openehr/aql/lexer.rb +295 -0
- data/lib/openehr/aql/model/containment.rb +45 -0
- data/lib/openehr/aql/model/from_clause.rb +35 -0
- data/lib/openehr/aql/model/function_call.rb +56 -0
- data/lib/openehr/aql/model/identified_path.rb +20 -0
- data/lib/openehr/aql/model/literal.rb +15 -0
- data/lib/openehr/aql/model/object_path.rb +34 -0
- data/lib/openehr/aql/model/order_by_and_limit.rb +50 -0
- data/lib/openehr/aql/model/predicate.rb +84 -0
- data/lib/openehr/aql/model/query.rb +24 -0
- data/lib/openehr/aql/model/select_clause.rb +29 -0
- data/lib/openehr/aql/model/where_clause.rb +105 -0
- data/lib/openehr/aql/model.rb +14 -0
- data/lib/openehr/aql/parser.rb +484 -0
- data/lib/openehr/aql/result_set.rb +54 -0
- data/lib/openehr/aql.rb +28 -0
- data/lib/openehr/assumed_library_types.rb +68 -17
- data/lib/openehr/parser/adl_grammar.tt +29 -13
- data/lib/openehr/parser/adl_parser.rb +18 -4
- data/lib/openehr/parser/archetype_validator.rb +126 -0
- data/lib/openehr/parser/exception.rb +13 -0
- data/lib/openehr/parser/opt_parser.rb +162 -9
- data/lib/openehr/parser.rb +2 -0
- data/lib/openehr/path.rb +195 -0
- data/lib/openehr/rm/common/archetyped.rb +116 -6
- data/lib/openehr/rm/common/change_control.rb +3 -8
- data/lib/openehr/rm/common/directory.rb +4 -0
- data/lib/openehr/rm/common/generic.rb +24 -10
- data/lib/openehr/rm/composition/content/entry.rb +24 -5
- data/lib/openehr/rm/composition/content/navigation.rb +2 -1
- data/lib/openehr/rm/composition.rb +19 -2
- data/lib/openehr/rm/data_structures/history.rb +3 -0
- data/lib/openehr/rm/data_structures/item_structure/representation.rb +13 -9
- data/lib/openehr/rm/data_structures/item_structure.rb +28 -15
- data/lib/openehr/rm/data_types/encapsulated.rb +23 -3
- data/lib/openehr/rm/data_types/quantity/date_time.rb +9 -1
- data/lib/openehr/rm/data_types/quantity.rb +66 -14
- data/lib/openehr/rm/data_types/text.rb +8 -0
- data/lib/openehr/rm/data_types/time_specification.rb +5 -4
- data/lib/openehr/rm/demographic.rb +4 -3
- data/lib/openehr/rm/ehr.rb +20 -1
- data/lib/openehr/rm/factory.rb +234 -6
- data/lib/openehr/rm/integration.rb +1 -0
- data/lib/openehr/rm/support/identification.rb +37 -8
- data/lib/openehr/rm/support/measurement.rb +32 -0
- data/lib/openehr/rm/type_name.rb +90 -0
- data/lib/openehr/rm.rb +3 -0
- data/lib/openehr/serializer/adl_serializer.rb +335 -0
- data/lib/openehr/serializer/base.rb +20 -0
- data/lib/openehr/serializer/opt_serializer.rb +49 -0
- data/lib/openehr/serializer/rm_json_serializer.rb +62 -0
- data/lib/openehr/serializer/xml_serializer.rb +260 -0
- data/lib/openehr/serializer.rb +5 -291
- data/lib/openehr/terminology_service.rb +44 -0
- data/lib/openehr/version.rb +1 -1
- data/lib/openehr.rb +5 -2
- metadata +35 -7
- data/lib/openehr/parser/validator.rb +0 -18
- data/lib/openehr/parser/xml_parser.rb +0 -13
- data/lib/openehr/rm/data_types/charset_extract.rb +0 -24
- data/lib/openehr/writer.rb +0 -12
|
@@ -71,6 +71,35 @@ module OpenEHR
|
|
|
71
71
|
(@lower_included == value.lower_included?) &&
|
|
72
72
|
(@upper_included == value.upper_included?)
|
|
73
73
|
end
|
|
74
|
+
|
|
75
|
+
# True if every value this interval has is also had by other (a
|
|
76
|
+
# standard subset test, e.g. for conformance checks: does a
|
|
77
|
+
# constraint's occurrences/existence/cardinality interval fit
|
|
78
|
+
# within its parent's).
|
|
79
|
+
def subset_of?(other)
|
|
80
|
+
return false if other.nil?
|
|
81
|
+
|
|
82
|
+
lower_ok = if lower_unbounded?
|
|
83
|
+
other.lower_unbounded?
|
|
84
|
+
elsif other.lower_unbounded? || lower > other.lower
|
|
85
|
+
true
|
|
86
|
+
elsif lower == other.lower
|
|
87
|
+
!lower_included? || other.lower_included?
|
|
88
|
+
else
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
upper_ok = if upper_unbounded?
|
|
92
|
+
other.upper_unbounded?
|
|
93
|
+
elsif other.upper_unbounded? || upper < other.upper
|
|
94
|
+
true
|
|
95
|
+
elsif upper == other.upper
|
|
96
|
+
!upper_included? || other.upper_included?
|
|
97
|
+
else
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
lower_ok && upper_ok
|
|
101
|
+
end
|
|
102
|
+
|
|
74
103
|
private
|
|
75
104
|
|
|
76
105
|
def check_lower_upper(lower, upper)
|
|
@@ -441,6 +470,8 @@ module OpenEHR
|
|
|
441
470
|
return false
|
|
442
471
|
end
|
|
443
472
|
end
|
|
473
|
+
# A timezone is optional in ISO 8601; its absence does not make an
|
|
474
|
+
# otherwise-valid time invalid.
|
|
444
475
|
unless tz.nil?
|
|
445
476
|
timezone = Timezone.new(tz)
|
|
446
477
|
if timezone.hour < 0 or timezone.hour >= HOURS_IN_DAY
|
|
@@ -449,10 +480,8 @@ module OpenEHR
|
|
|
449
480
|
if timezone.minute < 0 or timezone.minute >= MINUTES_IN_HOUR
|
|
450
481
|
return false
|
|
451
482
|
end
|
|
452
|
-
return true
|
|
453
|
-
else
|
|
454
|
-
return false
|
|
455
483
|
end
|
|
484
|
+
return true
|
|
456
485
|
end
|
|
457
486
|
end
|
|
458
487
|
end # end of ISO8601_TIME
|
|
@@ -589,6 +618,19 @@ module OpenEHR
|
|
|
589
618
|
include TimeDefinitions
|
|
590
619
|
attr_reader :years, :months, :weeks, :days
|
|
591
620
|
attr_reader :hours, :minutes, :seconds, :fractional_second
|
|
621
|
+
# RM 1.1.0 (SPECRM-96): a leading '-' before 'P' negates the whole
|
|
622
|
+
# duration (e.g. "-P10D", 10 days before an unstated origin point).
|
|
623
|
+
# Components stay non-negative magnitudes - the sign applies to
|
|
624
|
+
# the duration as a whole, not each field - so it's tracked here.
|
|
625
|
+
attr_reader :negative
|
|
626
|
+
|
|
627
|
+
def negative=(negative)
|
|
628
|
+
@negative = negative ? true : false
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def negative?
|
|
632
|
+
@negative == true
|
|
633
|
+
end
|
|
592
634
|
|
|
593
635
|
def years=(years)
|
|
594
636
|
unless years.nil? || years >= 0
|
|
@@ -647,7 +689,7 @@ module OpenEHR
|
|
|
647
689
|
end
|
|
648
690
|
|
|
649
691
|
def as_string
|
|
650
|
-
str = 'P'
|
|
692
|
+
str = negative? ? '-P' : 'P'
|
|
651
693
|
unless @years.nil?
|
|
652
694
|
str += @years.to_s + 'Y'
|
|
653
695
|
end
|
|
@@ -681,27 +723,36 @@ module OpenEHR
|
|
|
681
723
|
nilthenzero(@months)*TimeDefinitions::DAYS_IN_MONTH +
|
|
682
724
|
nilthenzero(@weeks)*TimeDefinitions::DAYS_IN_WEEK +
|
|
683
725
|
nilthenzero(@days)
|
|
684
|
-
seconds_with_fractional = (((days*TimeDefinitions::HOURS_IN_DAY + nilthenzero(@hours))*TimeDefinitions::MINUTES_IN_HOUR)+nilthenzero(@minutes))*TimeDefinitions::SECONDS_IN_MINUTE +
|
|
726
|
+
seconds_with_fractional = (((days*TimeDefinitions::HOURS_IN_DAY + nilthenzero(@hours))*TimeDefinitions::MINUTES_IN_HOUR)+nilthenzero(@minutes))*TimeDefinitions::SECONDS_IN_MINUTE +
|
|
685
727
|
nilthenzero(@seconds) +
|
|
686
728
|
@fractional_second.to_f
|
|
687
|
-
return seconds_with_fractional
|
|
729
|
+
return negative? ? -seconds_with_fractional : seconds_with_fractional
|
|
688
730
|
end
|
|
689
731
|
end
|
|
690
732
|
|
|
691
733
|
class ISO8601Duration
|
|
692
734
|
include ISO8601DurationModule, Comparable
|
|
693
735
|
def initialize(str)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
self.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
736
|
+
# A leading '-' negates the whole duration (RM 1.1.0, SPECRM-96);
|
|
737
|
+
# stripped before the main match so the existing capture group
|
|
738
|
+
# numbering below doesn't need to shift.
|
|
739
|
+
self.negative = str.start_with?('-')
|
|
740
|
+
duration_str = negative? ? str[1..-1] : str
|
|
741
|
+
md = /^P((\d+)[Yy])?((\d+)[Mm])?((\d+)[Ww])?((\d+)[dD])?(T((\d+)[Hh])?((\d+)[Mm])?((\d+)(\.\d+)?[Ss])?)?$/.match(duration_str)
|
|
742
|
+
if md.nil?
|
|
743
|
+
raise ArgumentError, 'invalid ISO8601 duration format'
|
|
744
|
+
end
|
|
745
|
+
# Absent components stay nil rather than being coerced to 0, so
|
|
746
|
+
# as_string can round-trip a partially-specified duration and
|
|
747
|
+
# "unset" is distinguishable from "explicitly zero".
|
|
748
|
+
self.years = md[2] && md[2].to_i
|
|
749
|
+
self.months = md[4] && md[4].to_i
|
|
750
|
+
self.weeks = md[6] && md[6].to_i
|
|
751
|
+
self.days = md[8] && md[8].to_i
|
|
752
|
+
self.hours = md[11] && md[11].to_i
|
|
753
|
+
self.minutes = md[13] && md[13].to_i
|
|
754
|
+
self.seconds = md[15] && md[15].to_i
|
|
755
|
+
self.fractional_second = md[16] && md[16].to_f
|
|
705
756
|
end
|
|
706
757
|
|
|
707
758
|
def <=>(other)
|
|
@@ -15,7 +15,7 @@ module OpenEHR
|
|
|
15
15
|
lang:(arch_language)?
|
|
16
16
|
desc:(arch_description)?
|
|
17
17
|
arch_definition
|
|
18
|
-
arch_invariant?
|
|
18
|
+
inv:(arch_invariant)?
|
|
19
19
|
arch_ontology {
|
|
20
20
|
def archetype_id
|
|
21
21
|
arch_identification.archetype_id
|
|
@@ -52,6 +52,14 @@ module OpenEHR
|
|
|
52
52
|
def ontology
|
|
53
53
|
arch_ontology.value
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
def parent_archetype_id
|
|
57
|
+
spec.archetype_id unless spec.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def invariants
|
|
61
|
+
inv.value unless inv.empty?
|
|
62
|
+
end
|
|
55
63
|
}
|
|
56
64
|
end
|
|
57
65
|
|
|
@@ -116,9 +124,9 @@ module OpenEHR
|
|
|
116
124
|
{ :uid => uid.value }
|
|
117
125
|
end
|
|
118
126
|
}
|
|
119
|
-
/
|
|
127
|
+
/ SYM_IS_CONTROLLED space {
|
|
120
128
|
def value
|
|
121
|
-
{ :
|
|
129
|
+
{ :is_controlled? => true } # If elements[0]
|
|
122
130
|
end
|
|
123
131
|
}
|
|
124
132
|
end
|
|
@@ -194,7 +202,11 @@ module OpenEHR
|
|
|
194
202
|
end
|
|
195
203
|
|
|
196
204
|
rule arch_invariant
|
|
197
|
-
SYM_INVARIANT V_ASSERTION_TEXT space
|
|
205
|
+
SYM_INVARIANT text:V_ASSERTION_TEXT space {
|
|
206
|
+
def value
|
|
207
|
+
text.value
|
|
208
|
+
end
|
|
209
|
+
}
|
|
198
210
|
end
|
|
199
211
|
|
|
200
212
|
rule arch_ontology
|
|
@@ -327,7 +339,7 @@ module OpenEHR
|
|
|
327
339
|
rule c_complex_object_body
|
|
328
340
|
c_any '' {
|
|
329
341
|
def value(node)
|
|
330
|
-
Hash[:attributes =>
|
|
342
|
+
Hash[:attributes => nil]
|
|
331
343
|
end
|
|
332
344
|
}
|
|
333
345
|
/ c_attributes '' {
|
|
@@ -386,10 +398,10 @@ module OpenEHR
|
|
|
386
398
|
}
|
|
387
399
|
/ V_C_DOMAIN_TYPE '' {
|
|
388
400
|
def value(node = nil)
|
|
389
|
-
|
|
401
|
+
raise OpenEHR::Parser::ParseError,
|
|
402
|
+
"generic C_DOMAIN_TYPE constraints (e.g. C_DV_STATE) are not yet supported by the ADL parser: #{text_value}"
|
|
390
403
|
end
|
|
391
404
|
}
|
|
392
|
-
# / ERR_V_C_DOMAIN_TYPE
|
|
393
405
|
end
|
|
394
406
|
|
|
395
407
|
rule archetype_internal_ref
|
|
@@ -1460,7 +1472,7 @@ module OpenEHR
|
|
|
1460
1472
|
def value
|
|
1461
1473
|
if (id && !id.empty?)
|
|
1462
1474
|
OpenEHR::AM::Archetype::Assertion::Assertion.new(
|
|
1463
|
-
:tag => id.value, :expression => boolean_expression.value,
|
|
1475
|
+
:tag => id.elements[0].value, :expression => boolean_expression.value,
|
|
1464
1476
|
:string_expression => id.text_value + boolean_expression.text_value)
|
|
1465
1477
|
else
|
|
1466
1478
|
OpenEHR::AM::Archetype::Assertion::Assertion.new(
|
|
@@ -1726,7 +1738,7 @@ module OpenEHR
|
|
|
1726
1738
|
}
|
|
1727
1739
|
/ integer_value '' {
|
|
1728
1740
|
def value
|
|
1729
|
-
OpenEHR::AM::Archetype::
|
|
1741
|
+
OpenEHR::AM::Archetype::Assertion::ExprLeaf.new(
|
|
1730
1742
|
:type => 'Integer',
|
|
1731
1743
|
:item => integer_value.value,
|
|
1732
1744
|
:reference_type => 'CONSTANT')
|
|
@@ -1734,7 +1746,7 @@ module OpenEHR
|
|
|
1734
1746
|
}
|
|
1735
1747
|
/ real_value '' {
|
|
1736
1748
|
def value
|
|
1737
|
-
OpenEHR::AM::Archetype::
|
|
1749
|
+
OpenEHR::AM::Archetype::Assertion::ExprLeaf.new(
|
|
1738
1750
|
:type => 'Real',
|
|
1739
1751
|
:item => real_value.value,
|
|
1740
1752
|
:reference_type => 'CONSTANT')
|
|
@@ -1742,7 +1754,7 @@ module OpenEHR
|
|
|
1742
1754
|
}
|
|
1743
1755
|
/ absolute_path space {
|
|
1744
1756
|
def value
|
|
1745
|
-
OpenEHR::AM::Archetype::
|
|
1757
|
+
OpenEHR::AM::Archetype::Assertion::ExprLeaf.new(
|
|
1746
1758
|
:type => 'String',
|
|
1747
1759
|
:item => absolute_path.value,
|
|
1748
1760
|
:reference_type => 'CONSTANT')
|
|
@@ -2168,9 +2180,9 @@ module OpenEHR
|
|
|
2168
2180
|
type_identifier.value
|
|
2169
2181
|
end
|
|
2170
2182
|
}
|
|
2171
|
-
/ V_ATTRIBUTE_IDENTIFIER '' {
|
|
2183
|
+
/ id:V_ATTRIBUTE_IDENTIFIER '' {
|
|
2172
2184
|
def value
|
|
2173
|
-
|
|
2185
|
+
id.value
|
|
2174
2186
|
end
|
|
2175
2187
|
}
|
|
2176
2188
|
end
|
|
@@ -3004,6 +3016,10 @@ module OpenEHR
|
|
|
3004
3016
|
'=' white_space
|
|
3005
3017
|
end
|
|
3006
3018
|
|
|
3019
|
+
rule SYM_NE
|
|
3020
|
+
'!=' white_space
|
|
3021
|
+
end
|
|
3022
|
+
|
|
3007
3023
|
rule SYM_GE
|
|
3008
3024
|
('=>' / '>=') white_space
|
|
3009
3025
|
end
|
|
@@ -13,8 +13,10 @@ module OpenEHR
|
|
|
13
13
|
super
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def parse
|
|
17
|
-
archetype
|
|
16
|
+
def parse(validate: false)
|
|
17
|
+
archetype.tap do |a|
|
|
18
|
+
ArchetypeValidator.new(a).validate! if validate
|
|
19
|
+
end
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
private
|
|
@@ -67,6 +69,16 @@ module OpenEHR
|
|
|
67
69
|
parsed_data.concept
|
|
68
70
|
end
|
|
69
71
|
|
|
72
|
+
def parent_archetype_id
|
|
73
|
+
if parsed_data.parent_archetype_id
|
|
74
|
+
OpenEHR::RM::Support::Identification::ArchetypeID.new(:value => parsed_data.parent_archetype_id)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def invariants
|
|
79
|
+
parsed_data.invariants
|
|
80
|
+
end
|
|
81
|
+
|
|
70
82
|
def description
|
|
71
83
|
parsed_data.description
|
|
72
84
|
end
|
|
@@ -82,13 +94,15 @@ module OpenEHR
|
|
|
82
94
|
def archetype
|
|
83
95
|
OpenEHR::AM::Archetype::Archetype.new(:archetype_id => archetype_id,
|
|
84
96
|
:adl_version => adl_version,
|
|
85
|
-
:uid => uid,
|
|
97
|
+
:uid => uid,
|
|
86
98
|
:concept => concept,
|
|
87
99
|
:original_language => original_language,
|
|
88
100
|
:translations => translations,
|
|
89
101
|
:description => description,
|
|
90
102
|
:definition => definition,
|
|
91
|
-
:ontology => ontology
|
|
103
|
+
:ontology => ontology,
|
|
104
|
+
:parent_archetype_id => parent_archetype_id,
|
|
105
|
+
:invariants => invariants)
|
|
92
106
|
end
|
|
93
107
|
end
|
|
94
108
|
end # of Parser
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require_relative 'exception'
|
|
2
|
+
|
|
3
|
+
module OpenEHR
|
|
4
|
+
module Parser
|
|
5
|
+
# Post-parse semantic validator, checking a parsed archetype
|
|
6
|
+
# against the VARID..VDFPT rules (Beale07, p.100). VARID/VARDF/
|
|
7
|
+
# VARON/VDFAI are already enforced structurally by the RM/AM
|
|
8
|
+
# object model itself (their setters raise on construction), so
|
|
9
|
+
# they can never actually fail here - they're still checked
|
|
10
|
+
# explicitly, both for rule-traceability and so a validator run
|
|
11
|
+
# against a duck-typed object (not necessarily a real
|
|
12
|
+
# OpenEHR::AM::Archetype::Archetype) stays honest.
|
|
13
|
+
class ArchetypeValidator
|
|
14
|
+
Validation = Exception::Validation
|
|
15
|
+
|
|
16
|
+
def initialize(archetype)
|
|
17
|
+
@archetype = archetype
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def errors
|
|
21
|
+
RULES.filter_map { |rule| send(rule) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def valid?
|
|
25
|
+
errors.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate!
|
|
29
|
+
error = errors.first
|
|
30
|
+
raise error if error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Checks +rm_root+ against the archetype's definition, returning
|
|
34
|
+
# a path-annotated OpenEHR::Parser::Exception::Validation::
|
|
35
|
+
# InstanceNonConformant for the first violation found (path
|
|
36
|
+
# resolved via rm_root.path_of_item, the PATHABLE navigation
|
|
37
|
+
# method), or [] when rm_root fully conforms.
|
|
38
|
+
def validate_instance(rm_root)
|
|
39
|
+
return [] if @archetype.definition.valid_value?(rm_root)
|
|
40
|
+
|
|
41
|
+
_node, value = @archetype.definition.find_violation(rm_root)
|
|
42
|
+
message = "value does not conform to #{@archetype.archetype_id.value}"
|
|
43
|
+
[Validation::InstanceNonConformant.new(message, resolve_path(rm_root, value))]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def resolve_path(rm_root, value)
|
|
49
|
+
return nil unless rm_root.respond_to?(:path_of_item)
|
|
50
|
+
|
|
51
|
+
rm_root.path_of_item(value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def varid
|
|
55
|
+
Validation::VARID.new(Validation::VARID::MESSAGE) if @archetype.archetype_id.nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def vardf
|
|
59
|
+
Validation::VARDF.new(Validation::VARDF::MESSAGE) if @archetype.definition.nil?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def varon
|
|
63
|
+
Validation::VARON.new(Validation::VARON::MESSAGE) if @archetype.ontology.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def varcn
|
|
67
|
+
return if @archetype.concept.nil?
|
|
68
|
+
return if @archetype.ontology.term_codes.include?(@archetype.concept)
|
|
69
|
+
|
|
70
|
+
Validation::VARCN.new(Validation::VARCN::MESSAGE)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def vardt
|
|
74
|
+
definition_type = @archetype.definition.rm_type_name.to_s.upcase
|
|
75
|
+
id_type = @archetype.archetype_id.rm_entity.to_s.upcase
|
|
76
|
+
return if definition_type == id_type
|
|
77
|
+
|
|
78
|
+
Validation::VARDT.new(Validation::VARDT::MESSAGE)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def vatdf
|
|
82
|
+
Validation::VATDF.new(Validation::VATDF::MESSAGE) unless @archetype.node_ids_valid?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def vacdf
|
|
86
|
+
Validation::VACDF.new(Validation::VACDF::MESSAGE) unless @archetype.constraint_references_valid?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# In practice this can never actually fail through this gem's own
|
|
90
|
+
# object model: CArchetypeRoot#archetype_id= already rejects
|
|
91
|
+
# anything but a well-typed ArchetypeID at construction time.
|
|
92
|
+
# Still checked genuinely (rather than a hardcoded pass) in case
|
|
93
|
+
# +archetype+ is a duck-typed object built some other way.
|
|
94
|
+
def vdfai
|
|
95
|
+
return if archetype_roots(@archetype.definition).all? do |id|
|
|
96
|
+
id.is_a?(OpenEHR::RM::Support::Identification::ArchetypeID)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Validation::VDFAI.new(Validation::VDFAI::MESSAGE)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def archetype_roots(node, found = [])
|
|
103
|
+
return found if node.nil?
|
|
104
|
+
|
|
105
|
+
found << node.archetype_id if node.is_a?(OpenEHR::AM::Archetype::ConstraintModel::CArchetypeRoot)
|
|
106
|
+
if node.respond_to?(:attributes) && node.attributes
|
|
107
|
+
node.attributes.each { |attribute| archetype_roots(attribute, found) }
|
|
108
|
+
end
|
|
109
|
+
if node.respond_to?(:children) && node.children
|
|
110
|
+
node.children.each { |child| archetype_roots(child, found) }
|
|
111
|
+
end
|
|
112
|
+
found
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def vdfpt
|
|
116
|
+
return if @archetype.internal_references_valid? &&
|
|
117
|
+
@archetype.physical_paths.all? { |path| OpenEHR::Path.valid?(path) }
|
|
118
|
+
|
|
119
|
+
Validation::VDFPT.new(Validation::VDFPT::MESSAGE)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
RULES = %i[varid vardf varon varcn vardt vatdf vacdf vdfai vdfpt].freeze
|
|
123
|
+
private_constant :RULES
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -60,6 +60,19 @@ module OpenEHR
|
|
|
60
60
|
class VDFPT < Base
|
|
61
61
|
MESSAGE = "Any path mentioned in the definition section must be valid syntactically, and a valid path with respect to the hierarchical structure of the definition section."
|
|
62
62
|
end
|
|
63
|
+
|
|
64
|
+
# Raised by ArchetypeValidator#validate_instance for an RM
|
|
65
|
+
# instance that does not conform to an archetype's definition;
|
|
66
|
+
# not one of the VARID..VDFPT archetype-structure rules, since
|
|
67
|
+
# this checks an RM instance against the archetype instead.
|
|
68
|
+
class InstanceNonConformant < Base
|
|
69
|
+
attr_reader :path
|
|
70
|
+
|
|
71
|
+
def initialize(message, path = nil)
|
|
72
|
+
super(message)
|
|
73
|
+
@path = path
|
|
74
|
+
end
|
|
75
|
+
end
|
|
63
76
|
end
|
|
64
77
|
|
|
65
78
|
end
|
|
@@ -40,7 +40,7 @@ module OpenEHR
|
|
|
40
40
|
@opt = Nokogiri::XML::Document.parse(File.open(@filename))
|
|
41
41
|
@opt.remove_namespaces!
|
|
42
42
|
|
|
43
|
-
uid =
|
|
43
|
+
uid = build_uid
|
|
44
44
|
defs = definition
|
|
45
45
|
|
|
46
46
|
# Create operational template with archetype-compatible parameters
|
|
@@ -52,7 +52,7 @@ module OpenEHR
|
|
|
52
52
|
template_id: template_id,
|
|
53
53
|
archetype_id: template_id, # Use template_id as archetype_id for compatibility
|
|
54
54
|
definition: defs,
|
|
55
|
-
ontology: create_template_ontology,
|
|
55
|
+
ontology: (@component_terminologies || {})[defs.archetype_id.value] || create_template_ontology,
|
|
56
56
|
component_terminologies: @component_terminologies || {},
|
|
57
57
|
terminology_extracts: @component_terminologies || {},
|
|
58
58
|
adl_version: "1.4"
|
|
@@ -61,8 +61,20 @@ module OpenEHR
|
|
|
61
61
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
|
+
# template_id is mandatory for an operational template (enforced by
|
|
65
|
+
# OperationalTemplate itself); a missing/blank <template_id> element
|
|
66
|
+
# must therefore resolve to nil rather than an invalid TemplateID.
|
|
64
67
|
def template_id
|
|
65
|
-
@template_id
|
|
68
|
+
return @template_id if @template_id
|
|
69
|
+
value = text_on_path(@opt, TEMPLATE_ID_PATH)
|
|
70
|
+
@template_id = value.nil? || value.empty? ? nil : OpenEHR::RM::Support::Identification::TemplateID.new(value: value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# uid is optional on an operational template; a missing/blank <uid>
|
|
74
|
+
# element must resolve to nil rather than an invalid UIDBasedID.
|
|
75
|
+
def build_uid
|
|
76
|
+
value = text_on_path(@opt, UID_PATH)
|
|
77
|
+
value.nil? || value.empty? ? nil : OpenEHR::RM::Support::Identification::UIDBasedID.new(value: value)
|
|
66
78
|
end
|
|
67
79
|
|
|
68
80
|
def concept
|
|
@@ -260,7 +272,12 @@ module OpenEHR
|
|
|
260
272
|
upper_included = upper_included_node ? to_bool(upper_included_node.text) : (upper.nil? ? nil : true)
|
|
261
273
|
lower_unbounded = lower_unbounded_node ? to_bool(lower_unbounded_node.text) : false
|
|
262
274
|
upper_unbounded = upper_unbounded_node ? to_bool(upper_unbounded_node.text) : false
|
|
263
|
-
|
|
275
|
+
|
|
276
|
+
# An occurrences element with none of its children present carries no
|
|
277
|
+
# constraint at all; Interval requires at least one bound, so treat
|
|
278
|
+
# this as "no occurrences data" rather than raising.
|
|
279
|
+
return nil if lower.nil? && upper.nil? && !lower_unbounded && !upper_unbounded
|
|
280
|
+
|
|
264
281
|
# Handle unbounded intervals properly
|
|
265
282
|
if upper_unbounded || upper.nil?
|
|
266
283
|
upper = nil
|
|
@@ -282,11 +299,14 @@ module OpenEHR
|
|
|
282
299
|
|
|
283
300
|
def cardinality(xml)
|
|
284
301
|
return nil if xml.nil?
|
|
285
|
-
|
|
302
|
+
|
|
286
303
|
order_node = xml.at('is_ordered')
|
|
287
304
|
unique_node = xml.at('is_unique')
|
|
288
305
|
interval_node = xml.at('interval')
|
|
289
|
-
|
|
306
|
+
|
|
307
|
+
# No cardinality sub-elements at all means no cardinality data.
|
|
308
|
+
return nil if order_node.nil? && unique_node.nil? && interval_node.nil?
|
|
309
|
+
|
|
290
310
|
order = order_node ? to_bool(order_node.text) : false
|
|
291
311
|
unique = unique_node ? to_bool(unique_node.text) : false
|
|
292
312
|
interval = interval_node ? occurrences(interval_node) : nil
|
|
@@ -298,6 +318,21 @@ module OpenEHR
|
|
|
298
318
|
)
|
|
299
319
|
end
|
|
300
320
|
|
|
321
|
+
def archetype_internal_ref(attr_xml, node)
|
|
322
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
323
|
+
target_path = attr_xml.at('target_path').text
|
|
324
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
325
|
+
OpenEHR::AM::Archetype::ConstraintModel::ArchetypeInternalRef.new(rm_type_name: rm_type_name, occurrences: occurrences, target_path: target_path)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# No .opt fixture in this gem's corpus uses a C_DV_STATE (state
|
|
329
|
+
# machine) constraint, so its actual OPT XML shape is unverified;
|
|
330
|
+
# raising a clear, documented error here is safer than guessing
|
|
331
|
+
# at element names and risking a silently wrong StateMachine.
|
|
332
|
+
def c_dv_state(_attr_xml, _node)
|
|
333
|
+
raise NotImplementedError, 'OPTParser does not yet support C_DV_STATE (state machine) constraints'
|
|
334
|
+
end
|
|
335
|
+
|
|
301
336
|
def constraint_ref(attr_xml, node)
|
|
302
337
|
rm_type_name = attr_xml.at('rm_type_name').text
|
|
303
338
|
reference = attr_xml.at('reference').text
|
|
@@ -354,9 +389,7 @@ module OpenEHR
|
|
|
354
389
|
def c_dv_quantity(attr_xml, node)
|
|
355
390
|
rm_type_name = attr_xml.at('rm_type_name').text
|
|
356
391
|
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
357
|
-
|
|
358
|
-
property_code_string = attr_xml.at('property/code_string').text
|
|
359
|
-
property = OpenEHR::RM::DataTypes::Text::CodePhrase.new(terminology_id: property_terminology_id, code_string: property_code_string)
|
|
392
|
+
property = property_code_phrase(attr_xml.at('property'))
|
|
360
393
|
list = attr_xml.xpath('.//list').map do |element|
|
|
361
394
|
units = element.at('units').text if element.at('units')
|
|
362
395
|
magnitude = occurrences(element.at('magnitude')) if element.at('magnitude')
|
|
@@ -366,6 +399,59 @@ module OpenEHR
|
|
|
366
399
|
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvQuantity.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list, property: property)
|
|
367
400
|
end
|
|
368
401
|
|
|
402
|
+
# The <property> element is optional in real templates; return nil rather
|
|
403
|
+
# than dereferencing missing terminology/code nodes.
|
|
404
|
+
def property_code_phrase(property_xml)
|
|
405
|
+
return nil if property_xml.nil?
|
|
406
|
+
terminology_node = property_xml.at('terminology_id/value')
|
|
407
|
+
code_node = property_xml.at('code_string')
|
|
408
|
+
return nil if terminology_node.nil? || code_node.nil?
|
|
409
|
+
terminology_id = OpenEHR::RM::Support::Identification::TerminologyID.new(value: terminology_node.text)
|
|
410
|
+
OpenEHR::RM::DataTypes::Text::CodePhrase.new(terminology_id: terminology_id, code_string: code_node.text)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def c_dv_ordinal(attr_xml, node)
|
|
414
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
415
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
416
|
+
list = attr_xml.xpath('list').map { |element| dv_ordinal_item(element) }.compact
|
|
417
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvOrdinal.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# DV_ORDINAL.symbol is spec'd as DV_CODED_TEXT; the OPT XML only
|
|
421
|
+
# carries a defining_code (terminology_id + code_string), so the
|
|
422
|
+
# DvCodedText's own value is set to that same code_string (there
|
|
423
|
+
# is no separate display text in this element).
|
|
424
|
+
def dv_ordinal_item(element)
|
|
425
|
+
value_node = element.at('value')
|
|
426
|
+
return nil unless value_node && !value_node.text.empty?
|
|
427
|
+
|
|
428
|
+
code_phrase = property_code_phrase(element.at('symbol/defining_code'))
|
|
429
|
+
return nil if code_phrase.nil?
|
|
430
|
+
|
|
431
|
+
symbol = OpenEHR::RM::DataTypes::Text::DvCodedText.new(value: code_phrase.code_string, defining_code: code_phrase)
|
|
432
|
+
OpenEHR::RM::DataTypes::Quantity::DvOrdinal.new(value: value_node.text.to_i, symbol: symbol)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def c_dv_scale(attr_xml, node)
|
|
436
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
437
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
438
|
+
list = attr_xml.xpath('list').map { |element| dv_scale_item(element) }.compact
|
|
439
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvScale.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Same XML shape as C_DV_ORDINAL's list items, but DV_SCALE.value
|
|
443
|
+
# is Real rather than Integer.
|
|
444
|
+
def dv_scale_item(element)
|
|
445
|
+
value_node = element.at('value')
|
|
446
|
+
return nil unless value_node && !value_node.text.empty?
|
|
447
|
+
|
|
448
|
+
code_phrase = property_code_phrase(element.at('symbol/defining_code'))
|
|
449
|
+
return nil if code_phrase.nil?
|
|
450
|
+
|
|
451
|
+
symbol = OpenEHR::RM::DataTypes::Text::DvCodedText.new(value: code_phrase.code_string, defining_code: code_phrase)
|
|
452
|
+
OpenEHR::RM::DataTypes::Quantity::DvScale.new(value: value_node.text.to_f, symbol: symbol)
|
|
453
|
+
end
|
|
454
|
+
|
|
369
455
|
def c_date(xml)
|
|
370
456
|
pattern = xml.at('pattern')
|
|
371
457
|
range = xml.at('range')
|
|
@@ -403,6 +489,73 @@ module OpenEHR
|
|
|
403
489
|
end
|
|
404
490
|
end
|
|
405
491
|
|
|
492
|
+
def c_real(xml)
|
|
493
|
+
range = xml.at('range')
|
|
494
|
+
list = xml.xpath('list')
|
|
495
|
+
if range
|
|
496
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new(range: occurrences(range))
|
|
497
|
+
elsif !list.empty?
|
|
498
|
+
list_values = list.map { |item| item.text.to_f }
|
|
499
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new(list: list_values)
|
|
500
|
+
else
|
|
501
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def c_duration(xml)
|
|
506
|
+
pattern = xml.at('pattern')
|
|
507
|
+
range_xml = xml.at('range')
|
|
508
|
+
list = xml.xpath('list')
|
|
509
|
+
if pattern
|
|
510
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(pattern: pattern.text)
|
|
511
|
+
elsif range_xml
|
|
512
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(range: duration_range(range_xml))
|
|
513
|
+
elsif !list.empty?
|
|
514
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(list: list.map(&:text))
|
|
515
|
+
else
|
|
516
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# A C_DURATION range's bounds are ISO8601 duration strings (e.g.
|
|
521
|
+
# PT24H), not plain numbers, so occurrences() (built for numeric
|
|
522
|
+
# Interval bounds) doesn't apply here; wrap each bound as a
|
|
523
|
+
# DV_DURATION instead, matching what CDuration#valid_value?
|
|
524
|
+
# already expects its range bounds to be.
|
|
525
|
+
def duration_range(range_xml)
|
|
526
|
+
lower = duration_bound(range_xml.at('lower'), range_xml.at('lower_unbounded'))
|
|
527
|
+
upper = duration_bound(range_xml.at('upper'), range_xml.at('upper_unbounded'))
|
|
528
|
+
return nil if lower.nil? && upper.nil?
|
|
529
|
+
|
|
530
|
+
OpenEHR::AssumedLibraryTypes::Interval.new(
|
|
531
|
+
lower: lower, upper: upper,
|
|
532
|
+
lower_included: lower.nil? ? nil : bool_node(range_xml.at('lower_included'), true),
|
|
533
|
+
upper_included: upper.nil? ? nil : bool_node(range_xml.at('upper_included'), true))
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def duration_bound(value_node, unbounded_node)
|
|
537
|
+
return nil if bool_node(unbounded_node, false)
|
|
538
|
+
return nil if value_node.nil? || value_node.text.empty?
|
|
539
|
+
|
|
540
|
+
OpenEHR::RM::DataTypes::Quantity::DateTime::DvDuration.new(value: value_node.text)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def bool_node(node, default)
|
|
544
|
+
node ? to_bool(node.text) : default
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def c_time(xml)
|
|
548
|
+
pattern = xml.at('pattern')
|
|
549
|
+
range = xml.at('range')
|
|
550
|
+
if pattern
|
|
551
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new(pattern: pattern.text)
|
|
552
|
+
elsif range
|
|
553
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new(range: occurrences(range))
|
|
554
|
+
else
|
|
555
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
406
559
|
def c_boolean(xml)
|
|
407
560
|
true_valid = xml.at('true_valid')
|
|
408
561
|
false_valid = xml.at('false_valid')
|