openehr 1.3.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +257 -12
  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
@@ -21,13 +21,30 @@ module OpenEHR
21
21
  param.each_with_object({}) do |item, parameters|
22
22
  key = item.shift
23
23
  value = item.shift
24
- if value.instance_of? Hash
25
- parameters[key] = Factory.create(value[:_type], **value)
26
- else
27
- parameters[key] = value
28
- end
24
+ parameters[key] = convert_value(value)
29
25
  end
30
- end
26
+ end
27
+
28
+ private
29
+
30
+ # A canonical-JSON attribute value is either a typed sub-object
31
+ # (Hash with _type), a List<...> of those (Array - openEHR RM
32
+ # multiplicity 0..*/1..* attributes, e.g. COMPOSITION.content,
33
+ # HISTORY.events, ITEM_TREE.items), or a plain value (String,
34
+ # Numeric, ...). Arrays recurse so nested multiplicities (e.g. a
35
+ # CLUSTER's own items) convert too; non-Hash array elements
36
+ # (there is no List<String>/List<Numeric> attribute in this RM,
37
+ # but doubles/pre-built objects show up in specs) pass through
38
+ # unchanged.
39
+ def convert_value(value)
40
+ if value.instance_of? Hash
41
+ Factory.create(value[:_type], **value)
42
+ elsif value.instance_of? Array
43
+ value.map { |element| convert_value(element) }
44
+ else
45
+ value
46
+ end
47
+ end
31
48
  end
32
49
 
33
50
  def build
@@ -80,12 +97,6 @@ module OpenEHR
80
97
  end
81
98
  end
82
99
 
83
- class TermMappingFactory
84
- def self.create(*param)
85
- OpenEHR::RM::DataTypes::Text::TermMapping.new(*param)
86
- end
87
- end
88
-
89
100
  class CodePhraseFactory
90
101
  def self.create(*param)
91
102
  OpenEHR::RM::DataTypes::Text::CodePhrase.new(*param)
@@ -128,6 +139,12 @@ module OpenEHR
128
139
  end
129
140
  end
130
141
 
142
+ class DvScaleFactory
143
+ def self.create(*param)
144
+ OpenEHR::RM::DataTypes::Quantity::DvScale.new(*param)
145
+ end
146
+ end
147
+
131
148
  class DvQuantifiedFactory
132
149
  def self.create(*param)
133
150
  DataTypes::Quantity::DvQuantified.new(*param)
@@ -296,6 +313,234 @@ module OpenEHR
296
313
  end
297
314
  end
298
315
 
