class StructureMatch

StructureMatch encapsulates all the logic needed perform deep binding and scoring of a JSON data structure.

Public Class Methods

new(matcher) click to toggle source

StructureMatchers are initialized by passing in an example nested hash that maps out what StructureMatch should dig through, bind matching values, and how to score the matches it found. As an example:

StructureMatch.new("foo" => "bar",
                   "numbermatch" => 5,
                   "regexmatch" => {
                     "__sm_leaf" => true,
                     "op"=> "regex",
                     "match" => "foo(bar)"},
                   "ormatch" => {
                     "__sm_leaf" => true,
                     "op" => "or",
                     "match" => [
                       {
                         "op" => ">",
                         "match" => 7
                       },
                       {
                         "op" => "<",
                         "match" => 10}]})

will create a matcher that perfectly matches the following JSON:

{ "foo": "bar",
  "numbermatch": 5,
  "regexmatch": "foobar",
  "ormatch": 8
}

This match will be assigned a score of 5.

# File lib/structurematch.rb, line 127
def initialize(matcher)
  raise "#{matcher.inspect} must be a Hash" unless matcher.kind_of?(Hash)
  @matchers = Hash.new
  matcher.each do |k,v|
    raise "#{k} must be a String" unless k.kind_of?(String)
    key = k.dup.freeze
    @matchers[key] = case
                     when v.kind_of?(TrueClass) ||
                         v.kind_of?(FalseClass) ||
                         v.kind_of?(NilClass)   ||
                         v.kind_of?(Numeric)
                       Comparator.new("op" => "==", "match" => v)
                     when v.kind_of?(String) then Comparator.new("op" => "==", "match" => v.dup.freeze)
                     when v.kind_of?(Array) then Comparator.new("op" => "member", "match" => v.dup.freeze)
                     when v.kind_of?(Hash) then !!v["__sm_leaf"] ? Comparator.new(v) : StructureMatch.new(v)
                     else
                       raise "Cannot handle node type #{v.inspect}"
                     end
  end
end

Public Instance Methods

bind(val) click to toggle source

Takes a (possibly nested) hash that is a result of parsing JSON and matches what it can, assigning a score in the process.

Returns a two-entry array in the form of [binds,score]:

binds

The nested hash corresponding to the matched values from val.

score

The score assigned to this match.

# File lib/structurematch.rb, line 154
def bind(val)
  raise "Must pass a Hash to StructureMatch.bind" unless val.kind_of?(Hash)
  score = 0
  binds = Hash.new
  @matchers.each do |k,v|
    offset = 0
    res = nil
    case
    when !val.has_key?(k) then offset = -1
    when v.kind_of?(Comparator)
      offset,res = v.test(val[k])
      binds[k] = res if offset > 0
    when v.kind_of?(StructureMatch)
      res,offset = v.bind(val[k])
      binds[k] = res unless res.empty?
    else
      raise "StructureMatch.bind: No idea how to handle #{v.class.name}: #{v.inspect}"
    end
    score += offset
  end
  [binds,score]
end
flatbind(val, sep='.') click to toggle source

As bind, but transforms the nested hash into a flat hash with nested keys joined by sep.

# File lib/structurematch.rb, line 179
def flatbind(val, sep='.')
  binds,score = bind(val)
  target = Hash.new
  prefix = ''
  _flatbind = lambda do |vals,p|
    vals.each do |k,v|
      key = p.empty? ? k : "#{p}#{sep}#{k.to_s}"
      if v.kind_of?(Hash)
        _flatbind.call(v,key)
      else
        target[key] = v
      end
    end
  end
  _flatbind.call(binds,"")
  [target,score]
end
score(val) click to toggle source

Runs bind on val and returns just the score component.

# File lib/structurematch.rb, line 198
def score(val)
  bind(val)[1]
end