openehr 1.2.999999 → 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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +95 -14
  3. data/lib/openehr/am/archetype/constraint_model/primitive.rb +204 -6
  4. data/lib/openehr/am/archetype/constraint_model.rb +228 -4
  5. data/lib/openehr/am/archetype/ontology.rb +2 -2
  6. data/lib/openehr/am/archetype.rb +101 -2
  7. data/lib/openehr/am/openehr_profile/data_types/basic.rb +28 -0
  8. data/lib/openehr/am/openehr_profile/data_types/quantity.rb +88 -0
  9. data/lib/openehr/am/openehr_profile/data_types/text.rb +23 -0
  10. data/lib/openehr/am/template.rb +77 -24
  11. data/lib/openehr/aql/engine/binding.rb +13 -0
  12. data/lib/openehr/aql/engine/contains_resolver.rb +161 -0
  13. data/lib/openehr/aql/engine/dataset.rb +122 -0
  14. data/lib/openehr/aql/engine/path_evaluator.rb +137 -0
  15. data/lib/openehr/aql/engine/predicate_evaluator.rb +65 -0
  16. data/lib/openehr/aql/engine.rb +119 -0
  17. data/lib/openehr/aql/errors.rb +27 -0
  18. data/lib/openehr/aql/lexer.rb +295 -0
  19. data/lib/openehr/aql/model/containment.rb +45 -0
  20. data/lib/openehr/aql/model/from_clause.rb +35 -0
  21. data/lib/openehr/aql/model/function_call.rb +56 -0
  22. data/lib/openehr/aql/model/identified_path.rb +20 -0
  23. data/lib/openehr/aql/model/literal.rb +15 -0
  24. data/lib/openehr/aql/model/object_path.rb +34 -0
  25. data/lib/openehr/aql/model/order_by_and_limit.rb +50 -0
  26. data/lib/openehr/aql/model/predicate.rb +84 -0
  27. data/lib/openehr/aql/model/query.rb +24 -0
  28. data/lib/openehr/aql/model/select_clause.rb +29 -0
  29. data/lib/openehr/aql/model/where_clause.rb +105 -0
  30. data/lib/openehr/aql/model.rb +14 -0
  31. data/lib/openehr/aql/parser.rb +484 -0
  32. data/lib/openehr/aql/result_set.rb +54 -0
  33. data/lib/openehr/aql.rb +28 -0
  34. data/lib/openehr/assumed_library_types.rb +68 -17
  35. data/lib/openehr/parser/adl_grammar.tt +29 -13
  36. data/lib/openehr/parser/adl_parser.rb +18 -4
  37. data/lib/openehr/parser/archetype_validator.rb +126 -0
  38. data/lib/openehr/parser/exception.rb +13 -0
  39. data/lib/openehr/parser/opt_parser.rb +306 -27
  40. data/lib/openehr/parser.rb +2 -0
  41. data/lib/openehr/path.rb +195 -0
  42. data/lib/openehr/rm/common/archetyped.rb +117 -7
  43. data/lib/openehr/rm/common/change_control.rb +3 -8
  44. data/lib/openehr/rm/common/directory.rb +4 -0
  45. data/lib/openehr/rm/common/generic.rb +24 -10
  46. data/lib/openehr/rm/composition/content/entry.rb +24 -5
  47. data/lib/openehr/rm/composition/content/navigation.rb +2 -1
  48. data/lib/openehr/rm/composition.rb +19 -2
  49. data/lib/openehr/rm/data_structures/history.rb +3 -0
  50. data/lib/openehr/rm/data_structures/item_structure/representation.rb +13 -9
  51. data/lib/openehr/rm/data_structures/item_structure.rb +28 -15
  52. data/lib/openehr/rm/data_types/encapsulated.rb +23 -3
  53. data/lib/openehr/rm/data_types/quantity/date_time.rb +9 -1
  54. data/lib/openehr/rm/data_types/quantity.rb +66 -14
  55. data/lib/openehr/rm/data_types/text.rb +8 -0
  56. data/lib/openehr/rm/data_types/time_specification.rb +5 -4
  57. data/lib/openehr/rm/data_types/uri.rb +0 -1
  58. data/lib/openehr/rm/demographic.rb +4 -3
  59. data/lib/openehr/rm/ehr.rb +20 -1
  60. data/lib/openehr/rm/factory.rb +345 -36
  61. data/lib/openehr/rm/integration.rb +1 -0
  62. data/lib/openehr/rm/support/identification.rb +37 -8
  63. data/lib/openehr/rm/support/measurement.rb +32 -0
  64. data/lib/openehr/rm/type_name.rb +90 -0
  65. data/lib/openehr/rm.rb +3 -0
  66. data/lib/openehr/serializer/adl_serializer.rb +335 -0
  67. data/lib/openehr/serializer/base.rb +20 -0
  68. data/lib/openehr/serializer/opt_serializer.rb +49 -0
  69. data/lib/openehr/serializer/rm_json_serializer.rb +62 -0
  70. data/lib/openehr/serializer/xml_serializer.rb +260 -0
  71. data/lib/openehr/serializer.rb +5 -291
  72. data/lib/openehr/terminology_service.rb +44 -0
  73. data/lib/openehr/version.rb +1 -1
  74. data/lib/openehr.rb +5 -2
  75. metadata +36 -11
  76. data/lib/openehr/parser/validator.rb +0 -18
  77. data/lib/openehr/parser/xml_parser.rb +0 -13
  78. data/lib/openehr/rm/data_types/charset_extract.rb +0 -24
  79. 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
- /^P((\d+)[Yy])?((\d+)[Mm])?((\d+)[Ww])?((\d)[dD])?(T((\d+)[Hh])?((\d+)[Mm])?((\d+)(\.\d+)?[Ss])?)?$/ =~ str
695
- self.years = $2.to_i
696
- self.months = $4.to_i
697
- self.weeks = $6.to_i
698
- self.days = $8.to_i
699
- self.hours = $11.to_i
700
- self.minutes = $13.to_i
701
- self.seconds = $15.to_i
702
- unless $16.nil?
703
- self.fractional_second = $16.to_f
704
- end
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
- / SYM_IS_CONTROLED space {
127
+ / SYM_IS_CONTROLLED space {
120
128
  def value
121
- { :is_controled? => true } # If elements[0]
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 => [c_any.value(node)]]
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
- p elemetns
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::ConstraintModel::ExprLeaf.new(
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::ConstraintModel::ExprLeaf.new(
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::ConstraintModel::ExprLeaf.new(
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
- V_ATTRIBUTE_IDENTIFIER.value
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