module FHIRPath

Public Class Methods

atom(token) click to toggle source

This method converts a token within an expression to a native number (if applicable) otherwise it considers it to be a symbol.

# File lib/fhir_models/fhirpath/parse.rb, line 89
def self.atom(token)
  # check if it is a number
  value = token
  begin
    value = Float(token)
  rescue
    value = token
    value = token.to_sym if @@reserved.include?(token)
    value = true if token == 'true'
    value = false if token == 'false'
  end
  value
end
build_tree(tokens) click to toggle source

This method builds an Abstract Syntax Tree (AST) from a flat list of tokens

# File lib/fhir_models/fhirpath/parse.rb, line 64
def self.build_tree(tokens)
  return if tokens.empty?
  tree = []
  until tokens.empty?
    token = tokens.delete_at(0)
    if '(' == token # sub expression
      tree << FHIRPath::Expression.new(build_tree(tokens))
    elsif ')' == token
      return tree
    elsif '.' != token
      tree << atom(token)
    end
  end
  # post-processing
  tree.each_with_index do |t, index|
    if t == :extension # 'extension' can be a path or a function call (if followed by a block)
      next_token = tree[index + 1]
      tree[index] = 'extension' if next_token.nil? || !next_token.is_a?(FHIRPath::Expression)
    end
  end
  tree
end
clean_index(tree, index) click to toggle source
# File lib/fhir_models/fhirpath/evaluate.rb, line 65
def self.clean_index(tree, index)
  tree[index] = nil unless index.nil?
end
compute(tree, data) click to toggle source

evaluate a parsed expression given some context data

