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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +93 -12
  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 +25 -3
  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 +162 -9
  40. data/lib/openehr/parser.rb +2 -0
  41. data/lib/openehr/path.rb +195 -0
  42. data/lib/openehr/rm/common/archetyped.rb +116 -6
  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/demographic.rb +4 -3
  58. data/lib/openehr/rm/ehr.rb +20 -1
  59. data/lib/openehr/rm/factory.rb +234 -6
  60. data/lib/openehr/rm/integration.rb +1 -0
  61. data/lib/openehr/rm/support/identification.rb +37 -8
  62. data/lib/openehr/rm/support/measurement.rb +32 -0
  63. data/lib/openehr/rm/type_name.rb +90 -0
  64. data/lib/openehr/rm.rb +3 -0
  65. data/lib/openehr/serializer/adl_serializer.rb +335 -0
  66. data/lib/openehr/serializer/base.rb +20 -0
  67. data/lib/openehr/serializer/opt_serializer.rb +49 -0
  68. data/lib/openehr/serializer/rm_json_serializer.rb +62 -0
  69. data/lib/openehr/serializer/xml_serializer.rb +260 -0
  70. data/lib/openehr/serializer.rb +5 -291
  71. data/lib/openehr/terminology_service.rb +44 -0
  72. data/lib/openehr/version.rb +1 -1
  73. data/lib/openehr.rb +5 -2
  74. metadata +35 -7
  75. data/lib/openehr/parser/validator.rb +0 -18
  76. data/lib/openehr/parser/xml_parser.rb +0 -13
  77. data/lib/openehr/rm/data_types/charset_extract.rb +0 -24
  78. data/lib/openehr/writer.rb +0 -12
@@ -16,7 +16,9 @@ module OpenEHR
16
16
 
17
17
  end
18
18
 
19
+ require_relative 'parser/exception'
19
20
  require_relative 'parser/adl_parser'
20
21
  require_relative 'parser/opt_parser'
22
+ require_relative 'parser/archetype_validator'
21
23
  end
22
24
  end
