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
@@ -18,6 +18,7 @@ module OpenEHR
18
18
  class History < OpenEHR::RM::DataStructures::DataStructure
19
19
  attr_reader :origin, :events
20
20
  attr_accessor :duration, :period, :summary
21
+ path_attribute :events, :summary
21
22
 
22
23
  def initialize(args = { })
23
24
  super(args)
@@ -37,6 +38,7 @@ module OpenEHR
37
38
  if !events.nil? and events.empty?
38
39
  raise ArgumentError, 'events should not be empty'
39
40
  end
41
+ events&.each { |event| event.parent = self if event.respond_to?(:parent=) }
40
42
  @events = events
41
43
  end
42
44
 
@@ -48,6 +50,7 @@ module OpenEHR
48
50
  class Event < OpenEHR::RM::Common::Archetyped::Locatable
49
51
  attr_reader :data, :time
50
52
  attr_accessor :state
53
+ path_attribute :data, :state
51
54
 
52
55
  def initialize(args = { })
53
56
  super(args)
@@ -16,8 +16,13 @@ module OpenEHR
16
16
  end
17
17
 
18
18
  class Element < Item
19
+ # openEHR "Null flavours" code set (terminology_id "openehr"):
20
+ # 271 no information, 272 masked, 273 not applicable, 253 unknown.
21
+ NULL_FLAVOUR_CODES = %w[271 272 273 253].freeze
22
+
19
23
  attr_accessor :value
20
24
  attr_reader :null_flavor
25
+ path_attribute :value, :null_flavour
21
26
  def initialize(args = {})
22
27
  super(args)
23
28
  self.value = args[:value]
@@ -25,18 +30,15 @@ module OpenEHR
25
30
  end
26
31
 
27
32
  def null_flavor=(null_flavor)
28
- sr = nil
29
- if !null_flavor.nil? and
30
- null_flavor.defining_code.terminology_id.name == 'openehr'
31
- sr = Terminology.find(:first,
32
- :conditions => "code = '#{null_flavor.defining_code.code_string}'")
33
- end
34
- if null_flavor.nil? or (!sr.nil? and sr.group == 'null flavours')
35
- @null_flavor = null_flavor
36
- else
33
+ if !null_flavor.nil? &&
34
+ !(null_flavor.defining_code.terminology_id.name == 'openehr' &&
35
+ NULL_FLAVOUR_CODES.include?(null_flavor.defining_code.code_string))
37
36
  raise ArgumentError, 'null_flavor is invalid'
38
37
  end
38
+ @null_flavor = null_flavor
39
39
  end
40
+ alias null_flavour null_flavor
41
+ alias null_flavour= null_flavor=
40
42
 
41
43
  def is_null?
42
44
  return @value.nil?
@@ -45,6 +47,7 @@ module OpenEHR
45
47
 
46
48
  class Cluster < Item
47
49
  attr_reader :items
50
+ path_attribute :items
48
51
 
49
52
  def initialize(args = {})
50
53
  super(args)
@@ -55,6 +58,7 @@ module OpenEHR
55
58
  if !items.nil? and items.empty?
56
59
  raise ArgumentError, 'items should not empty'
57
60
  end
61
+ items&.each { |item| item.parent = self if item.respond_to?(:parent=) }
58
62
  @items = items
59
63
  end
60
64
  end
@@ -17,6 +17,7 @@ module OpenEHR
17
17
 
18
18
  class ItemSingle < ItemStructure
19
19
  attr_reader :item
20
+ path_attribute :item
20
21
 
21
22
  def initialize(args = {})
22
23
  super(args)
@@ -35,6 +36,7 @@ module OpenEHR
35
36
 
36
37
  class ItemList < ItemStructure
37
38
  attr_accessor :items
39
+ path_attribute :items
38
40
 
39
41
  def initialize(args = {})
40
42
  super(args)
@@ -74,6 +76,7 @@ module OpenEHR
74
76
 
75
77
  class ItemTable < ItemStructure
76
78
  attr_accessor :rows