316
+ class ElementFactory
317
+ def self.create(param)
318
+ DataStructures::ItemStructure::Representation::Element.new(param)
319
+ end
320
+ end
321
+
322
+ class ItemTreeFactory
323
+ def self.create(param)
324
+ DataStructures::ItemStructure::ItemTree.new(param)
325
+ end
326
+ end
327
+
328
+ class ItemListFactory
329
+ def self.create(param)
330
+ DataStructures::ItemStructure::ItemList.new(param)
331
+ end
332
+ end
333
+
334
+ class ItemSingleFactory
335
+ def self.create(param)
336
+ DataStructures::ItemStructure::ItemSingle.new(param)
337
+ end
338
+ end
339
+
340
+ class ItemTableFactory
341
+ def self.create(param)
342
+ DataStructures::ItemStructure::ItemTable.new(param)
343
+ end
344
+ end
345
+
346
+ class HistoryFactory
347
+ def self.create(param)
348
+ DataStructures::History::History.new(param)
349
+ end
350
+ end
351
+
352
+ class EventFactory
353
+ def self.create(param)
354
+ DataStructures::History::Event.new(param)
355
+ end
356
+ end
357
+
358
+ class PointEventFactory
359
+ def self.create(param)
360
+ DataStructures::History::PointEvent.new(param)
361
+ end
362
+ end
363
+
364
+ class IntervalEventFactory
365
+ def self.create(param)
366
+ DataStructures::History::IntervalEvent.new(param)
367
+ end
368
+ end
369
+
370
+ class EvaluationFactory
371
+ def self.create(param)
372
+ Composition::Content::Entry::Evaluation.new(param)
373
+ end
374
+ end
375
+
376
+ class InstructionFactory
377
+ def self.create(param)
378
+ Composition::Content::Entry::Instruction.new(param)
379
+ end
380
+ end
381
+
382
+ class ActionFactory
383
+ def self.create(param)
384
+ Composition::Content::Entry::Action.new(param)
385
+ end
386
+ end
387
+
388
+ class ActivityFactory
389
+ def self.create(param)
390
+ Composition::Content::Entry::Activity.new(param)
391
+ end
392
+ end
393
+
394
+ class AdminEntryFactory
395
+ def self.create(param)
396
+ Composition::Content::Entry::AdminEntry.new(param)
397
+ end
398
+ end
399
+
400
+ class InstructionDetailsFactory
401
+ def self.create(param)
402
+ Composition::Content::Entry::InstructionDetails.new(param)
403
+ end
404
+ end
405
+
406
+ class IsmTransitionFactory
407
+ def self.create(param)
408
+ Composition::Content::Entry::IsmTransition.new(param)
409
+ end
410
+ end
411
+
412
+ class GenericEntryFactory
413
+ def self.create(param)
414
+ Integration::GenericEntry.new(param)
415
+ end
416
+ end
417
+
418
+ class PartySelfFactory
419
+ def self.create(param)
420
+ Common::Generic::PartySelf.new(param)
421
+ end
422
+ end
423
+
424
+ class PartyRelatedFactory
425
+ def self.create(param)
426
+ Common::Generic::PartyRelated.new(param)
427
+ end
428
+ end
429
+
430
+ class ParticipationFactory
431
+ def self.create(param)
432
+ Common::Generic::Participation.new(param)
433
+ end
434
+ end
435
+
436
+ class LinkFactory
437
+ def self.create(param)
438
+ Common::Archetyped::Link.new(param)
439
+ end
440
+ end
441
+
442
+ class FeederAuditFactory
443
+ def self.create(param)
444
+ Common::Archetyped::FeederAudit.new(param)
445
+ end
446
+ end
447
+
448
+ class AuditDetailsFactory
449
+ def self.create(param)
450
+ Common::Generic::AuditDetails.new(param)
451
+ end
452
+ end
453
+
454
+ class AttestationFactory
455
+ def self.create(param)
456
+ Common::Generic::Attestation.new(param)
457
+ end
458
+ end
459
+
460
+ class ObjectRefFactory
461
+ def self.create(param)
462
+ Support::Identification::ObjectRef.new(param)
463
+ end
464
+ end
465
+
466
+ class LocatableRefFactory
467
+ def self.create(param)
468
+ Support::Identification::LocatableRef.new(param)
469
+ end
470
+ end
471
+
472
+ class ObjectVersionIdFactory
473
+ def self.create(param)
474
+ Support::Identification::ObjectVersionID.new(param)
475
+ end
476
+ end
477
+
478
+ class HierObjectIdFactory
479
+ def self.create(param)
480
+ Support::Identification::HierObjectID.new(param)
481
+ end
482
+ end
483
+
484
+ class UidBasedIdFactory
485
+ def self.create(param)
486
+ Support::Identification::UIDBasedID.new(param)
487
+ end
488
+ end
489
+
490
+ class AccessGroupRefFactory
491
+ def self.create(param)
492
+ Support::Identification::AccessGroupRef.new(param)
493
+ end
494
+ end
495
+
496
+ class PersonFactory
497
+ def self.create(param)
498
+ Demographic::Person.new(param)
499
+ end
500
+ end
501
+
502
+ class OrganisationFactory
503
+ def self.create(param)
504
+ Demographic::Organisation.new(param)
505
+ end
506
+ end
507
+
508
+ class RoleFactory
509
+ def self.create(param)
510
+ Demographic::Role.new(param)
511
+ end
512
+ end
513
+
514
+ class PartyIdentityFactory
515
+ def self.create(param)
516
+ Demographic::PartyIdentity.new(param)
517
+ end
518
+ end
519
+
520
+ class ContactFactory
521
+ def self.create(param)
522
+ Demographic::Contact.new(param)
523
+ end
524
+ end
525
+
526
+ class AddressFactory
527
+ def self.create(param)
528
+ Demographic::Address.new(param)
529
+ end
530
+ end
531
+
532
+ class CapabilityFactory
533
+ def self.create(param)
534
+ Demographic::Capability.new(param)
535
+ end
536
+ end
537
+
538
+ class PartyRelationshipFactory
539
+ def self.create(param)
540
+ Demographic::PartyRelationship.new(param)
541
+ end
542
+ end
543
+
299
544
  class CompositionFactory < Factory
300
545
  class << self
301
546
  def create_from_json(json)
@@ -9,6 +9,7 @@ module OpenEHR
9
9
  module Integration
10
10
  class GenericEntry < OpenEHR::RM::Composition::Content::ContentItem
11
11
  attr_reader :data