@@ -0,0 +1,195 @@
1
+ # OpenEHR::Path parses the openEHR archetype path syntax used to
2
+ # navigate LOCATABLE/PATHABLE trees, e.g.
3
+ # /content[at0001]/data[at0002]/events[at0006]/data[at0003]/items[at0004, 'Systolic']/value
4
+ #
5
+ # Grammar subset supported (see the plan for the full rationale):
6
+ # path := '/' | ('/' segment)+
7
+ # segment := attribute predicate?
8
+ # attribute := [a-z][a-zA-Z0-9_]*
9
+ # predicate := '[' node_id (',' ws* name)? ']'
10
+ # node_id := at-code (at\d+(\.\d+)*, incl. specialised at0001.1 and
11
+ # the short at0.2 form ADL 1.4 uses for nodes newly
12
+ # introduced by a specialisation)
13
+ # | archetype-id
14
+ # name := "'" ... "'" (a Locatable#name.value literal)
15
+ #
16
+ # Deliberately unsupported (raises InvalidPathError): relative paths,
17
+ # '//' wildcards, numeric index predicates, and general expression
18
+ # predicates. Those are layered on top by consumers (e.g. AQL); this
19
+ # class only parses the path shape used by RM path navigation.
20
+ #
21
+ # This file has no dependency on any RM class, so it can be required
22
+ # standalone.
23
+ module OpenEHR
24
+ class Path
25
+ class InvalidPathError < ArgumentError; end
26
+
27
+ AT_CODE = /\Aat\d+(\.\d+)*\z/
28
+ ARCHETYPE_ID = /\A[a-zA-Z]\w+-[a-zA-Z]\w+-[a-zA-Z]\w+\.[a-zA-Z]\w+(-[a-zA-Z]\w+)?\.v\d+\z/
29
+ ATTRIBUTE = /\A[a-z][a-zA-Z0-9_]*\z/
30
+
31
+ class Segment
32
+ attr_reader :attribute, :archetype_node_id, :name
33
+
34
+ def initialize(attribute, archetype_node_id: nil, name: nil)
35
+ unless attribute.is_a?(String) && attribute =~ ATTRIBUTE
36
+ raise InvalidPathError, "invalid path attribute: #{attribute.inspect}"
37
+ end
38
+ if archetype_node_id && !(archetype_node_id =~ AT_CODE || archetype_node_id =~ ARCHETYPE_ID)
39
+ raise InvalidPathError, "invalid node id: #{archetype_node_id.inspect}"
40
+ end
41
+ raise InvalidPathError, 'name predicate requires a node_id predicate' if name && archetype_node_id.nil?
42
+
43
+ @attribute = attribute
44
+ @archetype_node_id = archetype_node_id
45
+ @name = name
46
+ freeze
47
+ end
48
+
49
+ def predicate?
50
+ !@archetype_node_id.nil?
51
+ end
52
+
53
+ def to_s
54
+ return @attribute unless predicate?
55
+
56
+ s = "#{@attribute}[#{@archetype_node_id}"
57
+ s += ", '#{@name}'" if @name
58
+ s + ']'
59
+ end
60
+
61
+ def ==(other)
62
+ other.is_a?(Segment) &&
63
+ attribute == other.attribute &&
64
+ archetype_node_id == other.archetype_node_id &&
65
+ name == other.name
66
+ end
67
+ alias eql? ==
68
+
69
+ def hash
70
+ [attribute, archetype_node_id, name].hash
71
+ end
72
+ end
73
+
74
+ def self.parse(str)
75
+ raise InvalidPathError, 'path must be a String' unless str.is_a?(String)
76
+ raise InvalidPathError, "path must start with '/': #{str.inspect}" unless str.start_with?('/')
77
+ return new([]) if str == '/'
78
+
79
+ body = str[1..-1]
80
+ raise InvalidPathError, "path must not end with '/': #{str.inspect}" if body.end_with?('/')
81
+
82
+ new(split_segments(body).map { |token| parse_segment(token) })
83
+ end
84
+
85
+ def self.valid?(str)
86
+ parse(str)
87
+ true
88
+ rescue InvalidPathError
89
+ false
90
+ end
91
+
92
+ # Splits on top-level '/' only (not '/' occurring inside a '[...]'
93
+ # predicate), and rejects empty segments (i.e. a '//' wildcard).
94
+ def self.split_segments(body)
95
+ tokens = []
96
+ depth = 0
97
+ current = +''
98
+ body.each_char do |c|
99
+ case c
100
+ when '['
101
+ depth += 1
102
+ current << c
103
+ when ']'
104
+ depth -= 1
105
+ current << c
106
+ when '/'
107
+ if depth.zero?
108
+ raise InvalidPathError, "empty path segment ('//' is not supported): #{body.inspect}" if current.empty?
109
+
110
+ tokens << current
111
+ current = +''
112
+ else
113
+ current << c
114
+ end
115
+ else
116
+ current << c
117
+ end
118
+ end
119
+ raise InvalidPathError, "unbalanced '[' in path: #{body.inspect}" unless depth.zero?
120
+ raise InvalidPathError, "empty path segment ('//' is not supported): #{body.inspect}" if current.empty?
121
+
122
+ tokens << current
123
+ tokens
124
+ end
125
+ private_class_method :split_segments
126
+
127
+ def self.parse_segment(token)
128
+ if token =~ /\A([^\[\]]+)\[(.*)\]\z/
129
+ attribute = Regexp.last_match(1)
130
+ predicate = Regexp.last_match(2)
131
+ if predicate =~ /\A([^,]+),\s*'([^']*)'\z/
132
+ Segment.new(attribute, archetype_node_id: Regexp.last_match(1).strip, name: Regexp.last_match(2))
133
+ else
134
+ Segment.new(attribute, archetype_node_id: predicate.strip)
135
+ end
136
+ elsif token =~ /\A[^\[\]]+\z/
137
+ Segment.new(token)
138
+ else
139
+ raise InvalidPathError, "malformed path segment: #{token.inspect}"
140
+ end
141
+ end
142
+ private_class_method :parse_segment
143
+
144
+ attr_reader :segments
145
+
146
+ def initialize(segments)
147
+ @segments = segments.freeze
148
+ freeze
149
+ end
150
+
151
+ def root?
152
+ @segments.empty?
153
+ end
154
+
155
+ def to_s
156
+ return '/' if root?
157
+
158
+ '/' + @segments.map(&:to_s).join('/')
159
+ end
160
+
161
+ def ==(other)
162
+ other.is_a?(Path) && segments == other.segments
163
+ end
164
+ alias eql? ==
165
+
166
+ def hash
167
+ segments.hash
168
+ end
169
+
170
+ def parent
171
+ return self if root?
172
+
173
+ self.class.new(segments[0..-2])
174
+ end
175
+
176
+ def +(other)
177
+ case other
178
+ when Segment
179
+ self.class.new(segments + [other])
180
+ when Path
181
+ self.class.new(segments + other.segments)
182
+ when String
183
+ self.class.new(segments + [Segment.new(other)])
184
+ else
185
+ raise ArgumentError, "cannot append #{other.class} to a Path"
186
+ end
187
+ end
188
+
189
+ def descend
190
+ return [nil, self] if root?
191
+
192
+ [segments.first, self.class.new(segments[1..-1])]
193
+ end
194
+ end
195
+ end
@@ -13,31 +13,138 @@ module OpenEHR
13
13
  MULTIPART_ID_DELIMITER = "::"