79
+ path_attribute :rows
77
80
 
78
81
  def initialize(args = {})
79
82
  super(args)
@@ -165,25 +168,29 @@ module OpenEHR
165
168
  end
166
169
 
167
170
  def element_at_named_cell(row_key, column_key)
168
- i,j=0,0
171
+ column_index = 0
169
172
  @rows[0].items.each do |c|
170
173
  break if c.name.value == column_key
171
- i+=1
174
+ column_index += 1
172
175
  end
176
+ row_index = 0
173
177
  @rows.each do |row|
174
178
  break if row.name.value == row_key
175
- j+=1
176
- end
177
- return element_at_cell_ij(i,j)
179
+ row_index += 1
180
+ end
181
+ return element_at_cell_ij(row_index + 1, column_index + 1)
178
182
  end
179
183
 
180
184
  def as_hierarchy
181
- return @rows[0]
185
+ return Cluster.new(:name => @name,
186
+ :archetype_node_id => @archetype_node_id,
187
+ :items => @rows)
182
188
  end
183
189
  end
184
190
 
185
191
  class ItemTree < ItemStructure
186
192
  attr_accessor :items
193
+ path_attribute :items
187
194
 
188
195
  def initialize(args ={ })
189
196
  super(args)
@@ -191,18 +198,11 @@ module OpenEHR
191
198
  end
192
199
 
193
200
  def has_element_path?(path)
194
- paths = [ ]
195
- @items.each do |item|
196
- paths << item.archetype_node_id
197
- end
198
- return paths.include? path
201
+ path_exists?(normalize_element_path(path))
199
202
  end
200
203
 
201
204
  def element_at_path(path)
202
- @items.each do |item|
203
- return item if item.archetype_node_id == path
204
- end
205
- return nil
205
+ item_at_path(normalize_element_path(path))
206
206
  end
207
207
 
208
208
  def as_hierarchy
@@ -210,6 +210,19 @@ module OpenEHR
210
210
  :archetype_node_id => @archetype_node_id,
211
211
  :items => @items)
212
212
  end
213
+
214
+ private
215
+
216
+ # Legacy callers pass a bare node id (e.g. 'at0002') rather
217
+ # than a real path; normalize that into /items[at0002] so it
218
+ # keeps working, via the real Pathable machinery.
219
+ def normalize_element_path(path)
220
+ return path if path.is_a?(OpenEHR::Path) || path.start_with?('/')
221
+
222
+ warn "[DEPRECATED] ItemTree#element_at_path/has_element_path? with a bare node id " \
223
+ "(#{path.inspect}) is deprecated; use a full path like \"/items[#{path}]\" instead"
224
+ "/items[#{path}]"
225
+ end
213
226
  end
214
227
  end # of ItemStructure
215
228
  end # of DataStructures
@@ -39,9 +39,11 @@ module OpenEHR
39
39
 
40
40
  private
41
41
 
42
+ CHARSET_LIST_PATH = File.expand_path('charset.lst', __dir__)
43
+
42
44
  def charset_valid?(charset)
43
45
  result = false
44
- open('lib/openehr/rm/data_types/charset.lst') do |file|
46
+ File.open(CHARSET_LIST_PATH) do |file|
45
47
  while line = file.gets
46
48
  if charset == line.chomp
47
49
  result = true
@@ -57,7 +59,8 @@ module OpenEHR
57
59
  class DvMultimedia < DvEncapsulated
58
60
  attr_reader :media_type
59
61
  attr_accessor :uri, :data, :compression_algorithm,
60
- :integrity_check, :integrity_check_algorithm, :alternate_text
62
+ :integrity_check, :integrity_check_algorithm, :alternate_text,
63
+ :thumbnail
61
64
 
62
65
  def initialize(args = {})
63
66
  super(args)
@@ -68,14 +71,31 @@ module OpenEHR
68
71
  self.integrity_check = args[:integrity_check]
69
72
  self.integrity_check_algorithm = args[:integrity_check_algorithm]
70
73
  self.alternate_text = args[:alternate_text]
