structurematch 0.1.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.
- checksums.yaml +7 -0
- data/lib/structurematch.rb +139 -0
- metadata +44 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c071ed38259f2735cad5b428fe364fa186e58ffb
|
4
|
+
data.tar.gz: 569278eab25de5512e30d557a409830e4544568b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 210676dc6826c5bc18356469db5931ab733562829d5a64c4e1d61c76eb787d0a625cb6e83eda394c8813e5d73bdce6fd6518779d1f94810789d4d52dd5b590a8
|
7
|
+
data.tar.gz: a29709b8af3237fd1a013a97ac32c71d40178d29580ebd748c85b36f5e1cf78dc57b86f7bba93cc878581be490329389c469fb8a80a7c06e4188bd3655ca8749
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# = structurematch.rb - Library for deep binding values from JSON
|
2
|
+
# Copyright 2014, Victor Lowther <victor.lowther@gmail.com>
|
3
|
+
# Licensed under the terms of the Apache 2 license.
|
4
|
+
#
|
5
|
+
|
6
|
+
# StructureMatch encapsulates all the logic needed perform deep binding and
|
7
|
+
# scoring of a JSON data structure.
|
8
|
+
class StructureMatch
|
9
|
+
|
10
|
+
# Comparator handles the actual comparison operations that StructureMatch uses.
|
11
|
+
# Comparators are initialized using a hash with the following structure:
|
12
|
+
#
|
13
|
+
# {
|
14
|
+
# "op" => "operation"
|
15
|
+
# "match" => "The value that the operation should work with"
|
16
|
+
# }
|
17
|
+
#
|
18
|
+
# Comparator knows about the following tests:
|
19
|
+
# "==","!=",">","<",">=","<=" --> The matching tests from Comparable.
|
20
|
+
# "and" --> Returns true if all the submatches return true, false otherwise.
|
21
|
+
# "or" --> Returns true if one of the submatches trturn true, false otherwise.
|
22
|
+
# "and" and "or" require that "match" be an array of hashes that Comparator.new can process.
|
23
|
+
# "not" --> Inverts its submatch. Requires that "match" be a hash that Comparator.new can process.
|
24
|
+
# "range" --> Tests to see if a value is within a range of values.
|
25
|
+
# Requires that "match" be a two-element array whose elements can be used to construct a Range.
|
26
|
+
# "regex" --> Tests to see if a value matches a regular expression. "match" must be a regex.
|
27
|
+
# "member" --> Tests to see if a value is within an array. "match" must be the array.
|
28
|
+
class Comparator
|
29
|
+
def initialize(op)
|
30
|
+
raise "#{op.inspect} must be a Hash" unless op.kind_of?(Hash)
|
31
|
+
unless ["==","!=",">","<",">=","<=","and","or","not","range","regex","member"].member?(op["op"])
|
32
|
+
raise "Operator #{op["op"]} is not one that Comparator knows how to use!"
|
33
|
+
end
|
34
|
+
raise "#{op.inspect} must have a match key" unless op.has_key?("match")
|
35
|
+
@op = op["op"].dup.freeze
|
36
|
+
@match = op["match"]
|
37
|
+
case @op
|
38
|
+
when "and","or"
|
39
|
+
raise "#{op.inspect} match key must be an array of submatches" unless @match.kind_of?(Array)
|
40
|
+
@match.map!{|m|Comparator.new(@match)}
|
41
|
+
when "not"
|
42
|
+
@match = Comparator.new(@match)
|
43
|
+
when "range"
|
44
|
+
raise "#{@match.inspect} is not a two-element array" unless @match.kind_of?(Array) && @match.length == 2
|
45
|
+
@match = Range.new(@match[0],@match[1])
|
46
|
+
when "regex" then @match = Regexp.compile(@match)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# test tests to see if v matches @match.
|
51
|
+
# It returns a two-element array:
|
52
|
+
# [score,val]
|
53
|
+
# score is the score adjustment factor for this test
|
54
|
+
# val is the value that test returns. It is usually the value that was passed in,
|
55
|
+
# except for regular expressions (which return the MatchData) and array & array
|
56
|
+
# comparisons performed by member (which returns the set intersection of the arrays)
|
57
|
+
def test(v=true)
|
58
|
+
case @op
|
59
|
+
when "and" then [_t(@match.all?{|m|m.test(v)[0]}),v]
|
60
|
+
when "or" then [_t(@match.any?{|m|m.test(v)[0]}),v]
|
61
|
+
when "not" then [_t(!@match.test(v)[0]),v]
|
62
|
+
when "range" then [_t(@match === v),v]
|
63
|
+
when "regex"
|
64
|
+
r = @match.match(v)
|
65
|
+
[r.nil? ? -1 : r.length, r]
|
66
|
+
when "member"
|
67
|
+
if v.kind_of?(Array)
|
68
|
+
r = @match & v
|
69
|
+
[r.empty? ? -1 : r.length, r]
|
70
|
+
else
|
71
|
+
[_t(@match.member?(v)),v]
|
72
|
+
end
|
73
|
+
when "==" then [_t(@match == v),v]
|
74
|
+
when "!=" then [_t(@match != v),v]
|
75
|
+
when ">" then [_t(@match > v),v]
|
76
|
+
when "<" then [_t(@match < v),v]
|
77
|
+
when ">=" then [_t(@match >= v),v]
|
78
|
+
when "<=" then [_t(@match <= v),v]
|
79
|
+
else
|
80
|
+
raise "Comparator cannot handle #{@op}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
def _t(v)
|
86
|
+
v ? 1 : -1
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def initialize(matcher)
|
91
|
+
raise "#{matcher.inspect} must be a Hash" unless matcher.kind_of?(Hash)
|
92
|
+
@matchers = Hash.new
|
93
|
+
matcher.each do |k,v|
|
94
|
+
raise "#{k} must be a String" unless k.kind_of?(String)
|
95
|
+
key = k.dup.freeze
|
96
|
+
@matchers[key] = case
|
97
|
+
when v.kind_of?(TrueClass) ||
|
98
|
+
v.kind_of?(FalseClass) ||
|
99
|
+
v.kind_of?(NilClass) ||
|
100
|
+
v.kind_of?(Numeric)
|
101
|
+
Comparator.new("op" => "==", "match" => v)
|
102
|
+
when v.kind_of?(String) then Comparator.new("op" => "==", "match" => v.dup.freeze)
|
103
|
+
when v.kind_of?(Array) then Comparator.new("op" => "member", "match" => v.dup.freeze)
|
104
|
+
when v.kind_of?(Hash) then !!v["__sm_leaf"] ? Comparator.new(v) : StructureMatch.new(v)
|
105
|
+
else
|
106
|
+
raise "Cannot handle node type #{v.inspect}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def bind(val)
|
112
|
+
raise "Must pass a Hash to StructureMatch.bind" unless val.kind_of?(Hash)
|
113
|
+
score = 0
|
114
|
+
binds = Hash.new
|
115
|
+
@matchers.each do |k,v|
|
116
|
+
offset = 0
|
117
|
+
res = nil
|
118
|
+
case
|
119
|
+
when !val.has_key?(k) then offset = -1
|
120
|
+
when v.kind_of?(Comparator)
|
121
|
+
offset,res = v.test(val[k])
|
122
|
+
binds[k] = res if offset > 0
|
123
|
+
when v.kind_of?(StructureMatch)
|
124
|
+
res,offset = v.bind(val[k])
|
125
|
+
binds[k] = v
|
126
|
+
else
|
127
|
+
raise "StructureMatch.bind: No idea how to handle #{v.class.name}: #{v.inspect}"
|
128
|
+
end
|
129
|
+
score += offset
|
130
|
+
end
|
131
|
+
[binds,score]
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
def score(val)
|
136
|
+
bind(val)[1]
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
metadata
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: structurematch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Victor Lowther
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-25 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A library for ferforming structural and semantic matching on JSON
|
14
|
+
email: victor.lowther@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/structurematch.rb
|
20
|
+
homepage: https://github.com/VictorLowther/structurematch
|
21
|
+
licenses:
|
22
|
+
- Apache 2
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements: []
|
39
|
+
rubyforge_project:
|
40
|
+
rubygems_version: 2.2.2
|
41
|
+
signing_key:
|
42
|
+
specification_version: 4
|
43
|
+
summary: Perform structural matching on JSON datastructures
|
44
|
+
test_files: []
|