14
14
  end
15
15
 
16
+ # Raised by Pathable#item_at_path when a path predicate matches
17
+ # more than one child (e.g. a node_id shared by siblings with
18
+ # different names).
19
+ class PathNotUniqueError < StandardError; end
20
+
16
21
  class Pathable
17
22
  attr_accessor :parent
18
23
 
24
+ # Declares which of this class's attributes are exposed to
25
+ # path navigation, e.g. `path_attribute :data, :state`.
26
+ # Declarations accumulate down the inheritance chain.
27
+ def self.path_attribute(*attr_names)
28
+ @path_attributes ||= []
29
+ @path_attributes.concat(attr_names.map(&:to_sym))
30
+ end
31
+
32
+ def self.path_attributes
33
+ own = @path_attributes || []
34
+ self == Pathable ? own : superclass.path_attributes + own
35
+ end
36
+
19
37
  def initialize(args = { })
20
38
  self.parent = args[:parent]
21
39
  end
22
40
 
41
+ # The one-level-deep path-navigable children of this node, as
42
+ # {attribute_name_string => value}. Attributes whose value is
43
+ # nil are omitted; a value may itself be an Array (e.g. a
44
+ # CLUSTER's items).
45
+ def path_children
46
+ self.class.path_attributes.each_with_object({}) do |attr, hash|
47
+ value = send(attr)
48
+ hash[attr.to_s] = value unless value.nil?
49
+ end
50
+ end
51
+
23
52
  def item_at_path(path)
24
- raise NotImplementedError, "item_at_path must be implemented"
53
+ matches = items_at_path(path)
54
+ case matches.size
55
+ when 0 then nil
56
+ when 1 then matches.first
57
+ else raise PathNotUniqueError, "path #{path} matches #{matches.size} items"
58
+ end
25
59
  end
26
60
 
27
61
  def items_at_path(path)
28
- raise NotImplementedError, "items_at_path must be implemented"
62
+ path = self.class.normalize_path(path)
63
+ return [self] if path.root?
64
+
65
+ segment, rest = path.descend
66
+ children = path_children[segment.attribute]
67
+ return [] if children.nil?
68
+
69
+ matched = self.class.select_by_predicate(Array(children), segment)
70
+ return matched if rest.root?
71
+
72
+ matched.flat_map do |child|
73
+ child.is_a?(Pathable) ? child.items_at_path(rest) : []
74
+ end
29
75
  end
30
76
 
31
77
  def path_exists?(path)
32
- raise NotImplementedError, "path_exists? must be implemented"
78
+ !items_at_path(path).empty?
33
79
  end
34
80
 
35
81
  def path_of_item(item)
36
- raise NotImplementedError, "path_of_item must be implemented"
82
+ find_path_to(item, OpenEHR::Path.new([]))
37
83
  end
38
84
 
39
85
  def path_unique?(path)