74
+ self.thumbnail = args[:thumbnail]
71
75
  end
72
76
 
73
77
  def media_type=(media_type)
74
- if media_type.code_string.nil?
78
+ if media_type.nil? || media_type.code_string.nil?
75
79
  raise ArgumentError, 'media_type should not be nil'
76
80
  end
77
81
  @media_type = media_type
78
82
  end
83
+
84
+ def is_external?
85
+ !@uri.nil?
86
+ end
87
+
88
+ def is_inline?
89
+ !@data.nil?
90
+ end
91
+
92
+ def is_compressed?
93
+ !@compression_algorithm.nil?
94
+ end
95
+
96
+ def has_integrity_check?
97
+ !@integrity_check_algorithm.nil?
98
+ end
79
99
  end
80
100
 
81
101
  class DvParsable < DvEncapsulated
@@ -224,6 +224,7 @@ module OpenEHR
224
224
  self.minutes = iso8601_duration.minutes
225
225
  self.seconds = iso8601_duration.seconds
226
226
  self.fractional_second = iso8601_duration.fractional_second
227
+ self.negative = iso8601_duration.negative?
227
228
  end
228
229
 
229
230
  def magnitude
@@ -244,7 +245,14 @@ module OpenEHR
244
245
  seconds += @seconds if @seconds
245
246
  seconds += @fractional_second if @fractional_second
246
247
  seconds += minutes * SECONDS_IN_MINUTE if minutes
247
- return seconds
248
+ negative? ? -seconds : seconds
249
+ end
250
+
251
+ # DV_AMOUNT.negative alias "-": a new DvDuration with the
252
+ # sign flipped, leaving self untouched.
253
+ def -@
254
+ text = value.start_with?('-') ? value[1..-1] : "-#{value}"
255
+ DvDuration.new(:value => text)
248
256
  end
249
257
  end
250
258
  end # of DateTime
@@ -24,7 +24,7 @@ module OpenEHR
24
24
  if @normal_range.nil? and @normal_status.nil?
25
25
  return false
26
26
  elsif !@normal_range.nil?
27
- return @normal_range.has(@value)
27
+ return @normal_range.has?(@value)
28
28
  elsif !@normal_status.nil?
29
29
  return @normal_status.code_string == 'N'
30
30
  end
@@ -143,6 +143,40 @@ module OpenEHR
143
143
  end
144
144
  end
145
145
 
146
+ # RM 1.1.0 (SPECRM-19): like DV_ORDINAL, but for scales/scores
147
+ # whose points are Real-valued rather than Integer-valued (e.g.
148
+ # the Borg CR10 breathlessness scale, which includes 0.5).
149
+ class DvScale < DvOrdered
150
+ attr_reader :value, :symbol
151
+
152
+ def initialize(args = {})
153
+ super(args)
154
+ self.symbol = args[:symbol]
155
+ end
156
+
157
+ def value=(value)
158
+ raise ArgumentError, 'value should not be nil' if value.nil?
159
+ raise ArgumentError, 'value should be a Real (Numeric)' unless value.is_a?(Numeric)
160
+ @value = value
161
+ end
162
+
163
+ def symbol=(symbol)
164
+ raise ArgumentError, 'symbol should not be nil' if symbol.nil?
165
+ @symbol = symbol
166
+ end
167
+
168
+ def <=>(other)
169
+ @value <=> other.value
170
+ end
171
+
172
+ def is_strictly_comparable_to?(others)
173
+ return false unless super(others)
174
+
175
+ others.symbol.defining_code.terminology_id.value ==
176
+ @symbol.defining_code.terminology_id.value
177
+ end
178
+ end
179
+
146
180
  class DvAbsoluteQuantity < DvQuantified
147
181
  attr_accessor :accuracy
148
182
 
@@ -207,14 +241,21 @@ module OpenEHR
207
241
  return result
208
242
  end
209
243
 