12
+ path_attribute :data
12
13
 
13
14
  def initialize(args = { })
14
15
  super(args)
@@ -68,7 +68,7 @@ module OpenEHR
68
68
  end
69
69
 
70
70
  def value=(value)
71
- if /([a-zA-Z]\w+)-([a-zA-Z]\w+)-([a-zA-Z]\w+)\.([a-zA-Z]\w+)(-([a-zA-Z]\w+))?\.(v[1-9]\d*)/ =~ value
71
+ if /\A([a-zA-Z]\w+)-([a-zA-Z]\w+)-([a-zA-Z]\w+)\.([a-zA-Z]\w+)(-([a-zA-Z]\w+))?\.(v\d+)\z/ =~ value
72
72
  self.rm_originator = $1
73
73
  self.rm_name = $2
74
74
  self.rm_entity = $3
@@ -108,7 +108,7 @@ module OpenEHR
108
108
  if domain_concept.nil? or domain_concept.empty?
109
109
  raise ArgumentError, "domain concept not valid"
110
110
  end
111
- if /([a-zA-Z]\w+)(-([a-zA-Z]\w))?/ =~ domain_concept
111
+ if /\A([a-zA-Z]\w+)(-([a-zA-Z]\w+))?\z/ =~ domain_concept
112
112
  self.concept_name = $1
113
113
  self.specialisation = $3
114
114
  else
@@ -249,6 +249,8 @@ module OpenEHR
249
249
  self.objectid = UID.new(:value => $1)
250
250
  self.creating_system_id = UID.new(:value => $2)
251
251
  self.version_tree_id = VersionTreeID.new(:value => $3)
252
+ @root = @oid
253
+ @extension = @creating_system_id.value + '::' + @version_tree_id.value
252
254
  else
253
255
  raise ArgumentError, 'invalid format'
254
256
  end
@@ -297,12 +299,14 @@ module OpenEHR
297
299
  end
298
300
 
299
301
  def path=(path)
300
- raise ArgumentError if path.nil? or path.empty?
302
+ raise ArgumentError if !path.nil? and path.empty?
301
303
  @path = path
302
304
  end
303
305
 
304
306
  def as_uri
305
- 'ehr://' + @id.value + '/' + @path
307
+ uri = 'ehr://' + @id.value
308
+ uri += '/' + @path unless @path.nil?
309
+ uri
306
310
  end
307
311
  end
308
312
 
@@ -395,16 +399,41 @@ module OpenEHR
395
399
  end
396
400
 
397
401
  class UUID < UID
402
+ FORMAT = /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/
398
403
 
404
+ def value=(value)
405
+ unless value.is_a?(String) && value =~ FORMAT
406
+ raise ArgumentError, 'invalid UUID form'
407
+ end
408
+ super
409
+ end
399
410
  end
400
411
 