40
- raise NotImplementedError, "path_unique? must be implemented"
86
+ items_at_path(path).size == 1
87
+ end
88
+
89
+ def self.normalize_path(path)
90
+ path.is_a?(OpenEHR::Path) ? path : OpenEHR::Path.parse(path)
91
+ end
92
+
93
+ # Filters children matched by a path segment's predicate: a
94
+ # node_id predicate keeps only children whose
95
+ # archetype_node_id matches (non-Locatable children never
96
+ # match a node_id predicate); a name predicate further
97
+ # narrows to the child whose name.value matches.
98
+ def self.select_by_predicate(children, segment)
99
+ return children unless segment.predicate?
100
+
101
+ matched = children.select do |child|
102
+ child.respond_to?(:archetype_node_id) &&
103
+ child.archetype_node_id == segment.archetype_node_id
104
+ end
105
+ return matched if segment.name.nil?
106
+
107
+ matched.select do |child|
108
+ child.respond_to?(:name) && child.name.respond_to?(:value) &&
109
+ child.name.value == segment.name
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def find_path_to(target, prefix)
116
+ return prefix.to_s if equal?(target)
117
+
118
+ self.class.path_attributes.each do |attr|
119
+ value = send(attr)
120
+ next if value.nil?
121
+
122
+ siblings = Array(value)
123
+ siblings.each do |child|
124
+ child_prefix = prefix + segment_for(attr, child, siblings)
125
+ if child.equal?(target)
126
+ return child_prefix.to_s
127
+ elsif child.is_a?(Pathable)
128
+ found = child.send(:find_path_to, target, child_prefix)
129
+ return found if found
130
+ end
131
+ end
132
+ end
133
+ nil
134
+ end
135
+
136
+ def segment_for(attribute, child, siblings)
137
+ node_id = child.respond_to?(:archetype_node_id) ? child.archetype_node_id : nil
138
+ return OpenEHR::Path::Segment.new(attribute.to_s) if node_id.nil?
139
+
140
+ sharing_node_id = siblings.count do |sibling|
141
+ sibling.respond_to?(:archetype_node_id) && sibling.archetype_node_id == node_id
142
+ end
143
+ if sharing_node_id > 1 && child.respond_to?(:name) && child.name.respond_to?(:value)
144
+ OpenEHR::Path::Segment.new(attribute.to_s, archetype_node_id: node_id, name: child.name.value)
145
+ else
146
+ OpenEHR::Path::Segment.new(attribute.to_s, archetype_node_id: node_id)
147
+ end
41
148
  end
42
149
  end
43
150
 
@@ -79,7 +186,7 @@ module OpenEHR
79
186
 
80
187
  def concept
81
188
  if self.is_archetype_root?
