factbase 0.0.41 → 0.0.42
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/Gemfile +1 -1
- data/Gemfile.lock +2 -2
- data/README.md +1 -0
- data/lib/factbase/inv.rb +4 -4
- data/lib/factbase/looged.rb +4 -4
- data/lib/factbase/rules.rb +26 -11
- data/lib/factbase/term.rb +24 -10
- data/lib/factbase/to_json.rb +1 -1
- data/lib/factbase/to_xml.rb +1 -1
- data/lib/factbase/to_yaml.rb +1 -1
- data/lib/factbase/tuples.rb +3 -4
- data/lib/factbase.rb +1 -1
- data/test/factbase/test_query.rb +1 -0
- data/test/factbase/test_rules.rb +41 -0
- data/test/factbase/test_term.rb +28 -0
- data/test/factbase/test_to_json.rb +10 -0
- data/test/factbase/test_to_xml.rb +11 -0
- data/test/factbase/test_to_yaml.rb +10 -0
- 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: 42066d9d82538ecb6f0ec8df6cc711f4328e1ba496f00366fe67719ca8a1185e
|
4
|
+
data.tar.gz: d40e21b7e426af5184b9b03f515da4508b1e1e9e52995f566b16c778eefefca3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff04d1f7e3eb1b18b36544ea8fb61280cda250a5e9920ba7b858092ae7c13111efce537d39acd7119f2f70c8704a89d06c98bf54b5a4639b4f187599f61eecbc
|
7
|
+
data.tar.gz: '09ba7d04f933f758c1db716c9490d464a2202644560a594bfae0a77e8bee8d26dcc38605101417a062ccd5c9645b5187e7f069e8f31e6bc782e48dc79775a347'
|
data/Gemfile
CHANGED
@@ -28,7 +28,7 @@ gem 'rake', '13.2.1', require: false
|
|
28
28
|
gem 'rspec-rails', '6.1.2', require: false
|
29
29
|
gem 'rubocop', '1.64.1', require: false
|
30
30
|
gem 'rubocop-performance', '1.21.0', require: false
|
31
|
-
gem 'rubocop-rspec', '2.
|
31
|
+
gem 'rubocop-rspec', '2.30.0', require: false
|
32
32
|
gem 'simplecov', '0.22.0', require: false
|
33
33
|
gem 'simplecov-cobertura', '2.1.0', require: false
|
34
34
|
gem 'yard', '0.9.36', require: false
|
data/Gemfile.lock
CHANGED
@@ -145,7 +145,7 @@ GEM
|
|
145
145
|
rubocop-performance (1.21.0)
|
146
146
|
rubocop (>= 1.48.1, < 2.0)
|
147
147
|
rubocop-ast (>= 1.31.1, < 2.0)
|
148
|
-
rubocop-rspec (2.
|
148
|
+
rubocop-rspec (2.30.0)
|
149
149
|
rubocop (~> 1.40)
|
150
150
|
rubocop-capybara (~> 2.17)
|
151
151
|
rubocop-factory_bot (~> 2.22)
|
@@ -186,7 +186,7 @@ DEPENDENCIES
|
|
186
186
|
rspec-rails (= 6.1.2)
|
187
187
|
rubocop (= 1.64.1)
|
188
188
|
rubocop-performance (= 1.21.0)
|
189
|
-
rubocop-rspec (= 2.
|
189
|
+
rubocop-rspec (= 2.30.0)
|
190
190
|
simplecov (= 0.22.0)
|
191
191
|
simplecov-cobertura (= 2.1.0)
|
192
192
|
yard (= 0.9.36)
|
data/README.md
CHANGED
@@ -88,6 +88,7 @@ Also, some simple arithmetic:
|
|
88
88
|
One term is for meta-programming:
|
89
89
|
|
90
90
|
* `(defn foo "self.to_s")` defines a new term using Ruby syntax and returns true
|
91
|
+
* `(undef foo)` undefines a term (nothing happens if it's not defined yet)
|
91
92
|
|
92
93
|
There are terms that are history of search aware:
|
93
94
|
|
data/lib/factbase/inv.rb
CHANGED
@@ -81,13 +81,13 @@ class Factbase::Inv
|
|
81
81
|
end
|
82
82
|
|
83
83
|
# rubocop:disable Style/OptionalBooleanParameter
|
84
|
-
def respond_to?(
|
84
|
+
def respond_to?(_method, _include_private = false)
|
85
85
|
# rubocop:enable Style/OptionalBooleanParameter
|
86
|
-
|
86
|
+
true
|
87
87
|
end
|
88
88
|
|
89
|
-
def respond_to_missing?(
|
90
|
-
|
89
|
+
def respond_to_missing?(_method, _include_private = false)
|
90
|
+
true
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
data/lib/factbase/looged.rb
CHANGED
@@ -91,13 +91,13 @@ class Factbase::Looged
|
|
91
91
|
end
|
92
92
|
|
93
93
|
# rubocop:disable Style/OptionalBooleanParameter
|
94
|
-
def respond_to?(
|
94
|
+
def respond_to?(_method, _include_private = false)
|
95
95
|
# rubocop:enable Style/OptionalBooleanParameter
|
96
|
-
|
96
|
+
true
|
97
97
|
end
|
98
98
|
|
99
|
-
def respond_to_missing?(
|
100
|
-
|
99
|
+
def respond_to_missing?(_method, _include_private = false)
|
100
|
+
true
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
data/lib/factbase/rules.rb
CHANGED
@@ -21,20 +21,21 @@
|
|
21
21
|
# SOFTWARE.
|
22
22
|
|
23
23
|
require_relative '../factbase'
|
24
|
+
require_relative '../factbase/syntax'
|
24
25
|
|
25
26
|
# A decorator of a Factbase, that checks rules on every set.
|
26
27
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
27
28
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
28
29
|
# License:: MIT
|
29
30
|
class Factbase::Rules
|
30
|
-
def initialize(fb, rules)
|
31
|
+
def initialize(fb, rules, check = Check.new(rules))
|
31
32
|
@fb = fb
|
32
33
|
@rules = rules
|
33
|
-
@check =
|
34
|
+
@check = check
|
34
35
|
end
|
35
36
|
|
36
37
|
def dup
|
37
|
-
Factbase::Rules.new(@fb.dup, @rules)
|
38
|
+
Factbase::Rules.new(@fb.dup, @rules, @check)
|
38
39
|
end
|
39
40
|
|
40
41
|
def size
|
@@ -50,7 +51,13 @@ class Factbase::Rules
|
|
50
51
|
end
|
51
52
|
|
52
53
|
def txn(this = self, &)
|
54
|
+
before = @check
|
55
|
+
@check = Blind.new
|
53
56
|
@fb.txn(this, &)
|
57
|
+
@check = before
|
58
|
+
@fb.query('(always)').each do |f|
|
59
|
+
@check.it(f)
|
60
|
+
end
|
54
61
|
end
|
55
62
|
|
56
63
|
def export
|
@@ -83,13 +90,13 @@ class Factbase::Rules
|
|
83
90
|
end
|
84
91
|
|
85
92
|
# rubocop:disable Style/OptionalBooleanParameter
|
86
|
-
def respond_to?(
|
93
|
+
def respond_to?(_method, _include_private = false)
|
87
94
|
# rubocop:enable Style/OptionalBooleanParameter
|
88
|
-
|
95
|
+
true
|
89
96
|
end
|
90
97
|
|
91
|
-
def respond_to_missing?(
|
92
|
-
|
98
|
+
def respond_to_missing?(_method, _include_private = false)
|
99
|
+
true
|
93
100
|
end
|
94
101
|
end
|
95
102
|
|
@@ -118,16 +125,24 @@ class Factbase::Rules
|
|
118
125
|
# Check one fact.
|
119
126
|
#
|
120
127
|
# This is an internal class, it is not supposed to be instantiated directly.
|
121
|
-
#
|
122
128
|
class Check
|
123
|
-
def initialize(
|
124
|
-
@fb = fb
|
129
|
+
def initialize(expr)
|
125
130
|
@expr = expr
|
126
131
|
end
|
127
132
|
|
128
133
|
def it(fact)
|
129
134
|
return if Factbase::Syntax.new(@expr).to_term.evaluate(fact, [])
|
130
|
-
|
135
|
+
e = "#{@expr[0..32]}..." if @expr.length > 32
|
136
|
+
raise "The fact doesn't match the #{e.inspect} rule: #{fact}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Check one fact (never complaining).
|
141
|
+
#
|
142
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
143
|
+
class Blind
|
144
|
+
def it(_fact)
|
145
|
+
true
|
131
146
|
end
|
132
147
|
end
|
133
148
|
end
|
data/lib/factbase/term.rb
CHANGED
@@ -68,7 +68,7 @@ class Factbase::Term
|
|
68
68
|
rescue NoMethodError => e
|
69
69
|
raise "Term '#{@op}' is not defined at #{self}: #{e.message}"
|
70
70
|
rescue StandardError => e
|
71
|
-
raise "#{e.message} at #{self}"
|
71
|
+
raise "#{e.message} at #{self} (#{e.backtrace[0]})"
|
72
72
|
end
|
73
73
|
|
74
74
|
# Simplify it if possible.
|
@@ -118,14 +118,14 @@ class Factbase::Term
|
|
118
118
|
|
119
119
|
def or(fact, maps)
|
120
120
|
(0..@operands.size - 1).each do |i|
|
121
|
-
return true if only_bool(the_values(i, fact, maps),
|
121
|
+
return true if only_bool(the_values(i, fact, maps), i)
|
122
122
|
end
|
123
123
|
false
|
124
124
|
end
|
125
125
|
|
126
126
|
def and(fact, maps)
|
127
127
|
(0..@operands.size - 1).each do |i|
|
128
|
-
return false unless only_bool(the_values(i, fact, maps),
|
128
|
+
return false unless only_bool(the_values(i, fact, maps), i)
|
129
129
|
end
|
130
130
|
true
|
131
131
|
end
|
@@ -179,7 +179,7 @@ class Factbase::Term
|
|
179
179
|
def at(fact, maps)
|
180
180
|
assert_args(2)
|
181
181
|
i = the_values(0, fact, maps)
|
182
|
-
raise
|
182
|
+
raise "Too many values (#{i.size}) at first position, one expected" unless i.size == 1
|
183
183
|
i = i[0]
|
184
184
|
return nil if i.nil?
|
185
185
|
v = the_values(1, fact, maps)
|
@@ -301,8 +301,12 @@ class Factbase::Term
|
|
301
301
|
end
|
302
302
|
|
303
303
|
def defn(_fact, _maps)
|
304
|
+
assert_args(2)
|
304
305
|
fn = @operands[0]
|
305
|
-
raise
|
306
|
+
raise "A symbol expected as first argument of 'defn'" unless fn.is_a?(Symbol)
|
307
|
+
raise "Can't use '#{fn}' name as a term" if Factbase::Term.instance_methods(true).include?(fn)
|
308
|
+
raise "Term '#{fn}' is already defined" if Factbase::Term.private_instance_methods(false).include?(fn)
|
309
|
+
raise "The '#{fn}' is a bad name for a term" unless fn.match?(/^[a-z_]+$/)
|
306
310
|
e = "class Factbase::Term\nprivate\ndef #{fn}(fact, maps)\n#{@operands[1]}\nend\nend"
|
307
311
|
# rubocop:disable Security/Eval
|
308
312
|
eval(e)
|
@@ -310,6 +314,16 @@ class Factbase::Term
|
|
310
314
|
true
|
311
315
|
end
|
312
316
|
|
317
|
+
def undef(_fact, _maps)
|
318
|
+
assert_args(1)
|
319
|
+
fn = @operands[0]
|
320
|
+
raise "A symbol expected as first argument of 'undef'" unless fn.is_a?(Symbol)
|
321
|
+
if Factbase::Term.private_instance_methods(false).include?(fn)
|
322
|
+
Factbase::Term.class_eval("undef :#{fn}", __FILE__, __LINE__ - 1) # undef :foo
|
323
|
+
end
|
324
|
+
true
|
325
|
+
end
|
326
|
+
|
313
327
|
def min(_fact, maps)
|
314
328
|
@min ||= best(maps) { |v, b| v < b }
|
315
329
|
end
|
@@ -326,7 +340,7 @@ class Factbase::Term
|
|
326
340
|
@sum ||=
|
327
341
|
begin
|
328
342
|
k = @operands[0]
|
329
|
-
raise "A symbol expected, but
|
343
|
+
raise "A symbol expected, but '#{k}' provided" unless k.is_a?(Symbol)
|
330
344
|
sum = 0
|
331
345
|
maps.each do |m|
|
332
346
|
vv = m[k.to_s]
|
@@ -343,9 +357,9 @@ class Factbase::Term
|
|
343
357
|
def agg(_fact, maps)
|
344
358
|
assert_args(2)
|
345
359
|
selector = @operands[0]
|
346
|
-
raise "A term expected, but #{selector} provided" unless selector.is_a?(Factbase::Term)
|
360
|
+
raise "A term expected, but '#{selector}' provided" unless selector.is_a?(Factbase::Term)
|
347
361
|
term = @operands[1]
|
348
|
-
raise "A term expected, but #{term} provided" unless term.is_a?(Factbase::Term)
|
362
|
+
raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
349
363
|
subset = maps.select { |m| selector.evaluate(m, maps) }
|
350
364
|
@agg ||=
|
351
365
|
if subset.empty?
|
@@ -363,7 +377,7 @@ class Factbase::Term
|
|
363
377
|
|
364
378
|
def by_symbol(pos, fact)
|
365
379
|
o = @operands[pos]
|
366
|
-
raise "A symbol expected at ##{pos}, but
|
380
|
+
raise "A symbol expected at ##{pos}, but '#{o}' provided" unless o.is_a?(Symbol)
|
367
381
|
k = o.to_s
|
368
382
|
fact[k]
|
369
383
|
end
|
@@ -388,7 +402,7 @@ class Factbase::Term
|
|
388
402
|
|
389
403
|
def best(maps)
|
390
404
|
k = @operands[0]
|
391
|
-
raise "A symbol expected, but
|
405
|
+
raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
|
392
406
|
best = nil
|
393
407
|
maps.each do |m|
|
394
408
|
vv = m[k.to_s]
|
data/lib/factbase/to_json.rb
CHANGED
data/lib/factbase/to_xml.rb
CHANGED
data/lib/factbase/to_yaml.rb
CHANGED
data/lib/factbase/tuples.rb
CHANGED
@@ -22,8 +22,6 @@
|
|
22
22
|
|
23
23
|
require_relative '../factbase'
|
24
24
|
|
25
|
-
# Tuples.
|
26
|
-
#
|
27
25
|
# With the help of this class, it's possible to select a few facts
|
28
26
|
# from a factbase at a time, which depend on each other. For example,
|
29
27
|
# it's necessary to find a fact where the +name+ is set and then find
|
@@ -36,8 +34,9 @@ require_relative '../factbase'
|
|
36
34
|
# end
|
37
35
|
#
|
38
36
|
# Here, the +{f0.salary}+ is a special substitution place, which is replaced
|
39
|
-
# by the +salary+ of the fact that is found by the previous query.
|
40
|
-
#
|
37
|
+
# by the +salary+ of the fact that is found by the previous query.
|
38
|
+
#
|
39
|
+
# The indexing of queries starts from zero.
|
41
40
|
#
|
42
41
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
43
42
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
data/lib/factbase.rb
CHANGED
@@ -79,7 +79,7 @@ require 'yaml'
|
|
79
79
|
# License:: MIT
|
80
80
|
class Factbase
|
81
81
|
# Current version of the gem (changed by .rultor.yml on every release)
|
82
|
-
VERSION = '0.0.
|
82
|
+
VERSION = '0.0.42'
|
83
83
|
|
84
84
|
# An exception that may be thrown in a transaction, to roll it back.
|
85
85
|
class Rollback < StandardError; end
|
data/test/factbase/test_query.rb
CHANGED
@@ -79,6 +79,7 @@ class TestQuery < Minitest::Test
|
|
79
79
|
'(eq time (min time))' => 1,
|
80
80
|
'(and (absent time) (exists pi))' => 1,
|
81
81
|
"(and (exists time) (not (\t\texists pi)))" => 1,
|
82
|
+
'(undef something)' => 3,
|
82
83
|
"(or (eq num +66) (lt time #{(Time.now - 200).utc.iso8601}))" => 1
|
83
84
|
}.each do |q, r|
|
84
85
|
assert_equal(r, Factbase::Query.new(maps, Mutex.new, q).each.to_a.size, q)
|
data/test/factbase/test_rules.rb
CHANGED
@@ -42,4 +42,45 @@ class TestRules < Minitest::Test
|
|
42
42
|
f2.first = 1
|
43
43
|
end
|
44
44
|
end
|
45
|
+
|
46
|
+
def test_to_string
|
47
|
+
fb = Factbase::Rules.new(
|
48
|
+
Factbase.new,
|
49
|
+
'(when (exists a) (exists b))'
|
50
|
+
)
|
51
|
+
f = fb.insert
|
52
|
+
assert(f.to_s.length.positive?)
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_check_only_when_txn_is_closed
|
56
|
+
fb = Factbase::Rules.new(Factbase.new, '(when (exists a) (exists b))')
|
57
|
+
ok = false
|
58
|
+
assert_raises do
|
59
|
+
fb.txn do |fbt|
|
60
|
+
f = fbt.insert
|
61
|
+
f.a = 1
|
62
|
+
f.c = 2
|
63
|
+
ok = true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
assert(ok)
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_in_combination_with_pre
|
70
|
+
fb = Factbase::Rules.new(Factbase.new, '(when (exists a) (exists b))')
|
71
|
+
fb = Factbase::Pre.new(fb) do |f|
|
72
|
+
f.hello = 42
|
73
|
+
end
|
74
|
+
ok = false
|
75
|
+
assert_raises do
|
76
|
+
fb.txn do |fbt|
|
77
|
+
f = fbt.insert
|
78
|
+
f.a = 1
|
79
|
+
f.c = 2
|
80
|
+
ok = true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
assert(ok)
|
84
|
+
assert_equal(1, fb.query('(eq hello 42)').each.to_a.size)
|
85
|
+
end
|
45
86
|
end
|
data/test/factbase/test_term.rb
CHANGED
@@ -166,6 +166,34 @@ class TestTerm < Minitest::Test
|
|
166
166
|
assert_equal('(foo \'hello, world!\')', t1.evaluate(fact, []))
|
167
167
|
end
|
168
168
|
|
169
|
+
def test_defn_again_by_mistake
|
170
|
+
t = Factbase::Term.new(:defn, [:and, 'self.to_s'])
|
171
|
+
assert_raises do
|
172
|
+
t.evaluate(fact, [])
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_defn_bad_name_by_mistake
|
177
|
+
t = Factbase::Term.new(:defn, [:to_s, 'self.to_s'])
|
178
|
+
assert_raises do
|
179
|
+
t.evaluate(fact, [])
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_defn_bad_name_spelling_by_mistake
|
184
|
+
t = Factbase::Term.new(:defn, [:'some-key', 'self.to_s'])
|
185
|
+
assert_raises do
|
186
|
+
t.evaluate(fact, [])
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def test_undef_simple
|
191
|
+
t = Factbase::Term.new(:defn, [:hello, 'self.to_s'])
|
192
|
+
assert_equal(true, t.evaluate(fact, []))
|
193
|
+
t = Factbase::Term.new(:undef, [:hello])
|
194
|
+
assert_equal(true, t.evaluate(fact, []))
|
195
|
+
end
|
196
|
+
|
169
197
|
def test_past
|
170
198
|
t = Factbase::Term.new(:prev, [:foo])
|
171
199
|
assert_nil(t.evaluate(fact('foo' => 4), []))
|
@@ -39,4 +39,14 @@ class TestToJSON < Minitest::Test
|
|
39
39
|
json = JSON.parse(to.json)
|
40
40
|
assert(42, json[0]['foo'][1])
|
41
41
|
end
|
42
|
+
|
43
|
+
def test_sort_keys
|
44
|
+
fb = Factbase.new
|
45
|
+
f = fb.insert
|
46
|
+
f.c = 42
|
47
|
+
f.b = 1
|
48
|
+
f.a = 256
|
49
|
+
json = Factbase::ToJSON.new(fb).json
|
50
|
+
assert(json.include?('{"a":256,"b":1,"c":42}'), json)
|
51
|
+
end
|
42
52
|
end
|
@@ -73,4 +73,15 @@ class TestToXML < Minitest::Test
|
|
73
73
|
assert(!xml.xpath('/fb/f/f').empty?)
|
74
74
|
assert(!xml.xpath('/fb/f/class').empty?)
|
75
75
|
end
|
76
|
+
|
77
|
+
def test_sorts_keys
|
78
|
+
fb = Factbase.new
|
79
|
+
f = fb.insert
|
80
|
+
f.x = 20
|
81
|
+
f.t = 40
|
82
|
+
f.a = 10
|
83
|
+
f.c = 1
|
84
|
+
xml = Factbase::ToXML.new(fb).xml
|
85
|
+
assert(xml.gsub(/\s*/, '').include?('<f><a>10</a><c>1</c><t>40</t><x>20</x></f>'), xml)
|
86
|
+
end
|
76
87
|
end
|
@@ -42,4 +42,14 @@ class TestToYAML < Minitest::Test
|
|
42
42
|
assert_equal(42, yaml['facts'][0]['foo'][0])
|
43
43
|
assert_equal(256, yaml['facts'][0]['foo'][1])
|
44
44
|
end
|
45
|
+
|
46
|
+
def test_sorts_keys
|
47
|
+
fb = Factbase.new
|
48
|
+
f = fb.insert
|
49
|
+
f.b = 42
|
50
|
+
f.a = 256
|
51
|
+
f.c = 10
|
52
|
+
yaml = Factbase::ToYAML.new(fb).yaml
|
53
|
+
assert(yaml.include?("a: 256\n b: 42\n c: 10"), yaml)
|
54
|
+
end
|
45
55
|
end
|
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.42
|
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-06-
|
11
|
+
date: 2024-06-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|