210
- def -(other)
211
- other.magnitude = - other.magnitude
212
- self+(other)
244
+ def -(other)
245
+ negated = other.dup
246
+ negated.magnitude = -other.magnitude
247
+ self + negated
248
+ end
249
+
250
+ def -@
251
+ negated = self.dup
252
+ negated.magnitude = -magnitude
253
+ negated
213
254
  end
214
255
 
215
256
  def set_accuracy(accuracy, accuracy_percent)
216
257
  if accuracy_percent
217
- raise ArgumentError, 'accuracy invalid' if accuracy < 0.0 || accuracy > 100.0
258
+ raise ArgumentError, 'accuracy invalid' unless self.class.valid_percentage(accuracy)
218
259
  else
219
260
  raise ArgumentError, 'accuracy invaild' if accuracy < 0.0 || accuracy > 1.0
220
261
  end
@@ -224,15 +265,26 @@ module OpenEHR
224
265
  def accuracy_is_percent?
225
266
  return @accuracy_percent
226
267
  end
268
+
269
+ def self.valid_percentage(number)
270
+ number >= 0.0 && number <= 100.0
271
+ end
227
272
  end
228
273
 
229
274
  class DvQuantity < DvAmount
230
275
  attr_reader :units, :precision
276
+ # RM 1.1.0 (SPECRM-65): units is UCUM by default; units_system
277
+ # names another units system when units isn't UCUM, and
278
+ # units_display_name gives a displayable form when the units
279
+ # code itself isn't one (e.g. UCUM 'Cel' vs '°C'). Both optional.
280
+ attr_accessor :units_system, :units_display_name
231
281
 
232
282
  def initialize(args = {})
233
283
  super(args)
234
284
  self.units = args[:units]
235
285
  self.precision = args[:precision]
286
+ self.units_system = args[:units_system]
287
+ self.units_display_name = args[:units_display_name]
236
288
  end
237
289
 
238
290
  def units=(units)
@@ -248,14 +300,9 @@ module OpenEHR
248
300
  end
249
301
 
250
302
  def is_strictly_comparable_to?(others)
251
- unless super(others)
252
- return false
253
- end
254
- if others.units == @units
255
- return true
256
- else
257
- return false
258
- end
303
+ return false unless super(others)
304
+
305
+ others.units == @units && others.units_system == @units_system
259
306
  end
260
307
 
261
308
  def is_integral?
@@ -268,7 +315,12 @@ module OpenEHR
268
315
  end
269
316
 
270
317
  class DvCount < DvAmount
271
-
318
+ def magnitude=(magnitude)
319
+ unless magnitude.is_a?(Integer)
320
+ raise ArgumentError, 'magnitude should be an Integer'
321
+ end
322
+ super
323
+ end
272
324
  end
273
325
 
274
326
  class ReferenceRange
@@ -59,10 +59,15 @@ module OpenEHR
59
59
 
60
60
  class CodePhrase
61
61
  attr_reader :terminology_id, :code_string
62
+ # RM 1.1.0: optional preferred term for code_string, typically
63
+ # used in integration mappings that need both a (non-preferred)
64
+ # actual term and a preferred term.
65
+ attr_accessor :preferred_term
62
66
 
63
67
  def initialize(args = {})
64
68
  self.code_string = args[:code_string]
65
69
  self.terminology_id = args[:terminology_id]
70
+ self.preferred_term = args[:preferred_term]
66
71
  end
67
72
 
68
73
  def terminology_id=(terminology_id)
@@ -152,6 +157,9 @@ module OpenEHR
152
157
  attr_reader :items
153
158
 
154
159
  def initialize(args ={})
160
+ warn '[DEPRECATED] DvParagraph (DV_PARAGRAPH) is deprecated as of RM 1.0.4; ' \
161
+ 'use DvText/DvCodedText with markdown or newlines instead. ' \
162
+ 'It remains legal for legacy data.'
155
163
  self.items = args[:items]
156
164
  end
157
165
 
@@ -17,16 +17,17 @@ module OpenEHR
17
17
  @value = value
18
18
  end
19
19
 