# File lib/fhir_models/fhirpath/evaluate.rb, line 70
def self.compute(tree, data)
  tree = tree.tree if tree.is_a?(FHIRPath::Expression)
  # --------------- OPERATOR PRECEDENCE ------------------
  # 01 . (path/function invocation)
  # 02 [] (indexer)
  # 03 unary + and -
  # 04: *, /, div, mod
  # 05: +, -,
  # 06: |
  # 07: >, <, >=, <=
  # 08: is, as
  # 09: =, ~, !=, !~
  # 10: in, contains
  # 11: and
  # 12: xor, or
  # 13: implies

  # evaluate all the data at this level
  functions = [:where, :select, :extension, :children, :first, :last, :tail]
  size = -1
  substitutions = 1
  while tree.length != size || substitutions > 0
    substitutions = 0
    FHIR.logger.debug "DATA: #{tree}"
    previous_node = nil
    previous_index = nil
    size = tree.length
    tree.each_with_index do |node, index|
      if node.is_a?(String) && !(node.start_with?("'") && node.end_with?("'"))
        array_index = nil
        if node.include?('[') && node.end_with?(']')
          array_index = node[node.index('[')..-1].gsub(/\[|\]/, '')
          t = get(array_index, data)
          t = array_index.to_i if t.nil? || t == :null
          array_index = t
          node = node[0..node.index('[') - 1]
        end
        if previous_node.is_a?(Hash) || previous_node.is_a?(Array)
          tree[index] = get(node, previous_node)
          clean_index(tree, previous_index)
        elsif !previous_node.is_a?(FHIRPath::Expression)
          tree[index] = get(node, data)
        end
        if array_index && tree[index].is_a?(Array)
          tree[index] = tree[index][array_index]
        end
        FHIR.logger.debug "V===> #{tree}"
      elsif node.is_a?(Symbol) && functions.include?(node)
        previous_node = compute(previous_node, data) if previous_node.is_a?(FHIRPath::Expression)
        case node
        when :where
          # the previous node should be data (as Array or Hash)
          # the next node should be a block or subexpression (as FHIRPath::Expression)
          block = tree[index + 1]
          if block.is_a?(FHIRPath::Expression)
            tree[index + 1] = nil
          else
            raise 'Where function requires a block.'
          end
          previous_node = [] if previous_node == :null
          if previous_node.is_a?(Array)
            previous_node.keep_if do |item|
              sub = compute(block.clone, item)
              convert_to_boolean(sub)
            end
            tree[index] = previous_node
            clean_index(tree, previous_index)
          elsif previous_node.is_a?(Hash)
            sub = compute(block, previous_node)
            tree[index] = convert_to_boolean(sub) ? previous_node : {}
            clean_index(tree, previous_index)
          else
            raise "Where function not applicable to #{previous_node.class}: #{previous_node}"
          end
          break
        when :select
          # select is equivalent to ruby Array.map!
          # the previous node should be data (as Array or Hash)
          # the next node should be a block or subexpression (as FHIRPath::Expression)
          block = tree[index + 1]
          if block.is_a?(FHIRPath::Expression)
            tree[index + 1] = nil
          else
            raise 'Select function requires a block.'
          end
          previous_node = [] if previous_node == :null
          if previous_node.is_a?(Array)
            previous_node.map! do |item|
              compute(block.clone, item)
            end
            tree[index] = previous_node
            clean_index(tree, previous_index)
          elsif previous_node.is_a?(Hash)
            tree[index] = compute(block, previous_node)
            clean_index(tree, previous_index)
          else
            raise "Select function not applicable to #{previous_node.class}: #{previous_node}"
          end
          break
        when :extension
          # the previous node should be a data (as Hash)
          # the next node optionally is a block or subexpression (as FHIRPath::Expression)
          block = tree[index + 1]
          if block.is_a?(FHIRPath::Expression)
            tree[index + 1] = nil
          else
            raise 'Extension function requires a block.'
          end
          if previous_node.is_a?(Hash)
            FHIR.logger.debug 'Evaling Extension Block....'
            exts = data['extension']
            if exts.is_a?(Array)
              url = nil
              begin
                url = block.tree.first.gsub(/\'|\"/, '')
              rescue
                raise 'Extension function requires a single URL as String.'
              end
              ext = exts.select { |x| x['url'] == url }.first
              tree[index] = ext
              clean_index(tree, previous_index)
            else
              raise "Extension function not applicable to #{exts.class}: #{exts}"
            end
          else
            raise "Extension not applicable to #{previous_node.class}: #{previous_node}"
          end
          break
        when :children
          # if there is a previous node, it should be data (as Hash)
          # otherwise, use the context as data
          if previous_node.is_a?(Hash)
            tree[index] = previous_node.values
            clean_index(tree, previous_index)
            substitutions += 1
          elsif data.is_a?(Hash)
            tree[index] = data.values
            substitutions += 1
          else
            raise "Children not applicable to #{previous_node.class}: #{previous_node}"
          end
          break
        when :first
          # the previous node should be an Array of length > 1
          if previous_node.is_a?(Array)
            tree[index] = previous_node.first
            clean_index(tree, previous_index)
          else
            raise "First function is not applicable to #{previous_node.class}: #{previous_node}"
          end
        when :last
          # the previous node should be an Array of length > 1
          if previous_node.is_a?(Array)
            tree[index] = previous_node.last
            clean_index(tree, previous_index)
          else
            raise "Last function is not applicable to #{previous_node.class}: #{previous_node}"
          end
        when :tail
          # the previous node should be an Array of length > 1
          if previous_node.is_a?(Array)
            tree[index] = previous_node.last(previous_node.length - 1)
            clean_index(tree, previous_index)
          else
            raise "Tail function is not applicable to #{previous_node.class}: #{previous_node}"
          end
        end
        FHIR.logger.debug "F===> #{tree}"
      end
      previous_index = index
      previous_node = tree[index]
    end
    FHIR.logger.debug '---------------------------------------------------'
    tree.compact!
  end
  tree.each_with_index do |node, index|
    tree[index] = node[1..-2] if node.is_a?(String) && node.start_with?("'") && node.end_with?("'")
  end
  FHIR.logger.debug "DATA: #{tree}"

  # evaluate all the functions at this level
  functions = [:all, :not, :empty, :exists, :startsWith, :substring, :contains, :in, :distinct, :toInteger, :count, :length]
  size = -1
  while tree.length != size
    FHIR.logger.debug "FUNC: #{tree}"
    previous_node = data
    previous_index = nil
    size = tree.length
    tree.each_with_index do |node, index|
      if node.is_a?(Symbol) && functions.include?(node)
        previous_node = compute(previous_node, data) if previous_node.is_a?(FHIRPath::Expression)
        case node
        when :all
          if previous_node.is_a?(Array)
            result = true
            previous_node.each { |item| result = (result && convert_to_boolean(item)) }
            tree[index] = result
          else
            tree[index] = convert_to_boolean(previous_node)
          end
          clean_index(tree, previous_index)
        when :not
          tree[index] = !convert_to_boolean(previous_node)
          clean_index(tree, previous_index)
        when :count
          tree[index] = 0
          tree[index] = 1 unless previous_node == :null || previous_node.nil?
          tree[index] = previous_node.length if previous_node.is_a?(Array)
          clean_index(tree, previous_index)
        when :length
          if previous_node == :null || previous_node.nil?
            tree[index] = 0
          else
            tree[index] = previous_node.to_s.length
          end
          clean_index(tree, previous_index)
        when :empty
          tree[index] = (previous_node == :null || previous_node.empty? rescue previous_node.nil?)
          clean_index(tree, previous_index)
        when :exists
          tree[index] = !previous_node.nil? && previous_node != :null && previous_node != []
          clean_index(tree, previous_index)
        when :distinct
          tree[index] = (previous_node.uniq rescue previous_node)
          clean_index(tree, previous_index)
        when :startsWith
          # the previous node should be a data (as String)
          # the next node should be a block or subexpression (as FHIRPath::Expression)
          block = tree[index + 1]
          if block.is_a?(FHIRPath::Expression)
            tree[index + 1] = nil
          else
            raise 'StartsWith function requires a block.'
          end
          if previous_node.is_a?(String)
            FHIR.logger.debug 'Evaling StartsWith Block....'
            prefix = compute(block, data)
            tree[index] = previous_node.start_with?(prefix) rescue false
            clean_index(tree, previous_index)
          else
            raise "StartsWith function not applicable to #{previous_node.class}: #{previous_node}"
          end
          break
        when :substring
          # the previous node should be a data (as String)
          # the next node should be a block or subexpression (as FHIRPath::Expression)
          block = tree[index + 1]
          if block.is_a?(FHIRPath::Expression)
            tree[index + 1] = nil
          else
            raise 'Substring function requires a block.'
          end
          if previous_node.is_a?(String)
            args = block.tree.first
            start = 0
            length = previous_node.length
            if args.is_a?(String) && args.include?(',')
              args = args.split(',')
              start = args.first.to_i
              length = args.last.to_i - 1
            else
              FHIR.logger.debug 'Evaling Substring Block....'
              start = compute(block, data)
              length = previous_node.length - start
            end
            tree[index] = previous_node[start..(start + length)]
            clean_index(tree, previous_index)
          else
            raise "Substring function not applicable to #{previous_node.class}: #{previous_node}"
          end
          break
        when :contains
          # the previous node should be a data (as String)
          # the next node should be a block or subexpression (as FHIRPath::Expression)
          block = tree[index + 1]
          if block.is_a?(FHIRPath::Expression)
            tree[index + 1] = nil
          else
            raise 'Contains function requires a block.'
          end
          if previous_node.is_a?(String)
            FHIR.logger.debug 'Evaling Contains Block....'
            substring = compute(block, data)
            tree[index] = previous_node.include?(substring) rescue false
            clean_index(tree, previous_index)
          else
            raise "Contains function not applicable to #{previous_node.class}: #{previous_node}"
          end
          break
        when :in
          # the previous node should be a data (as String, Number, or Boolean)
          # the next node should an Array (possibly as a block or subexpression/FHIRPath::Expression)
          block = tree[index + 1]
          if block.is_a?(FHIRPath::Expression)
            FHIR.logger.debug 'Evaling In Block....'
            tree[index + 1] = compute(block, data)
          end
          array = tree[index + 1]
          if array.is_a?(Array)
            tree[index + 1] = nil
          else
            raise 'In function requires an array.'
          end
          if previous_node.is_a?(String) || previous_node == true || previous_node == false || previous_node.is_a?(Numeric)
            tree[index] = array.include?(previous_node) rescue false
            clean_index(tree, previous_index)
          else
            raise "In function not applicable to #{previous_node.class}: #{previous_node}"
          end
          break
        when :toInteger
          # the previous node should be a data (as String, Integer, Boolean)
          if previous_node.is_a?(String)
            tree[index] = previous_node.to_i rescue 0
          elsif previous_node.is_a?(Numeric)
            tree[index] = previous_node.to_i
          else
            tree[index] = 0
            tree[index] = 1 if convert_to_boolean(previous_node)
          end
          clean_index(tree, previous_index)
          break
        else
          raise "Function not implemented: #{node}"
        end
      end
      previous_index = index
      previous_node = node
    end
    tree.compact!
  end

  # evaluate all mult/div
  functions = [:"/", :"*"]
  size = -1
  while tree.length != size
    FHIR.logger.debug "MATH: #{tree}"
    previous_node = nil
    previous_index = nil
    size = tree.length
    tree.each_with_index do |node, index|
      if node.is_a?(Symbol) && functions.include?(node)
        previous_node = compute(previous_node, data) if previous_node.is_a?(FHIRPath::Expression)
        tree[index + 1] = compute(tree[index + 1], data) if tree[index + 1].is_a?(FHIRPath::Expression)
        left = previous_node
        right = tree[index + 1]
        case node
        when :"/"
          tree[index] = (left / right)
        when :"*"
          tree[index] = (left * right)
        end
        tree[previous_index] = nil
        tree[index + 1] = nil
        break
      end
      previous_index = index
      previous_node = node
    end
    tree.compact!
  end
  FHIR.logger.debug "MATH: #{tree}"

  # evaluate all add/sub
  functions = [:"+", :"-"]
  size = -1
  while tree.length != size
    FHIR.logger.debug "MATH: #{tree}"
    previous_node = nil
    previous_index = nil
    size = tree.length
    tree.each_with_index do |node, index|
      if node.is_a?(Symbol) && functions.include?(node)
        previous_node = compute(previous_node, data) if previous_node.is_a?(FHIRPath::Expression)
        tree[index + 1] = compute(tree[index + 1], data) if tree[index + 1].is_a?(FHIRPath::Expression)
        left = previous_node
        right = tree[index + 1]
        case node
        when :"+"
          tree[index] = (left + right)
        when :"-"
          tree[index] = (left - right)
        end
        tree[previous_index] = nil
        tree[index + 1] = nil
        break
      end
      previous_index = index
      previous_node = node
    end
    tree.compact!
  end
  FHIR.logger.debug "MATH: #{tree}"

  # evaluate all equality tests
  functions = [:"=", :"!=", :"<=", :">=", :"<", :">"]
  size = -1
  while tree.length != size
    FHIR.logger.debug "EQ: #{tree}"
    previous_node = nil
    previous_index = nil
    size = tree.length
    tree.each_with_index do |node, index|
      if node.is_a?(Symbol) && functions.include?(node)
        previous_node = compute(previous_node, data) if previous_node.is_a?(FHIRPath::Expression)
        tree[index + 1] = compute(tree[index + 1], data) if tree[index + 1].is_a?(FHIRPath::Expression)
        left = previous_node
        right = tree[index + 1]
        case node
        when :"="
          tree[index] = (left == right)
        when :"!="
          tree[index] = (left != right)
        when :"<="
          tree[index] = (left <= right)
        when :">="
          tree[index] = (left >= right)
        when :"<"
          tree[index] = (left < right)
        when :">"
          tree[index] = (left > right)
        else
          raise "Equality operator not implemented: #{node}"
        end
        tree[previous_index] = nil
        tree[index + 1] = nil
        break
      end
      previous_index = index
      previous_node = node
    end
    tree.compact!
  end
  FHIR.logger.debug "EQ: #{tree}"

  # evaluate all logical tests
  functions = [:and, :or, :xor]
  size = -1
  while tree.length != size
    FHIR.logger.debug "LOGIC: #{tree}"
    previous_node = nil
    previous_index = nil
    size = tree.length
    tree.each_with_index do |node, index|
      if node.is_a?(Symbol) && functions.include?(node)
        previous_node = compute(previous_node, data) if previous_node.is_a?(FHIRPath::Expression)
        tree[index + 1] = compute(tree[index + 1], data) if tree[index + 1].is_a?(FHIRPath::Expression)
        left = convert_to_boolean(previous_node)
        right = convert_to_boolean(tree[index + 1])
        case node
        when :and
          tree[index] = (left && right)
        when :or
          tree[index] = (left || right)
        when :xor
          tree[index] = (left ^ right)
        else
          raise "Logical operator not implemented: #{node}"
        end
        tree[previous_index] = nil
        tree[index + 1] = nil
        break
      end
      previous_index = index
      previous_node = node
    end
    tree.compact!
  end
  FHIR.logger.debug "LOGIC: #{tree}"

  functions = [:implies]
  size = -1
  while tree.length != size
    FHIR.logger.debug "IMPLIES: #{tree}"
    previous_node = nil
    previous_index = nil
    size = tree.length
    tree.each_with_index do |node, index|
      if node.is_a?(Symbol) && functions.include?(node)
        previous_node = compute(previous_node, data) if previous_node.is_a?(FHIRPath::Expression)
        tree[index + 1] = compute(tree[index + 1], data) if tree[index + 1].is_a?(FHIRPath::Expression)
        case node
        when :implies
          tree[index] = false
          exists = !previous_node.nil? && previous_node != :null
          implication = convert_to_boolean(tree[index + 1])
          tree[index] = true if exists && (implication || tree[index + 1] == false)
        else
          raise "Logical operator not implemented: #{node}"
        end
        tree[previous_index] = nil
        tree[index + 1] = nil
        break
      end
      previous_index = index
      previous_node = node
    end
    tree.compact!
  end
  FHIR.logger.debug "IMPLIES: #{tree}"

  # check for symbols
  tree.each do |node|
    raise "Unhandled reserved symbol: #{node}" if node.is_a?(Symbol)
  end

  FHIR.logger.debug "OUT: #{tree}"

  tree.map! do |out|
    while out.is_a?(FHIRPath::Expression)
      out = compute(out, data)
    end
    out
  end

  FHIR.logger.debug "RETURN: #{tree.first}"
  tree.first
end
convert_to_boolean(value) click to toggle source

Convert nils and empty Arrays to false Everything else is true.

# File lib/fhir_models/fhirpath/evaluate.rb, line 56
def self.convert_to_boolean(value)
  return false if value.nil?
  return false if value.is_a?(Array) && value.empty?
  return false if value.is_a?(Hash) && value.empty?
  return false if value == :null
  return false if value == false
  true
end
evaluate(expression, hash, parent = nil) click to toggle source

This is the entry point to using the FHIRPath class

# File lib/fhir_models/fhirpath/evaluate.rb, line 6
def self.evaluate(expression, hash, parent = nil)
  @@context = hash
  @@parent = parent
  tree = FHIRPath.parse(expression)
  FHIR.logger.debug "TREE: #{tree}"
  compute(tree, hash)
end
get(key, hash) click to toggle source

Get a value from a hash, with some special handling of self references

# File lib/fhir_models/fhirpath/evaluate.rb, line 16
def self.get(key, hash)
  return @@context if ['$context', '$resource'].include?(key)
  return @@parent if key == '$parent'
  return 'http://unitsofmeasure.org' if key == '%ucum'
  return 'http://snomed.info/sct' if key == '%sct'
  return 'http://loinc.org' if key == '%loinc'
  return key.gsub!(/\A\'|\'\Z/, '') if key.start_with?("'") && key.end_with?("'")
  key.gsub!(/\A"|"\Z/, '') # remove quotes around path if they exist
  if hash.is_a?(Array)
    response = []
    hash.each do |e|
      next unless e.is_a?(Hash)
      item = e[key]
      if item.is_a?(Array)
        item.each { |i| response << i }
      else
        response << item
      end
    end
    return response
  end
  return :null unless hash.is_a?(Hash)
  return hash if hash['resourceType'] == key
  val = hash[key]
  if val.nil?
    # this block is a dangerous hack to get fields of multiple data types
    # e.g. 'value' instead of 'valueQuantity', or 'onset' instead of 'onsetDateTime' or 'onsetPeriod'
    nkey = hash.keys.select { |x| x.start_with?(key) }.first
    unless nkey.nil?
      tail = nkey.gsub(key, '')
      val = hash[nkey] if tail[0] == tail[0].capitalize
    end
  end
  val = :null if val.nil?
  val = "'#{val}'" if val.is_a?(String) && !(val.start_with?("'") && val.end_with?("'"))
  val
end
parse(expression) click to toggle source
# File lib/fhir_models/fhirpath/parse.rb, line 4
def self.parse(expression)
  build_tree(tokenize(expression))
end
reassemble_strings(tokens) click to toggle source
# File lib/fhir_models/fhirpath/parse.rb, line 44
def self.reassemble_strings(tokens)
  tokens.each_with_index do |token, index|
    next unless token.is_a?(String)
    e_index = nil
    if token.start_with?('"') && !token.end_with?('"')
      e_index = tokens[index..-1].index { |t| t.end_with?('"') }
    elsif token.start_with?("'") && !token.end_with?("'")
      e_index = tokens[index..-1].index { |t| t.end_with?("'") }
    end
    next unless e_index
    i = index + 1
    while i <= index + e_index
      tokens[index] += tokens[i]
      tokens[i] = ''
      i += 1
    end
  end
end
tokenize(expression) click to toggle source

This method tokenizes the expression into a flat array of tokens

# File lib/fhir_models/fhirpath/parse.rb, line 9
def self.tokenize(expression)
  raw_tokens = expression.gsub('()', '').split(%r{(\(|\)|\s|>=|<=|>|<|=|!=|\+|-|\/|\*)})
  # recreate strings if they were split
  size = nil
  while raw_tokens.include?("'") && size != raw_tokens.length
    index = raw_tokens.index("'")
    e_index = raw_tokens[(index + 1)..raw_tokens.length].index("'")
    raw_tokens[index] = raw_tokens[index..(index + e_index + 1)].join
    ((index + 1)..(index + e_index + 1)).each { |i| raw_tokens[i] = nil }
    raw_tokens.compact!
    size = raw_tokens.length
  end
  # reassemble strings that might have been inadvertently split that start and end with double-quotes
  reassemble_strings(raw_tokens)
  tokens = []
  raw_tokens.each do |token|
    # split a path unless it is quoted
    if token.include?('.') && !(token.start_with?("'") && token.end_with?("'"))
      token.split('.').each { |t| tokens << t }
    # split arrays and replace with array
    elsif token.include?('|')
      array = []
      token.split('|').each { |t| array << t.delete('\'') }
      tokens << array
    else
      tokens << token
    end
  end
  # we may need to reassemble quoted strings again
  reassemble_strings(tokens)
  tokens.delete_if { |token| (token.length.zero? || (token.is_a?(String) && token.match(/\S/).nil?)) }
  FHIR.logger.debug "TOKENS: #{tokens}"
  tokens
end