factbase 0.0.28 → 0.0.30
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 +4 -4
- data/.rubocop.yml +2 -2
- data/README.md +40 -4
- data/lib/factbase/fact.rb +2 -1
- data/lib/factbase/looged.rb +7 -5
- data/lib/factbase/query.rb +3 -3
- data/lib/factbase/rules.rb +1 -1
- data/lib/factbase/syntax.rb +10 -8
- data/lib/factbase/term.rb +83 -8
- data/lib/factbase/to_xml.rb +5 -3
- data/lib/factbase.rb +1 -1
- data/test/factbase/test_fact.rb +18 -0
- data/test/factbase/test_looged.rb +19 -2
- data/test/factbase/test_query.rb +4 -0
- data/test/factbase/test_syntax.rb +6 -3
- data/test/factbase/test_term.rb +60 -46
- data/test/factbase/test_to_xml.rb +2 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 58a162c50ba7d2b9d21ca0ff2d62e34acd0c46d2666b596ceffcceb2dc4215c0
|
4
|
+
data.tar.gz: 61514fa0fa904ea71540c0f9d4776ea75d5405aa5a1b73d32e051c29606b5fdb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b3841073dba72e3fcc6e1fd089235d93829264504447557b3b23f6eb3fa74a4ebccad0d27062af84dc00ab115ca06d9dcc9621bf245d54e09b121bb21983737
|
7
|
+
data.tar.gz: aed653f93b7505db410e81d9fefd8fe9d38fc6b89947f89c952992bcdd76dab1d5f9e4a8cd941df426cb7b200855f26fb2981a9698ac35e8e6ea682c693787e7
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -12,15 +12,26 @@
|
|
12
12
|
[](https://github.com/yegor256/factbase/blob/master/LICENSE.txt)
|
13
13
|
|
14
14
|
This Ruby gem manages an in-memory database of facts.
|
15
|
+
A fact is simply a map of properties and values.
|
16
|
+
The values are either atomic literals or non-empty sets of literals.
|
17
|
+
It is possible to delete a fact, but impossible to delete a property from a fact.
|
18
|
+
|
19
|
+
**ATTENTION**: The current implemention is naive and,
|
20
|
+
because of that, very slow. I will be very happy
|
21
|
+
if you suggest a better implementation without the change of the interface.
|
22
|
+
The `Factbase::query()` method is what mostly needs performance optimization:
|
23
|
+
currently it simply iterates through all facts in the factbase in order
|
24
|
+
to find those that match the provided terms. Obviously,
|
25
|
+
even a simple indexing may significantly increase performance.
|
15
26
|
|
16
27
|
Here is how you use it (it's thread-safe, by the way):
|
17
28
|
|
18
29
|
```ruby
|
19
30
|
fb = Factbase.new
|
20
31
|
f = fb.insert
|
21
|
-
f.
|
32
|
+
f.kind = 'book'
|
22
33
|
f.title = 'Object Thinking'
|
23
|
-
fb.query('(eq
|
34
|
+
fb.query('(eq kind "book")').each do |f|
|
24
35
|
f.seen = true
|
25
36
|
end
|
26
37
|
fb.insert
|
@@ -29,17 +40,42 @@ fb.query('(not (exists seen))').each do |f|
|
|
29
40
|
end
|
30
41
|
```
|
31
42
|
|
32
|
-
You can save the factbase to disc and load it back:
|
43
|
+
You can save the factbase to the disc and then load it back:
|
33
44
|
|
34
45
|
```ruby
|
35
46
|
file = '/tmp/simple.fb'
|
36
47
|
f1 = Factbase.new
|
37
|
-
f1.insert
|
48
|
+
f = f1.insert
|
49
|
+
f.foo = 42
|
38
50
|
File.save(file, f1.export)
|
39
51
|
f2 = Factbase.new
|
40
52
|
f2.import(File.read(file))
|
53
|
+
assert(f2.query('(eq foo 42)').each.to_a.size == 1)
|
41
54
|
```
|
42
55
|
|
56
|
+
All terms available in a query:
|
57
|
+
|
58
|
+
* `()` is true
|
59
|
+
* `(nil)` is false
|
60
|
+
* `(not t)` inverses the `t` if it's boolean (exception otherwise)
|
61
|
+
* `(or t1 t2 ...)` returns true if at least one argument is true
|
62
|
+
* `(and t1 t2 ...)` returns true if all arguments are true
|
63
|
+
* `(when t1 t2)` returns true if `t1` is true and `t2` is true or `t1` is false
|
64
|
+
* `(exists k)` returns true if `k` property exists in the fact
|
65
|
+
* `(absent k)` returns true if `k` property is absent
|
66
|
+
* `(eq a b)` returns true if `a` equals to `b`
|
67
|
+
* `(lt a b)` returns true if `a` is less than `b`
|
68
|
+
* `(gt a b)` returns true if `a` is greater than `b`
|
69
|
+
* `(size k)` returns cardinality of `k` property (zero if property is absent)
|
70
|
+
* `(type a)` returns type of `a` ("String", "Integer", "Float", or "Time")
|
71
|
+
* `(matches a re)` returns true when `a` matches regular expression `re`
|
72
|
+
* `(defn foo "self.to_s")` defines a new term using Ruby syntax and returns true
|
73
|
+
|
74
|
+
There are also terms that match the entire factbase:
|
75
|
+
|
76
|
+
* `(max k)` returns true if the value of `k` property is the largest in the entire factbase
|
77
|
+
* `(min k)` returns true if the value of `k` is the smallest
|
78
|
+
|
43
79
|
## How to contribute
|
44
80
|
|
45
81
|
Read [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
|
data/lib/factbase/fact.rb
CHANGED
@@ -61,6 +61,7 @@ class Factbase::Fact
|
|
61
61
|
end
|
62
62
|
@map[kk] = [@map[kk]] unless @map[kk].is_a?(Array)
|
63
63
|
@map[kk] << v
|
64
|
+
@map[kk].uniq!
|
64
65
|
end
|
65
66
|
nil
|
66
67
|
elsif k == '[]'
|
@@ -69,7 +70,7 @@ class Factbase::Fact
|
|
69
70
|
v = @map[k]
|
70
71
|
if v.nil?
|
71
72
|
raise "Can't get '#{k}', the fact is empty" if @map.empty?
|
72
|
-
raise "Can't find '#{k}' attribute
|
73
|
+
raise "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]"
|
73
74
|
end
|
74
75
|
v.is_a?(Array) ? v[0] : v
|
75
76
|
end
|
data/lib/factbase/looged.rb
CHANGED
@@ -42,7 +42,7 @@ class Factbase::Looged
|
|
42
42
|
|
43
43
|
def insert
|
44
44
|
f = @fb.insert
|
45
|
-
@loog.debug(
|
45
|
+
@loog.debug("Inserted new fact ##{@fb.size}")
|
46
46
|
Fact.new(f, @loog)
|
47
47
|
end
|
48
48
|
|
@@ -79,6 +79,7 @@ class Factbase::Looged
|
|
79
79
|
v = args[1]
|
80
80
|
s = v.is_a?(Time) ? v.utc.iso8601 : v.to_s
|
81
81
|
s = v.to_s.inspect if v.is_a?(String)
|
82
|
+
s = "#{s[0..40]}...#{s[-40..]}" if s.length > 80
|
82
83
|
@loog.debug("Set '#{k[0..-2]}' to #{s} (#{v.class})") if k.end_with?('=')
|
83
84
|
r
|
84
85
|
end
|
@@ -103,13 +104,14 @@ class Factbase::Looged
|
|
103
104
|
end
|
104
105
|
|
105
106
|
def each(&)
|
107
|
+
q = Factbase::Syntax.new(@expr).to_term.to_s
|
106
108
|
if block_given?
|
107
109
|
r = @query.each(&)
|
108
110
|
raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
|
109
111
|
if r.zero?
|
110
|
-
@loog.debug("Nothing found by '#{
|
112
|
+
@loog.debug("Nothing found by '#{q}'")
|
111
113
|
else
|
112
|
-
@loog.debug("Found #{r} fact(s) by '#{
|
114
|
+
@loog.debug("Found #{r} fact(s) by '#{q}'")
|
113
115
|
end
|
114
116
|
r
|
115
117
|
else
|
@@ -120,9 +122,9 @@ class Factbase::Looged
|
|
120
122
|
end
|
121
123
|
# rubocop:enable Style/MapIntoArray
|
122
124
|
if array.empty?
|
123
|
-
@loog.debug("Nothing found by '#{
|
125
|
+
@loog.debug("Nothing found by '#{q}'")
|
124
126
|
else
|
125
|
-
@loog.debug("Found #{array.size} fact(s) by '#{
|
127
|
+
@loog.debug("Found #{array.size} fact(s) by '#{q}'")
|
126
128
|
end
|
127
129
|
array
|
128
130
|
end
|
data/lib/factbase/query.rb
CHANGED
@@ -40,11 +40,11 @@ class Factbase::Query
|
|
40
40
|
# @return [Integer] Total number of facts yielded
|
41
41
|
def each
|
42
42
|
return to_enum(__method__) unless block_given?
|
43
|
-
term = Factbase::Syntax.new(@query).to_term
|
43
|
+
term = Factbase::Syntax.new(@query).to_term.on(@maps)
|
44
44
|
yielded = 0
|
45
45
|
@maps.each do |m|
|
46
46
|
f = Factbase::Fact.new(@mutex, m)
|
47
|
-
next unless term.
|
47
|
+
next unless term.evaluate(f)
|
48
48
|
yield f
|
49
49
|
yielded += 1
|
50
50
|
end
|
@@ -59,7 +59,7 @@ class Factbase::Query
|
|
59
59
|
@mutex.synchronize do
|
60
60
|
@maps.delete_if do |m|
|
61
61
|
f = Factbase::Fact.new(@mutex, m)
|
62
|
-
if term.
|
62
|
+
if term.evaluate(f)
|
63
63
|
deleted += 1
|
64
64
|
true
|
65
65
|
else
|
data/lib/factbase/rules.rb
CHANGED
data/lib/factbase/syntax.rb
CHANGED
@@ -41,9 +41,10 @@ class Factbase::Syntax
|
|
41
41
|
def to_term
|
42
42
|
@tokens ||= to_tokens
|
43
43
|
@ast ||= to_ast(@tokens, 0)
|
44
|
+
raise "Too many terms: #{@query}" if @ast[1] != @tokens.size
|
44
45
|
term = @ast[0]
|
45
|
-
raise
|
46
|
-
raise
|
46
|
+
raise "No terms found: #{@query}" if term.nil?
|
47
|
+
raise "Not a term: #{@query}" unless term.is_a?(Factbase::Term)
|
47
48
|
term
|
48
49
|
end
|
49
50
|
|
@@ -57,7 +58,7 @@ class Factbase::Syntax
|
|
57
58
|
# is the term/literal and the second one is the position where the
|
58
59
|
# scanning should continue.
|
59
60
|
def to_ast(tokens, at)
|
60
|
-
raise "Closing too soon at ##{at}" if tokens[at] == :close
|
61
|
+
raise "Closing too soon at ##{at}: #{@query}" if tokens[at] == :close
|
61
62
|
return [tokens[at], at + 1] unless tokens[at] == :open
|
62
63
|
at += 1
|
63
64
|
op = tokens[at]
|
@@ -65,11 +66,11 @@ class Factbase::Syntax
|
|
65
66
|
operands = []
|
66
67
|
at += 1
|
67
68
|
loop do
|
68
|
-
raise "End of token stream at ##{at}" if tokens[at].nil?
|
69
|
+
raise "End of token stream at ##{at}: #{@query}" if tokens[at].nil?
|
69
70
|
break if tokens[at] == :close
|
70
71
|
(operand, at1) = to_ast(tokens, at)
|
71
|
-
raise "Stuck at position ##{at}" if at == at1
|
72
|
-
raise "Jump back at position ##{at}" if at1 < at
|
72
|
+
raise "Stuck at position ##{at}: #{@query}" if at == at1
|
73
|
+
raise "Jump back at position ##{at}: #{@query}" if at1 < at
|
73
74
|
at = at1
|
74
75
|
operands << operand
|
75
76
|
break if tokens[at] == :close
|
@@ -109,12 +110,12 @@ class Factbase::Syntax
|
|
109
110
|
acc += c
|
110
111
|
end
|
111
112
|
end
|
112
|
-
raise
|
113
|
+
raise "String not closed: : #{@query}" if string
|
113
114
|
list.map do |t|
|
114
115
|
if t.is_a?(Symbol)
|
115
116
|
t
|
116
117
|
elsif t.start_with?('\'', '"')
|
117
|
-
raise
|
118
|
+
raise "String literal can't be empty: #{@query}" if t.length <= 2
|
118
119
|
t[1..-2]
|
119
120
|
elsif t.match?(/^[0-9]+$/)
|
120
121
|
t.to_i
|
@@ -123,6 +124,7 @@ class Factbase::Syntax
|
|
123
124
|
elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
|
124
125
|
Time.parse(t)
|
125
126
|
else
|
127
|
+
raise "Wrong symbol format (#{t}): #{@query}" unless t.match?(/^[a-z][a-zA-Z0-9_]*$/)
|
126
128
|
t.to_sym
|
127
129
|
end
|
128
130
|
end
|
data/lib/factbase/term.rb
CHANGED
@@ -41,10 +41,22 @@ class Factbase::Term
|
|
41
41
|
# Does it match the fact?
|
42
42
|
# @param [Factbase::Fact] fact The fact
|
43
43
|
# @return [bool] TRUE if matches
|
44
|
-
def
|
44
|
+
def evaluate(fact)
|
45
45
|
send(@op, fact)
|
46
46
|
end
|
47
47
|
|
48
|
+
# Put it into the context: let it see the entire array of maps.
|
49
|
+
# @param [Array] maps The maps
|
50
|
+
# @return [Factbase::Term] Itself
|
51
|
+
def on(maps)
|
52
|
+
m = "#{@op}_on"
|
53
|
+
send(m, maps) if respond_to?(m, true)
|
54
|
+
@operands.each do |o|
|
55
|
+
o.on(maps) if o.is_a?(Factbase::Term)
|
56
|
+
end
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
48
60
|
# Turns it into a string.
|
49
61
|
# @return [String] The string of it
|
50
62
|
def to_s
|
@@ -71,19 +83,19 @@ class Factbase::Term
|
|
71
83
|
|
72
84
|
def not(fact)
|
73
85
|
assert_args(1)
|
74
|
-
|
86
|
+
!only_bool(the_value(0, fact))
|
75
87
|
end
|
76
88
|
|
77
89
|
def or(fact)
|
78
|
-
|
79
|
-
return true if
|
90
|
+
(0..@operands.size - 1).each do |i|
|
91
|
+
return true if only_bool(the_value(i, fact))
|
80
92
|
end
|
81
93
|
false
|
82
94
|
end
|
83
95
|
|
84
96
|
def and(fact)
|
85
|
-
|
86
|
-
return false unless
|
97
|
+
(0..@operands.size - 1).each do |i|
|
98
|
+
return false unless only_bool(the_value(i, fact))
|
87
99
|
end
|
88
100
|
true
|
89
101
|
end
|
@@ -92,7 +104,7 @@ class Factbase::Term
|
|
92
104
|
assert_args(2)
|
93
105
|
a = @operands[0]
|
94
106
|
b = @operands[1]
|
95
|
-
!a.
|
107
|
+
!a.evaluate(fact) || (a.evaluate(fact) && b.evaluate(fact))
|
96
108
|
end
|
97
109
|
|
98
110
|
def exists(fact)
|
@@ -132,6 +144,17 @@ class Factbase::Term
|
|
132
144
|
v.class.to_s
|
133
145
|
end
|
134
146
|
|
147
|
+
def matches(fact)
|
148
|
+
assert_args(2)
|
149
|
+
str = the_value(0, fact)
|
150
|
+
raise 'String is nil' if str.nil?
|
151
|
+
raise 'Exactly one string expected' unless str.size == 1
|
152
|
+
re = the_value(1, fact)
|
153
|
+
raise 'Regexp is nil' if re.nil?
|
154
|
+
raise 'Exactly one regexp expected' unless re.size == 1
|
155
|
+
str[0].to_s.match?(re[0])
|
156
|
+
end
|
157
|
+
|
135
158
|
def arithmetic(op, fact)
|
136
159
|
assert_args(2)
|
137
160
|
lefts = the_value(0, fact)
|
@@ -147,6 +170,36 @@ class Factbase::Term
|
|
147
170
|
end
|
148
171
|
end
|
149
172
|
|
173
|
+
def defn(_fact)
|
174
|
+
fn = @operands[0]
|
175
|
+
raise 'A symbol expected as first argument of defn' unless fn.is_a?(Symbol)
|
176
|
+
e = "class Factbase::Term\nprivate\ndef #{fn}(fact)\n#{@operands[1]}\nend\nend"
|
177
|
+
# rubocop:disable Security/Eval
|
178
|
+
eval(e)
|
179
|
+
# rubocop:enable Security/Eval
|
180
|
+
true
|
181
|
+
end
|
182
|
+
|
183
|
+
def min(fact)
|
184
|
+
vv = the_value(0, fact)
|
185
|
+
return nil if vv.nil?
|
186
|
+
vv.any? { |v| v == @min }
|
187
|
+
end
|
188
|
+
|
189
|
+
def max(fact)
|
190
|
+
vv = the_value(0, fact)
|
191
|
+
return nil if vv.nil?
|
192
|
+
vv.any? { |v| v == @max }
|
193
|
+
end
|
194
|
+
|
195
|
+
def max_on(maps)
|
196
|
+
@max = best(maps) { |v, b| v > b }
|
197
|
+
end
|
198
|
+
|
199
|
+
def min_on(maps)
|
200
|
+
@min = best(maps) { |v, b| v < b }
|
201
|
+
end
|
202
|
+
|
150
203
|
def assert_args(num)
|
151
204
|
c = @operands.size
|
152
205
|
raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
|
@@ -162,10 +215,32 @@ class Factbase::Term
|
|
162
215
|
|
163
216
|
def the_value(pos, fact)
|
164
217
|
v = @operands[pos]
|
165
|
-
v = v.
|
218
|
+
v = v.evaluate(fact) if v.is_a?(Factbase::Term)
|
166
219
|
v = fact[v.to_s] if v.is_a?(Symbol)
|
167
220
|
return v if v.nil?
|
168
221
|
v = [v] unless v.is_a?(Array)
|
169
222
|
v
|
170
223
|
end
|
224
|
+
|
225
|
+
def only_bool(val)
|
226
|
+
val = val[0] if val.is_a?(Array)
|
227
|
+
return false if val.nil?
|
228
|
+
raise "Boolean expected, while #{val.class} received" unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
229
|
+
val
|
230
|
+
end
|
231
|
+
|
232
|
+
def best(maps)
|
233
|
+
k = @operands[0]
|
234
|
+
raise "A symbol expected, but provided: #{k}" unless k.is_a?(Symbol)
|
235
|
+
best = nil
|
236
|
+
maps.each do |m|
|
237
|
+
vv = m[k.to_s]
|
238
|
+
next if vv.nil?
|
239
|
+
vv = [vv] unless vv.is_a?(Array)
|
240
|
+
vv.each do |v|
|
241
|
+
best = v if best.nil? || yield(v, best)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
best
|
245
|
+
end
|
171
246
|
end
|
data/lib/factbase/to_xml.rb
CHANGED
@@ -37,11 +37,13 @@ class Factbase::ToXML
|
|
37
37
|
# Convert the entire factbase into XML.
|
38
38
|
# @return [String] The factbase in XML format
|
39
39
|
def xml
|
40
|
+
bytes = @fb.export
|
41
|
+
maps = Marshal.load(bytes)
|
40
42
|
meta = {
|
41
|
-
|
42
|
-
dob: Time.now.utc.iso8601
|
43
|
+
version: Factbase::VERSION,
|
44
|
+
dob: Time.now.utc.iso8601,
|
45
|
+
size: bytes.size
|
43
46
|
}
|
44
|
-
maps = Marshal.load(@fb.export)
|
45
47
|
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
46
48
|
xml.fb(meta) do
|
47
49
|
maps.each do |m|
|
data/lib/factbase.rb
CHANGED
data/test/factbase/test_fact.rb
CHANGED
@@ -39,6 +39,16 @@ class TestFact < Minitest::Test
|
|
39
39
|
assert_equal([42, 256], f['foo'], f.to_s)
|
40
40
|
end
|
41
41
|
|
42
|
+
def test_keeps_values_unique
|
43
|
+
map = {}
|
44
|
+
f = Factbase::Fact.new(Mutex.new, map)
|
45
|
+
f.foo = 42
|
46
|
+
f.foo = 'Hello'
|
47
|
+
assert_equal(2, map['foo'].size)
|
48
|
+
f.foo = 42
|
49
|
+
assert_equal(2, map['foo'].size)
|
50
|
+
end
|
51
|
+
|
42
52
|
def test_fails_when_empty
|
43
53
|
f = Factbase::Fact.new(Mutex.new, {})
|
44
54
|
assert_raises do
|
@@ -74,6 +84,14 @@ class TestFact < Minitest::Test
|
|
74
84
|
assert_equal(42, f.foo_bar, f.to_s)
|
75
85
|
end
|
76
86
|
|
87
|
+
def test_set_twice_same_value
|
88
|
+
map = {}
|
89
|
+
f = Factbase::Fact.new(Mutex.new, map)
|
90
|
+
f.foo = 42
|
91
|
+
f.foo = 42
|
92
|
+
assert_equal(42, map['foo'])
|
93
|
+
end
|
94
|
+
|
77
95
|
def test_time_in_utc
|
78
96
|
f = Factbase::Fact.new(Mutex.new, {})
|
79
97
|
t = Time.now
|
@@ -69,13 +69,30 @@ class TestLooged < Minitest::Test
|
|
69
69
|
fb.insert
|
70
70
|
fb.insert.bar = 3
|
71
71
|
fb.insert
|
72
|
+
fb.insert.str =
|
73
|
+
"Он поскорей звонит. Вбегает
|
74
|
+
К нему слуга француз Гильо,
|
75
|
+
Халат и туфли предлагает
|
76
|
+
И подает ему белье.
|
77
|
+
Спешит Онегин одеваться,
|
78
|
+
Слуге велит приготовляться
|
79
|
+
С ним вместе ехать и с собой
|
80
|
+
Взять также ящик боевой.
|
81
|
+
Готовы санки беговые.
|
82
|
+
Он сел, на мельницу летит.
|
83
|
+
Примчались. Он слуге велит
|
84
|
+
Лепажа стволы роковые
|
85
|
+
Нести за ним, а лошадям
|
86
|
+
Отъехать в поле к двум дубкам."
|
72
87
|
fb.query('(exists bar)').each(&:to_s)
|
73
88
|
fb.query('(not (exists bar))').delete!
|
74
89
|
[
|
75
|
-
'Inserted new fact',
|
90
|
+
'Inserted new fact #1',
|
91
|
+
'Inserted new fact #2',
|
76
92
|
'Set \'bar\' to 3 (Integer)',
|
93
|
+
'Set \'str\' to "Он поскорей звонит. Вбегает\n К нем...м\n Отъехать в поле к двум дубкам." (String)',
|
77
94
|
'Found 1 fact(s) by \'(exists bar)\'',
|
78
|
-
'Deleted
|
95
|
+
'Deleted 3 fact(s) by \'(not (exists bar))\''
|
79
96
|
].each do |s|
|
80
97
|
assert(log.to_s.include?("#{s}\n"), "#{log}\n")
|
81
98
|
end
|
data/test/factbase/test_query.rb
CHANGED
@@ -60,6 +60,10 @@ class TestQuery < Minitest::Test
|
|
60
60
|
'(eq (size hello) 0)' => 3,
|
61
61
|
'(eq num pi)' => 0,
|
62
62
|
'(absent time)' => 2,
|
63
|
+
'(max num)' => 1,
|
64
|
+
'(and (exists time) (max num))' => 0,
|
65
|
+
'(and (exists pi) (max num))' => 1,
|
66
|
+
'(min time)' => 1,
|
63
67
|
'(and (absent time) (exists pi))' => 1,
|
64
68
|
"(and (exists time) (not (\t\texists pi)))" => 1,
|
65
69
|
"(or (eq num 66) (lt time #{(Time.now - 200).utc.iso8601}))" => 1
|
@@ -45,8 +45,9 @@ class TestSyntax < Minitest::Test
|
|
45
45
|
'(foo)',
|
46
46
|
'(foo (bar) (zz 77) )',
|
47
47
|
"(eq foo \n\n 'Hello, world!'\n)\n",
|
48
|
+
"(eq x 'Hello, \\' \n) \\' ( world!')",
|
48
49
|
"# this is a comment\n(eq foo # test\n 42)\n\n# another comment\n",
|
49
|
-
"(or ( a 4) (b 5) () (and () (c 5) \t\t(r 7
|
50
|
+
"(or ( a 4) (b 5) () (and () (c 5) \t\t(r 7 w8s w8is 'Foo')))"
|
50
51
|
].each do |q|
|
51
52
|
Factbase::Syntax.new(q).to_term
|
52
53
|
end
|
@@ -83,7 +84,7 @@ class TestSyntax < Minitest::Test
|
|
83
84
|
'(or (eq bar 888) (eq z 1))' => true,
|
84
85
|
"(or (gt bar 100) (eq foo 'Hello, world!'))" => true
|
85
86
|
}.each do |k, v|
|
86
|
-
assert_equal(v, Factbase::Syntax.new(k).to_term.
|
87
|
+
assert_equal(v, Factbase::Syntax.new(k).to_term.evaluate(m), k)
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|
@@ -91,11 +92,13 @@ class TestSyntax < Minitest::Test
|
|
91
92
|
[
|
92
93
|
'',
|
93
94
|
'(foo',
|
95
|
+
'(foo 1) (bar 2)',
|
94
96
|
'some text',
|
95
97
|
'"hello, world!',
|
96
98
|
'(foo 7',
|
97
99
|
"(foo 7 'Dude'",
|
98
100
|
'(foo x y z (',
|
101
|
+
'(bad-term-name 42)',
|
99
102
|
'(foo x y (z t (f 42 ',
|
100
103
|
')foo ) y z)',
|
101
104
|
'(x "")',
|
@@ -103,7 +106,7 @@ class TestSyntax < Minitest::Test
|
|
103
106
|
')',
|
104
107
|
'"'
|
105
108
|
].each do |q|
|
106
|
-
assert_raises do
|
109
|
+
assert_raises(q) do
|
107
110
|
Factbase::Syntax.new(q).to_term
|
108
111
|
end
|
109
112
|
end
|
data/test/factbase/test_term.rb
CHANGED
@@ -30,95 +30,102 @@ require_relative '../../lib/factbase/term'
|
|
30
30
|
class TestTerm < Minitest::Test
|
31
31
|
def test_simple_matching
|
32
32
|
t = Factbase::Term.new(:eq, [:foo, 42])
|
33
|
-
assert(t.
|
34
|
-
assert(!t.
|
35
|
-
assert(!t.
|
33
|
+
assert(t.evaluate(fact('foo' => [42])))
|
34
|
+
assert(!t.evaluate(fact('foo' => 'Hello!')))
|
35
|
+
assert(!t.evaluate(fact('bar' => ['Hello!'])))
|
36
36
|
end
|
37
37
|
|
38
38
|
def test_eq_matching
|
39
39
|
t = Factbase::Term.new(:eq, [:foo, 42])
|
40
|
-
assert(t.
|
41
|
-
assert(t.
|
42
|
-
assert(!t.
|
43
|
-
assert(!t.
|
44
|
-
assert(!t.
|
40
|
+
assert(t.evaluate(fact('foo' => 42)))
|
41
|
+
assert(t.evaluate(fact('foo' => [10, 5, 6, -8, 'hey', 42, 9, 'fdsf'])))
|
42
|
+
assert(!t.evaluate(fact('foo' => [100])))
|
43
|
+
assert(!t.evaluate(fact('foo' => [])))
|
44
|
+
assert(!t.evaluate(fact('bar' => [])))
|
45
45
|
end
|
46
46
|
|
47
47
|
def test_eq_matching_time
|
48
48
|
now = Time.now
|
49
49
|
t = Factbase::Term.new(:eq, [:foo, Time.parse(now.iso8601)])
|
50
|
-
assert(t.
|
51
|
-
assert(t.
|
50
|
+
assert(t.evaluate(fact('foo' => now)))
|
51
|
+
assert(t.evaluate(fact('foo' => [now, Time.now])))
|
52
52
|
end
|
53
53
|
|
54
54
|
def test_lt_matching
|
55
55
|
t = Factbase::Term.new(:lt, [:foo, 42])
|
56
|
-
assert(t.
|
57
|
-
assert(!t.
|
58
|
-
assert(!t.
|
59
|
-
assert(!t.
|
56
|
+
assert(t.evaluate(fact('foo' => [10])))
|
57
|
+
assert(!t.evaluate(fact('foo' => [100])))
|
58
|
+
assert(!t.evaluate(fact('foo' => 100)))
|
59
|
+
assert(!t.evaluate(fact('bar' => 100)))
|
60
60
|
end
|
61
61
|
|
62
62
|
def test_gt_matching
|
63
63
|
t = Factbase::Term.new(:gt, [:foo, 42])
|
64
|
-
assert(t.
|
65
|
-
assert(t.
|
66
|
-
assert(!t.
|
67
|
-
assert(!t.
|
68
|
-
assert(!t.
|
64
|
+
assert(t.evaluate(fact('foo' => [100])))
|
65
|
+
assert(t.evaluate(fact('foo' => 100)))
|
66
|
+
assert(!t.evaluate(fact('foo' => [10])))
|
67
|
+
assert(!t.evaluate(fact('foo' => 10)))
|
68
|
+
assert(!t.evaluate(fact('bar' => 10)))
|
69
69
|
end
|
70
70
|
|
71
71
|
def test_lt_matching_time
|
72
72
|
t = Factbase::Term.new(:lt, [:foo, Time.now])
|
73
|
-
assert(t.
|
74
|
-
assert(!t.
|
75
|
-
assert(!t.
|
73
|
+
assert(t.evaluate(fact('foo' => [Time.now - 100])))
|
74
|
+
assert(!t.evaluate(fact('foo' => [Time.now + 100])))
|
75
|
+
assert(!t.evaluate(fact('bar' => [100])))
|
76
76
|
end
|
77
77
|
|
78
78
|
def test_gt_matching_time
|
79
79
|
t = Factbase::Term.new(:gt, [:foo, Time.now])
|
80
|
-
assert(t.
|
81
|
-
assert(!t.
|
82
|
-
assert(!t.
|
80
|
+
assert(t.evaluate(fact('foo' => [Time.now + 100])))
|
81
|
+
assert(!t.evaluate(fact('foo' => [Time.now - 100])))
|
82
|
+
assert(!t.evaluate(fact('bar' => [100])))
|
83
83
|
end
|
84
84
|
|
85
85
|
def test_not_matching
|
86
86
|
t = Factbase::Term.new(:not, [Factbase::Term.new(:nil, [])])
|
87
|
-
assert(!t.
|
87
|
+
assert(!t.evaluate(fact('foo' => [100])))
|
88
88
|
end
|
89
89
|
|
90
90
|
def test_not_eq_matching
|
91
91
|
t = Factbase::Term.new(:not, [Factbase::Term.new(:eq, [:foo, 100])])
|
92
|
-
assert(t.
|
93
|
-
assert(!t.
|
92
|
+
assert(t.evaluate(fact('foo' => [42, 12, -90])))
|
93
|
+
assert(!t.evaluate(fact('foo' => 100)))
|
94
94
|
end
|
95
95
|
|
96
96
|
def test_size_matching
|
97
97
|
t = Factbase::Term.new(:size, [:foo])
|
98
|
-
assert_equal(3, t.
|
99
|
-
assert_equal(0, t.
|
98
|
+
assert_equal(3, t.evaluate(fact('foo' => [42, 12, -90])))
|
99
|
+
assert_equal(0, t.evaluate(fact('bar' => 100)))
|
100
100
|
end
|
101
101
|
|
102
102
|
def test_exists_matching
|
103
103
|
t = Factbase::Term.new(:exists, [:foo])
|
104
|
-
assert(t.
|
105
|
-
assert(!t.
|
104
|
+
assert(t.evaluate(fact('foo' => [42, 12, -90])))
|
105
|
+
assert(!t.evaluate(fact('bar' => 100)))
|
106
106
|
end
|
107
107
|
|
108
108
|
def test_absent_matching
|
109
109
|
t = Factbase::Term.new(:absent, [:foo])
|
110
|
-
assert(t.
|
111
|
-
assert(!t.
|
110
|
+
assert(t.evaluate(fact('z' => [42, 12, -90])))
|
111
|
+
assert(!t.evaluate(fact('foo' => 100)))
|
112
112
|
end
|
113
113
|
|
114
114
|
def test_type_matching
|
115
115
|
t = Factbase::Term.new(:type, [:foo])
|
116
|
-
assert_equal('Integer', t.
|
117
|
-
assert_equal('Array', t.
|
118
|
-
assert_equal('String', t.
|
119
|
-
assert_equal('Float', t.
|
120
|
-
assert_equal('Time', t.
|
121
|
-
assert_equal('nil', t.
|
116
|
+
assert_equal('Integer', t.evaluate(fact('foo' => 42)))
|
117
|
+
assert_equal('Array', t.evaluate(fact('foo' => [1, 2, 3])))
|
118
|
+
assert_equal('String', t.evaluate(fact('foo' => 'Hello, world!')))
|
119
|
+
assert_equal('Float', t.evaluate(fact('foo' => 3.14)))
|
120
|
+
assert_equal('Time', t.evaluate(fact('foo' => Time.now)))
|
121
|
+
assert_equal('nil', t.evaluate(fact))
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_regexp_matching
|
125
|
+
t = Factbase::Term.new(:matches, [:foo, '[a-z]+'])
|
126
|
+
assert(t.evaluate(fact('foo' => 'hello')))
|
127
|
+
assert(t.evaluate(fact('foo' => 'hello 42')))
|
128
|
+
assert(!t.evaluate(fact('foo' => 42)))
|
122
129
|
end
|
123
130
|
|
124
131
|
def test_or_matching
|
@@ -129,9 +136,9 @@ class TestTerm < Minitest::Test
|
|
129
136
|
Factbase::Term.new(:eq, [:bar, 5])
|
130
137
|
]
|
131
138
|
)
|
132
|
-
assert(t.
|
133
|
-
assert(t.
|
134
|
-
assert(!t.
|
139
|
+
assert(t.evaluate(fact('foo' => [4])))
|
140
|
+
assert(t.evaluate(fact('bar' => [5])))
|
141
|
+
assert(!t.evaluate(fact('bar' => [42])))
|
135
142
|
end
|
136
143
|
|
137
144
|
def test_when_matching
|
@@ -142,9 +149,16 @@ class TestTerm < Minitest::Test
|
|
142
149
|
Factbase::Term.new(:eq, [:bar, 5])
|
143
150
|
]
|
144
151
|
)
|
145
|
-
assert(t.
|
146
|
-
assert(!t.
|
147
|
-
assert(t.
|
152
|
+
assert(t.evaluate(fact('foo' => 4, 'bar' => 5)))
|
153
|
+
assert(!t.evaluate(fact('foo' => 4)))
|
154
|
+
assert(t.evaluate(fact('foo' => 5, 'bar' => 5)))
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_defn_simple
|
158
|
+
t = Factbase::Term.new(:defn, [:foo, 'self.to_s'])
|
159
|
+
assert_equal(true, t.evaluate(fact('foo' => 4)))
|
160
|
+
t1 = Factbase::Term.new(:foo, ['hello, world!'])
|
161
|
+
assert_equal('(foo \'hello, world!\')', t1.evaluate(fact))
|
148
162
|
end
|
149
163
|
|
150
164
|
private
|
@@ -57,7 +57,8 @@ class TestToXML < Minitest::Test
|
|
57
57
|
to = Factbase::ToXML.new(fb)
|
58
58
|
xml = Nokogiri::XML.parse(to.xml)
|
59
59
|
assert(!xml.xpath('/fb[@dob]').empty?)
|
60
|
-
assert(!xml.xpath('/fb[@
|
60
|
+
assert(!xml.xpath('/fb[@version]').empty?)
|
61
|
+
assert(!xml.xpath('/fb[@size]').empty?)
|
61
62
|
end
|
62
63
|
|
63
64
|
def test_to_xml_with_short_names
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: factbase
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.30
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-05-
|
11
|
+
date: 2024-05-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|