20
- def calender_alignment
21
- raise NotImlementedError, "calender_alignment must be implemented"
20
+ def calendar_alignment
21
+ raise NotImplementedError, "calendar_alignment must be implemented"
22
22
  end
23
+ alias calender_alignment calendar_alignment
23
24
 
24
25
  def event_alignment
25
- raise NotImlementedError, "event_alignment must be implemented"
26
+ raise NotImplementedError, "event_alignment must be implemented"
26
27
  end
27
28
 
28
29
  def institution_specified
29
- raise NotImlementedError, "institution_specified must be implemented"
30
+ raise NotImplementedError, "institution_specified must be implemented"
30
31
  end
31
32
  end
32
33
 
@@ -2,6 +2,7 @@
2
2
  # http://www.openehr.org/uml/release-1.0.1/Browsable/_9_5_76d0249_1118674798473_6021_0Report.html
3
3
  # Ticket refs #45
4
4
  require_relative 'common/archetyped'
5
+ require_relative 'common/change_control'
5
6
 
6
7
  module OpenEHR
7
8
  module RM
@@ -247,7 +248,7 @@ module OpenEHR
247
248
  end
248
249
 
249
250
  def source=(source)
250
- if source.nil? or source.id.value != @uid.value
251
+ if source.nil?
251
252
  raise ArgumentError, 'source is invalid'
252
253
  end
253
254
  @source = source
@@ -255,13 +256,13 @@ module OpenEHR
255
256
 
256
257
  def target=(target)
257
258
  if target.nil?
258
- raise ArgumentError, 'taraget is invalid'
259
+ raise ArgumentError, 'target is invalid'
259
260
  end
260
261
  @target = target
261
262
  end
262
263
  end
263
264
 
264
- class VersionedParty < OpenEHR::RM::Common::Archetyped::Locatable
265
+ class VersionedParty < OpenEHR::RM::Common::ChangeControl::VersionedObject
265
266
 
266
267
  end
267
268
  end # of Demographic
@@ -9,7 +9,8 @@ module OpenEHR
9
9
  module EHR
10
10
  class EHR
11
11
  attr_reader :system_id, :ehr_id, :time_created, :contributions,
12
- :ehr_access, :ehr_status, :compositions, :directory
12
+ :ehr_access, :ehr_status, :compositions, :directory,
13
+ :folders
13
14
 
14
15
  def initialize(args = { })
15
16
  self.system_id = args[:system_id]
@@ -20,6 +21,7 @@ module OpenEHR
20
21
  self.ehr_status = args[:ehr_status]
21
22
  self.compositions = args[:compositions]
22
23
  self.directory = args[:directory]
24
+ self.folders = args[:folders]
23
25
  end
24
26
 
25
27
  def system_id=(system_id)
@@ -89,6 +91,22 @@ module OpenEHR
89
91
  end
90
92
  @directory = directory
91
93
  end
94
+
95
+ # RM 1.1.0 (SPECRM-55): additional Folder hierarchies for this
96
+ # EHR. When set, directory is kept pointing at folders.item(1)
97
+ # (the Directory_in_folders invariant), for backward
98
+ # compatibility with pre-1.1.0 systems that only had directory.
99
+ def folders=(folders)
100
+ unless folders.nil?
101
+ folders.each do |f|
102
+ if f.type != 'VERSIONED_FOLDER'
103
+ raise ArgumentError, 'folder type should be VERSIONED_FOLDER'
104
+ end
105
+ end
106
+ @directory = folders.first
107
+ end
108
+ @folders = folders
109
+ end
92
110
  end
93
111
 
94
112
  class VersionedEHRAccess < OpenEHR::RM::Common::ChangeControl::VersionedObject
@@ -120,6 +138,7 @@ module OpenEHR
120
138
  class EHRStatus < OpenEHR::RM::Common::Archetyped::Locatable
121
139
  attr_reader :subject
122
140
  attr_accessor :is_modifiable, :is_queryable, :other_details
141
+ path_attribute :other_details
123
142
 
124
143
  def initialize(args = { })
125
144
  super(args)