82
- return DvText.new(:value =>
189
+ return OpenEHR::RM::DataTypes::Text::DvText.new(:value =>
83
190
  @archetype_details.archetype_id.concept_name)
84
191
  else
85
192
  raise ArgumentError, 'this is not root'
@@ -159,6 +266,8 @@ module OpenEHR
159
266
  class FeederAuditDetails
160
267
  attr_reader :system_id
161
268
  attr_accessor :provider, :location, :time, :subject, :version_id
269
+ # RM 1.1.0 (SPECRM-74): optional custom meta-data, ITEM_STRUCTURE.
270
+ attr_accessor :other_details
162
271
 
163
272
  def initialize(args = { })
164
273
  self.system_id = args[:system_id]
@@ -167,6 +276,7 @@ module OpenEHR
167
276
  self.time = args[:time]
168
277
  self.subject = args[:subject]
169
278
  self.version_id = args[:version_id]
279
+ self.other_details = args[:other_details]
170
280
  end
171
281
 
172
282
  def system_id=(system_id)
@@ -99,7 +99,7 @@ module OpenEHR
99
99
  end
100
100
 
101
101
  def owner_id
102
- return HierObjectID.new(:value => @uid.value)
102
+ return OpenEHR::RM::Support::Identification::HierObjectID.new(:value => @uid.value)
103
103
  end
104
104
 
105
105
  def is_branch?
@@ -187,7 +187,7 @@ module OpenEHR
187
187
  end
188
188
 
189
189
  def all_versions=(all_versions)
190
- if all_versions.nil? || all_versions.size < 0
190
+ if all_versions.nil? || all_versions.empty?
191
191
  raise ArgumentError, 'version count invalid'
192
192
  end
193
193
  @all_versions = all_versions
@@ -251,12 +251,7 @@ module OpenEHR
251
251
  end
252
252
 
253
253
  def latest_trunk_version
254
- trunk_versions = [ ]
255
- @all_versions.each do |ver|
256
- if ver.uid.version_tree_id.trunk_version == '1'
257
- trunk_versions << ver
258
- end
259
- end
254
+ trunk_versions = @all_versions.reject { |ver| ver.uid.is_branch? }
260
255
  sorted_trunk_version = trunk_versions.sort do |a,b|
261
256
  a.commit_audit.time_committed <=> b.commit_audit.time_committed
262
257
  end
@@ -11,11 +11,15 @@ module OpenEHR
11
11
  class Folder < OpenEHR::RM::Common::Archetyped::Locatable
12
12
  attr_accessor :items
13
13
  attr_reader :folders
14
+ # details: ITEM_STRUCTURE [0..1], "Archetypable meta-data for FOLDER".
15
+ attr_accessor :details
16
+ path_attribute :folders, :items, :details
14
17
 
15
18
  def initialize(args = { })
16
19
  super(args)
17
20
  self.folders = args[:folders]
18
21
  self.items = args[:items]
22
+ self.details = args[:details]
19
23
  end
20
24
 
21
25
  def folders=(folders)
@@ -99,38 +99,52 @@ module OpenEHR
99
99
  end
100
100
 
101
101
  class PartyIdentified < PartyProxy
102
- attr_reader :name, :identifier
102
+ attr_reader :name, :identifiers
103
103
 
104
104
  def initialize(args = { })
105
- if args[:external_ref].nil? && args[:name].nil? &&
106
- args[:identifier].nil?
105
+ identifiers = args[:identifiers] || args[:identifier]
106
+ if args[:external_ref].nil? && args[:name].nil? && identifiers.nil?
107
107
  raise ArgumentError, 'cannot identified'
108
108
  end
109
109
  self.name = args[:name]
110
- self.identifier = args[:identifier]
110
+ self.identifiers = identifiers
111
111
  super(args)
112
112
  end
113
113
 
114
114
  def name=(name)
115
- if name.nil? && @external_ref.nil? && @identifier.nil?
115
+ if name.nil? && @external_ref.nil? && @identifiers.nil?
116
116
  raise ArgumentError, 'cannot identified'
117
117
  end
118
118
  raise ArgumentError, 'invaild name' unless name.nil? || !name.empty?
119
119
  @name = name
120
120
  end
121
121
 
122
- def identifier=(identifier)
123
- if @name.nil? && @external_ref.nil? && identifier.nil?
122
+ def identifiers=(identifiers)
123
+ if @name.nil? && @external_ref.nil? && identifiers.nil?
124
124
  raise ArgumentError, 'cannot identified'
125
125
  end
126
- if !identifier.nil? && identifier.empty?
126
+ if !identifiers.nil? && identifiers.empty?
127
127
  raise ArgumentError, 'invaild identifier'
128
128
  end
129
- @identifier = identifier
129
+ @identifiers = identifiers
130
+ end
131
+
132
+ # Deprecated: PartyIdentified's identifiers attribute is a
133
+ # List<DV_IDENTIFIER> per spec, so the plural #identifiers is
134
+ # the correct name; these singular aliases exist only for
135
+ # backward compatibility with earlier releases.
136
+ def identifier
137
+ warn '[DEPRECATED] PartyIdentified#identifier is deprecated; use #identifiers instead'
138
+ @identifiers
139
+ end
140
+
141
+ def identifier=(identifier)
142
+ warn '[DEPRECATED] PartyIdentified#identifier= is deprecated; use #identifiers= instead'
143
+ self.identifiers = identifier
130
144
  end
131
145
 
132
146
  def external_ref=(external_ref)
133
- if @name.nil? && @identifier.nil? && external_ref.nil?
147
+ if @name.nil? && @identifiers.nil? && external_ref.nil?
134
148
  raise ArgumentError, 'invalid external_ref'
135
149
  end
136
150
  @external_ref = external_ref
@@ -48,12 +48,13 @@ module OpenEHR
48
48
  end
49
49
 
50
50
  def subject_is_self?
51
- return @subject.instance_of? PartySelf
51
+ return @subject.instance_of? OpenEHR::RM::Common::Generic::PartySelf
52
52
  end
53
53
  end
54
54
 
55
55
  class AdminEntry < Entry
56
56
  attr_reader :data
57
+ path_attribute :data
57
58
 
58
59
  def initialize(args = { })
59
60
  super(args)
@@ -67,6 +68,7 @@ module OpenEHR
67
68
  end
68
69
  class CareEntry < Entry
69
70
  attr_accessor :protocol, :guideline_id
71
+ path_attribute :protocol
70
72
 
71
73
  def initialize(args = { })
72
74
  super(args)
@@ -78,6 +80,7 @@ module OpenEHR
78
80
  class Observation < CareEntry
79
81
  attr_reader :data
80
82
  attr_accessor :state
83
+ path_attribute :data, :state
81
84
 
82
85
  def initialize(args = { })
83
86
  super(args)
@@ -93,6 +96,7 @@ module OpenEHR
93
96
 
94
97
  class Evaluation < CareEntry
95
98
  attr_reader :data
99
+ path_attribute :data
96
100
 
97
101
  def initialize(args = { })
98
102
  super(args)
@@ -108,6 +112,7 @@ module OpenEHR
108
112
  class Instruction < CareEntry
109
113
  attr_reader :narrative, :activities
110
114
  attr_accessor :expiry_time, :wf_definition
115
+ path_attribute :activities
111
116
 
112
117
  def initialize(args = { })
113
118
  super(args)
@@ -134,6 +139,7 @@ module OpenEHR
134
139
 
135
140
  class Activity < OpenEHR::RM::Common::Archetyped::Locatable
136
141
  attr_reader :description, :timing, :action_archetype_id
142
+ path_attribute :description
137
143
 
138
144
  def initialize(args = { })
139
145
  super(args)
@@ -149,10 +155,10 @@ module OpenEHR
149
155
  @description = description
150
156
  end
151
157
 
158
+ # RM 1.1.0: optional - may be omitted when timing is instead
159
+ # represented structurally in description, or is unavailable
160
+ # (e.g. imported legacy data).
152
161
  def timing=(timing)
153
- if timing.nil?
154
- raise ArgumentError, 'timing is mandatory'
155
- end
156
162
  @timing = timing
157
163
  end
158
164
 
@@ -167,7 +173,8 @@ module OpenEHR
167
173
  class Action < CareEntry
168
174
  attr_reader :time, :description, :ism_transition
169
175
  attr_accessor :instruction_details
170
-
176
+ path_attribute :description, :ism_transition, :instruction_details
177
+
171
178
  def initialize(args = { })
172
179
  super(args)
173
180
  self.description = args[:description]
@@ -201,6 +208,7 @@ module OpenEHR
201
208
  class InstructionDetails < OpenEHR::RM::Common::Archetyped::Pathable
202
209
  attr_reader :instruction_id, :activity_id
203
210
  attr_accessor :wf_details
211
+ path_attribute :wf_details
204
212
 
205
213
  def initialize(args = { })
206
214
  super(args)
@@ -227,18 +235,26 @@ module OpenEHR
227
235
  class IsmTransition < OpenEHR::RM::Common::Archetyped::Pathable
228
236
  attr_reader :current_state, :transition
229
237
  attr_accessor :careflow_step
238
+ # RM 1.1.0: optional list of one or more reasons for this
239
+ # careflow step having been taken (e.g. useful in medication
240
+ # management, where several reasons may apply).
241
+ attr_accessor :reason
230
242
 
231
243
  def initialize(args = { })
232
244
  super(args)
233
245
  self.current_state = args[:current_state]
234
246
  self.transition = args[:transition]
235
247
  self.careflow_step = args[:careflow_step]
248
+ self.reason = args[:reason]
236
249
  end
237
250
 
238
251
  def current_state=(current_state)
239
252
  if current_state.nil?
240
253
  raise ArgumentError, 'current_state is mandatory'
241
254
  end
255
+ unless OpenEHR::TerminologyService.has_code_for_group?('ism transition current state', current_state.defining_code.code_string)
256
+ raise ArgumentError, 'current_state code is invalid'
257
+ end
242
258
  @current_state = current_state
243
259
  end
244
260
 
@@ -246,6 +262,9 @@ module OpenEHR
246
262
  if transition.nil?
247
263
  raise ArgumentError, 'transition is mandatory'
248
264
  end
265
+ unless OpenEHR::TerminologyService.has_code_for_group?('ism transition careflow transition', transition.defining_code.code_string)
266
+ raise ArgumentError, 'transition code is invalid'
267
+ end
249
268
  @transition = transition
250
269
  end
251
270
  end
@@ -10,7 +10,8 @@ module OpenEHR
10
10
  module Content
11
11
  module Navigation
12
12
  class Section < ::OpenEHR::RM::Composition::Content::ContentItem
13
- attr_reader :items
13
+ attr_reader :items
14
+ path_attribute :items
14
15
 
15
16
  def initialize(args = { })
16
17
  super(args)
@@ -9,7 +9,9 @@ module OpenEHR
9
9
  module Composition
10
10
  class Composition < OpenEHR::RM::Common::Archetyped::Locatable
11
11
  attr_reader :language, :category, :territory, :composer
12
- attr_accessor :content, :context
12
+ attr_accessor :context
13
+ attr_reader :content
14
+ path_attribute :content, :context
13
15
 
14
16
  def initialize(args = { })
15
17
  super(args)
@@ -25,6 +27,9 @@ module OpenEHR
25
27
  if language.nil?
26
28
  raise ArgumentError, 'language is mandatory'
27
29
  end
30
+ unless OpenEHR::TerminologyService.valid_code?('ISO_639-1', language.code_string)
31
+ raise ArgumentError, 'language code is invalid'
32
+ end
28
33
  @language = language
29
34
  end
30
35
 
@@ -32,6 +37,9 @@ module OpenEHR
32
37
  if category.nil?
33
38
  raise ArgumentError, 'category is mandatory'
34
39
  end
40
+ unless OpenEHR::TerminologyService.has_code_for_group?('composition category', category.defining_code.code_string)
41
+ raise ArgumentError, 'category code is invalid'
42
+ end
35
43
  @category = category
36
44
  end
37
45
 
@@ -49,6 +57,11 @@ module OpenEHR
49
57
  @composer = composer
50
58
  end
51
59
 
60
+ def content=(content)
61
+ content.each { |item| item.parent = self if item.respond_to?(:parent=) } if content.respond_to?(:each)
62
+ @content = content
63
+ end
64
+
52
65
  def is_persistent?
53
66
  return category.value == 'persistent'
54
67
  end
@@ -56,7 +69,7 @@ module OpenEHR
56
69
 
57
70
  class EventContext < OpenEHR::RM::Common::Archetyped::Pathable
58
71
  attr_reader :start_time, :setting, :participations, :location
59
- attr_accessor :end_time, :other_context
72
+ attr_accessor :end_time, :other_context, :health_care_facility
60
73
 
61
74
  def initialize(args = { })
62
75
  super(args)
@@ -66,6 +79,7 @@ module OpenEHR
66
79
  self.participations = args[:participations]
67
80
  self.location = args[:location]
68
81
  self.other_context = args[:other_context]
82
+ self.health_care_facility = args[:health_care_facility]
69
83
  end
70
84
 
71
85
  def start_time=(start_time)
@@ -79,6 +93,9 @@ module OpenEHR
79
93
  if setting.nil?
80
94
  raise ArgumentError, 'setting is mandatory'
81
95
  end
96
+ unless OpenEHR::TerminologyService.has_code_for_group?('event context setting', setting.defining_code.code_string)
97
+ raise ArgumentError, 'setting code is invalid'
98
+ end
82
99
  @setting = setting
83
100
  end
84
101