Module: OpenEHR::AQL::PathEvaluator

Defined in:
lib/openehr/aql/engine/path_evaluator.rb

Overview

Evaluates a columnExpr/terminal Model node against a Binding.

SELECT paths ("o/data/.../value/magnitude", "c/name/value") walk one segment at a time: as long as the current value is Pathable AND the segment names one of its declared path_attributes (the content-structure attributes PATHABLE navigation was built for - see lib/openehr/rm/common/archetyped.rb), the hop goes through PATHABLE#items_at_path (reusing its predicate matching for node/archetype predicates). Everything else - RM metadata that isn't part of that DSL (name, archetype_details, composer, ...) and genuinely non-Pathable values (a DV_QUANTITY's magnitude, a DV_TEXT's value) - is a plain attribute read via a whitelisted public_send, never an arbitrary send driven by query text. A predicate directly on an identifiedPath's own variable (e.g. "c/..."), parameters and function calls are added by later engine milestones.

Defined Under Namespace

Classes: EhrIdValue

Constant Summary collapse

ALLOWED_TERMINAL_HOPS =

Attribute hops not reachable through the path_attribute DSL. Expand only when a real query needs another one - see the project's "no arbitrary send" rule in the class comment above.

%w[magnitude name value].freeze

Class Method Summary collapse

Class Method Details

.declared_path_attribute?(current, segment) ⇒ Boolean

Returns:

  • (Boolean)


81
82
83
84
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 81

def declared_path_attribute?(current, segment)
  current.is_a?(OpenEHR::RM::Common::Archetyped::Pathable) &&
    current.class.path_attributes.map(&:to_s).include?(segment.attribute)
end

.evaluate(expression, binding) ⇒ Object



33
34
35
36
37
38
39
40
41
42
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 33

def evaluate(expression, binding)
  case expression
  when Model::IdentifiedPath
    evaluate_identified_path(expression, binding)
  when Model::Literal
    expression.value
  else
    raise ExecutionError, "cannot evaluate a #{expression.class} yet"
  end
end

.evaluate_identified_path(path, binding) ⇒ Object

Raises:



44
45
46
47
48
49
50
51
52
53
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 44

def evaluate_identified_path(path, binding)
  raise ExecutionError, 'predicates on a SELECT variable are not yet supported' if path.predicate

  value = binding[path.variable]
  return value unless path.path

  path.path.segments.reduce(value) do |current, segment|
    current.nil? ? nil : navigate(current, segment)
  end
end


55
56
57
58
59
60
61
62
63
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 55

def navigate(current, segment)
  return navigate_ehr_record(current, segment) if current.is_a?(Dataset::EHRRecord)

  if declared_path_attribute?(current, segment)
    navigate_pathable(current, segment)
  else
    navigate_terminal(current, segment)
  end
end

"e/ehr_id/value" and "e/ehr_status/..." always resolve (from Dataset's own record shape); any other "e/..." path falls through to whatever full RM::EHR::EHR the record carries, if any, else resolves to nil rather than erroring - AQL's path-absent-means-null semantics again, not a missing feature.

Raises:



70
71
72
73
74
75
76
77
78
79
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 70

def navigate_ehr_record(record, segment)
  raise ExecutionError, "path predicates on an EHR root are not supported (#{segment.attribute})" if segment.predicate

  case segment.attribute
  when 'ehr_id' then EhrIdValue.new(record.ehr_id)
  when 'ehr_status' then record.ehr_status
  else
    record.ehr.nil? ? nil : navigate(record.ehr, segment)
  end
end

A path segment matching nothing is a legitimate "no value here" (AQL's path-absent-means-null semantics, same convention as PATHABLE#item_at_path itself), not an error - only an ambiguous match (more than one item) is.



90
91
92
93
94
95
96
97
98
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 90

def navigate_pathable(current, segment)
  matches = current.items_at_path(rm_path_for(segment))
  case matches.size
  when 0 then nil
  when 1 then matches.first
  else raise ExecutionError, "path segment #{segment.to_s.inspect} matched #{matches.size} items " \
                              '(fan-out SELECT paths are not yet supported)'
  end
end

Raises:



100
101
102
103
104
105
106
107
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 100

def navigate_terminal(current, segment)
  raise ExecutionError, "path predicates on a non-Pathable value are not supported (#{segment.attribute})" if segment.predicate
  unless ALLOWED_TERMINAL_HOPS.include?(segment.attribute)
    raise ExecutionError, "unsupported path attribute #{segment.attribute.inspect} on a #{current.class}"
  end

  current.public_send(segment.attribute)
end

.node_predicate_path_text(predicate) ⇒ Object



126
127
128
129
130
131
132
133
134
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 126

def node_predicate_path_text(predicate)
  return predicate.code unless predicate.value

  unless predicate.value.is_a?(String)
    raise ExecutionError, "cannot evaluate a #{predicate.value.class} node predicate value yet"
  end

  "#{predicate.code}, '#{predicate.value}'"
end

.predicate_path_text(predicate) ⇒ Object



115
116
117
118
119
120
121
122
123
124
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 115

def predicate_path_text(predicate)
  case predicate
  when Model::ArchetypePredicate
    predicate.archetype_id
  when Model::NodePredicate
    node_predicate_path_text(predicate)
  else
    raise ExecutionError, "cannot evaluate a #{predicate.class} path predicate yet"
  end
end

.rm_path_for(segment) ⇒ Object



109
110
111
112
113
# File 'lib/openehr/aql/engine/path_evaluator.rb', line 109

def rm_path_for(segment)
  return "/#{segment.attribute}" unless segment.predicate

  "/#{segment.attribute}[#{predicate_path_text(segment.predicate)}]"
end