401
- class InternetID <UID
402
-
412
+ class InternetID < UID
413
+ # RFC 1034 domain-name form: dot-separated labels, each
414
+ # alphanumeric (with internal hyphens allowed).
415
+ FORMAT = /\A[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+\z/
416
+
417
+ def value=(value)
418
+ unless value.is_a?(String) && value =~ FORMAT
419
+ raise ArgumentError, 'invalid internet id form'
420
+ end
421
+ super
422
+ end
403
423
  end
404
424
 
405
- class IsoOID <UID
425
+ class IsoOID < UID
426
+ # ISO/IEC 8824 OID form: a dot-separated sequence of
427
+ # non-negative integers, e.g. "1.2.840.113619.2.1".
428
+ FORMAT = /\A\d+(\.\d+)+\z/
406
429
 
407
- end
430
+ def value=(value)
431
+ unless value.is_a?(String) && value =~ FORMAT
432
+ raise ArgumentError, 'invalid ISO OID form'
433
+ end
434
+ super
435
+ end
436
+ end
408
437
  end # of Identification
409
438
  end # of Support
410
439
  end # of RM
@@ -2,7 +2,39 @@ module OpenEHR
2
2
  module RM
3
3
  module Support
4
4
  module Measurement
5
+ # A minimal UCUM-lite syntactic checker, not a full UCUM
6
+ # parser/conversion table: it validates that a units string is
7
+ # *shaped* like a UCUM unit (allowed characters, balanced
8
+ # brackets) and treats "equivalent" as exact string equality of
9
+ # two syntactically-valid units strings. It does not perform
10
+ # unit conversion (e.g. "kg" and "1000g" are not recognized as
11
+ # equivalent).
5
12
  class MeasurementService
13
+ UCUM_LITE_PATTERN = /\A[A-Za-z0-9%\/.*\[\]{}'_-]+\z/
14
+
15
+ def self.is_valid_units_string?(units)
16
+ return false unless units.is_a?(String) && !units.empty?
17
+ return false unless units =~ UCUM_LITE_PATTERN
18
+
19
+ balanced_brackets?(units, '[', ']') && balanced_brackets?(units, '{', '}')
20
+ end
21
+
22
+ def self.units_equivalent?(units1, units2)
23
+ return false unless is_valid_units_string?(units1) && is_valid_units_string?(units2)
24
+
25
+ units1 == units2
26
+ end
27
+
28
+ def self.balanced_brackets?(str, open, close)
29
+ depth = 0
30
+ str.each_char do |c|
31
+ depth += 1 if c == open
32
+ depth -= 1 if c == close
33
+ return false if depth.negative?
34
+ end
35
+ depth.zero?
36
+ end
37
+ private_class_method :balanced_brackets?
6
38
  end
7
39
  module ExternalEnvironmentAccess
8
40
  def eea_terminology_svc
@@ -0,0 +1,90 @@
1
+ require 'set'
2
+ require 'active_support/core_ext/string/inflections'
3
+
4
+ module OpenEHR
5
+ module RM
6
+ module TypeName
7
+ module_function
8
+
9
+ def type_name_of(class_or_instance)
10
+ class_of(class_or_instance).name.split('::').last.underscore.upcase
11
+ end
12
+
13
+ def class_for(type_name)
14
+ registry[normalize(type_name)]
15
+ end
16
+
17
+ def subtype_of?(class_or_instance, type_name)
18
+ expected = normalize(type_name)
19
+ ancestor = class_of(class_or_instance)
20
+ while ancestor
21
+ return true if ancestor.name && type_name_of(ancestor) == expected
22
+
23
+ ancestor = ancestor.superclass
24
+ end
25
+ false
26
+ end
27
+
28
+ # Accepts a spec type name in any of its forms seen across this
29
+ # codebase - UPPER_SNAKE ('DV_QUANTITY', from the ADL/OPT
30
+ # parsers), CamelCase ('DvQuantity', hardcoded by some
31
+ # CDomainType constructors) or lower_snake - and normalises to
32
+ # the UPPER_SNAKE form used as the registry key.
33
+ def normalize(type_name)
34
+ type_name.to_s.underscore.upcase
35
+ end
36
+ private_class_method :normalize
37
+
38
+ def class_of(class_or_instance)
39
+ class_or_instance.is_a?(Module) ? class_or_instance : class_or_instance.class
40
+ end
41
+ private_class_method :class_of
42
+
43
+ def registry
44
+ @registry ||= build_registry
45
+ end
46
+ private_class_method :registry
47
+
48
+ # Skips Factory (OpenEHR::RM::Factory and the *Factory helper
49
+ # classes in factory.rb) - those are builders, not RM types, and
50
+ # would otherwise shadow nothing but add registry noise.
51
+ def build_registry
52
+ classes = {}
53
+ visit(OpenEHR::RM, Set.new.compare_by_identity) do |klass|
54
+ next if klass.name.end_with?('Factory')
55
+
56
+ classes[type_name_of(klass)] ||= klass
57
+ end
58
+ classes
59
+ end
60
+ private_class_method :build_registry
61
+
62
+ def visit(mod, seen, &block)
63
+ mod.constants(false).each do |const_name|
64
+ const = mod.const_get(const_name)
65
+ next unless const.is_a?(Module)
66
+ next if seen.include?(const)
67
+
68
+ seen << const
69
+ yield const if const.is_a?(Class)
70
+ visit(const, seen, &block)
71
+ end
72
+ end
73
+ private_class_method :visit
74
+ end
75
+
76
+ class << self
77
+ def type_name_of(class_or_instance)
78
+ TypeName.type_name_of(class_or_instance)
79
+ end
80
+
81
+ def class_for(type_name)
82
+ TypeName.class_for(type_name)
83
+ end
84
+
85
+ def subtype_of?(class_or_instance, type_name)
86
+ TypeName.subtype_of?(class_or_instance, type_name)
87
+ end
88
+ end
89
+ end
90
+ end
data/lib/openehr/rm.rb CHANGED
@@ -44,3 +44,6 @@ require_relative 'rm/integration'
44
44
 
45
45
  #factory and builder
46
46
  require_relative 'rm/factory'
47
+
48
+ #RM type name <-> class lookup
49
+ require_relative 'rm/type_name'