factbase 0.0.39 → 0.0.41
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 +3 -3
- data/README.md +1 -0
- data/lib/factbase/fact.rb +7 -4
- data/lib/factbase/looged.rb +25 -12
- data/lib/factbase/query.rb +9 -2
- data/lib/factbase/term.rb +33 -6
- data/lib/factbase.rb +2 -1
- data/test/factbase/test_looged.rb +1 -1
- data/test/factbase/test_query.rb +3 -0
- data/test/factbase/test_term.rb +16 -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: 0ea2fe8670484d4eb3eac59caa947b547863a2106fa6ae771b8e9447b30573dc
|
4
|
+
data.tar.gz: b45d41378d4b61f0b3d8d01a3454819a4eca7724bd81adda7fe083c9594250ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40d6ca4cce2c4d64fe5e31b609c2fd15d05041358b8ae3be266c991f943ce242217378a8e83f96d8be83c50068fb65e04dff8c8c24708eeea8f79490e283c817
|
7
|
+
data.tar.gz: d09b19ac55775c511c0619b0a23be1260496f33d623927153e0b5b373b6919570a9f47104c0e2b1125299799f561e2b6fb39b2cb6ea5486b48ab76a05e15bfd1
|
data/Gemfile
CHANGED
@@ -26,7 +26,7 @@ gemspec
|
|
26
26
|
gem 'minitest', '5.23.1', require: false
|
27
27
|
gem 'rake', '13.2.1', require: false
|
28
28
|
gem 'rspec-rails', '6.1.2', require: false
|
29
|
-
gem 'rubocop', '1.64.
|
29
|
+
gem 'rubocop', '1.64.1', require: false
|
30
30
|
gem 'rubocop-performance', '1.21.0', require: false
|
31
31
|
gem 'rubocop-rspec', '2.29.2', require: false
|
32
32
|
gem 'simplecov', '0.22.0', require: false
|
data/Gemfile.lock
CHANGED
@@ -70,7 +70,7 @@ GEM
|
|
70
70
|
nokogiri (1.16.5-x86_64-linux)
|
71
71
|
racc (~> 1.4)
|
72
72
|
parallel (1.24.0)
|
73
|
-
parser (3.3.
|
73
|
+
parser (3.3.2.0)
|
74
74
|
ast (~> 2.4.1)
|
75
75
|
racc
|
76
76
|
psych (5.1.2)
|
@@ -125,7 +125,7 @@ GEM
|
|
125
125
|
rspec-mocks (~> 3.13)
|
126
126
|
rspec-support (~> 3.13)
|
127
127
|
rspec-support (3.13.1)
|
128
|
-
rubocop (1.64.
|
128
|
+
rubocop (1.64.1)
|
129
129
|
json (~> 2.3)
|
130
130
|
language_server-protocol (>= 3.17.0)
|
131
131
|
parallel (~> 1.10)
|
@@ -184,7 +184,7 @@ DEPENDENCIES
|
|
184
184
|
minitest (= 5.23.1)
|
185
185
|
rake (= 13.2.1)
|
186
186
|
rspec-rails (= 6.1.2)
|
187
|
-
rubocop (= 1.64.
|
187
|
+
rubocop (= 1.64.1)
|
188
188
|
rubocop-performance (= 1.21.0)
|
189
189
|
rubocop-rspec (= 2.29.2)
|
190
190
|
simplecov (= 0.22.0)
|
data/README.md
CHANGED
@@ -92,6 +92,7 @@ One term is for meta-programming:
|
|
92
92
|
There are terms that are history of search aware:
|
93
93
|
|
94
94
|
* `(prev a)` returns the value of `a` in the previously seen fact
|
95
|
+
* `(unique k)` returns true if the value of `k` property hasn't been seen yet
|
95
96
|
|
96
97
|
There are also terms that match the entire factbase
|
97
98
|
and must be used inside the `(agg ..)` term:
|
data/lib/factbase/fact.rb
CHANGED
@@ -24,17 +24,20 @@ require 'json'
|
|
24
24
|
require 'time'
|
25
25
|
require_relative '../factbase'
|
26
26
|
|
27
|
-
#
|
27
|
+
# A single fact in a factbase.
|
28
28
|
#
|
29
|
-
# This is an internal class, it is not supposed to be instantiated directly
|
30
|
-
#
|
31
|
-
#
|
29
|
+
# This is an internal class, it is not supposed to be instantiated directly,
|
30
|
+
# by the +Factbase+ class.
|
31
|
+
# However, it is possible to use it for testing directly, for example to make a
|
32
32
|
# fact with a single key/value pair inside:
|
33
33
|
#
|
34
34
|
# require 'factbase/fact'
|
35
35
|
# f = Factbase::Fact.new(Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
|
36
36
|
# assert_equal(42, f.foo)
|
37
37
|
#
|
38
|
+
# A fact is basically a key/value hash map, where values are non-empty
|
39
|
+
# sets of values.
|
40
|
+
#
|
38
41
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
39
42
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
40
43
|
# License:: MIT
|
data/lib/factbase/looged.rb
CHANGED
@@ -20,6 +20,7 @@
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
21
|
# SOFTWARE.
|
22
22
|
|
23
|
+
require 'time'
|
23
24
|
require 'loog'
|
24
25
|
|
25
26
|
# A decorator of a Factbase, that logs all operations.
|
@@ -114,39 +115,51 @@ class Factbase::Looged
|
|
114
115
|
def each(&)
|
115
116
|
q = Factbase::Syntax.new(@expr).to_term.to_s
|
116
117
|
if block_given?
|
117
|
-
r =
|
118
|
+
r = nil
|
119
|
+
tail = Factbase::Looged.elapsed do
|
120
|
+
r = @query.each(&)
|
121
|
+
end
|
118
122
|
raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
|
119
123
|
if r.zero?
|
120
|
-
@loog.debug("Nothing found by '#{q}'")
|
124
|
+
@loog.debug("Nothing found by '#{q}' #{tail}")
|
121
125
|
else
|
122
|
-
@loog.debug("Found #{r} fact(s) by '#{q}'")
|
126
|
+
@loog.debug("Found #{r} fact(s) by '#{q}' #{tail}")
|
123
127
|
end
|
124
128
|
r
|
125
129
|
else
|
126
130
|
array = []
|
127
|
-
|
128
|
-
|
129
|
-
|
131
|
+
tail = Factbase::Looged.elapsed do
|
132
|
+
@query.each do |f|
|
133
|
+
array << f
|
134
|
+
end
|
130
135
|
end
|
131
|
-
# rubocop:enable Style/MapIntoArray
|
132
136
|
if array.empty?
|
133
|
-
@loog.debug("Nothing found by '#{q}'")
|
137
|
+
@loog.debug("Nothing found by '#{q}' #{tail}")
|
134
138
|
else
|
135
|
-
@loog.debug("Found #{array.size} fact(s) by '#{q}'")
|
139
|
+
@loog.debug("Found #{array.size} fact(s) by '#{q}' #{tail}")
|
136
140
|
end
|
137
141
|
array
|
138
142
|
end
|
139
143
|
end
|
140
144
|
|
141
145
|
def delete!
|
142
|
-
r =
|
146
|
+
r = nil
|
147
|
+
tail = Factbase::Looged.elapsed do
|
148
|
+
r = @query.delete!
|
149
|
+
end
|
143
150
|
raise ".delete! of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
|
144
151
|
if r.zero?
|
145
|
-
@loog.debug("Nothing deleted by '#{@expr}'")
|
152
|
+
@loog.debug("Nothing deleted by '#{@expr}' #{tail}")
|
146
153
|
else
|
147
|
-
@loog.debug("Deleted #{r} fact(s) by '#{@expr}'")
|
154
|
+
@loog.debug("Deleted #{r} fact(s) by '#{@expr}' #{tail}")
|
148
155
|
end
|
149
156
|
r
|
150
157
|
end
|
151
158
|
end
|
159
|
+
|
160
|
+
def self.elapsed
|
161
|
+
start = Time.now
|
162
|
+
yield
|
163
|
+
"in #{format('%.2f', (Time.now - start) * 1000)}ms"
|
164
|
+
end
|
152
165
|
end
|
data/lib/factbase/query.rb
CHANGED
@@ -26,12 +26,17 @@ require_relative 'fact'
|
|
26
26
|
|
27
27
|
# Query.
|
28
28
|
#
|
29
|
-
# This is an internal class, it is not supposed to be instantiated directly.
|
29
|
+
# This is an internal class, it is not supposed to be instantiated directly. It
|
30
|
+
# is created by the +query()+ method of the +Factbase+ class.
|
30
31
|
#
|
31
32
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
32
33
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
33
34
|
# License:: MIT
|
34
35
|
class Factbase::Query
|
36
|
+
# Constructor.
|
37
|
+
# @param [Array<Fact>] maps Array of facts to start with
|
38
|
+
# @param [Mutex] mutex Mutex to sync all modifications to the +maps+
|
39
|
+
# @param [String] query The query as a string
|
35
40
|
def initialize(maps, mutex, query)
|
36
41
|
@maps = maps
|
37
42
|
@mutex = mutex
|
@@ -48,7 +53,9 @@ class Factbase::Query
|
|
48
53
|
@maps.each do |m|
|
49
54
|
f = Factbase::Fact.new(@mutex, m)
|
50
55
|
r = term.evaluate(f, @maps)
|
51
|
-
|
56
|
+
unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
|
57
|
+
raise "Unexpected evaluation result (#{r.class}), must be Boolean at #{@query}"
|
58
|
+
end
|
52
59
|
next unless r
|
53
60
|
yield f
|
54
61
|
yielded += 1
|
data/lib/factbase/term.rb
CHANGED
@@ -36,6 +36,15 @@ require_relative 'fact'
|
|
36
36
|
# t = Factbase::Term.new(:lt, [:foo, 50])
|
37
37
|
# assert(t.evaluate(f))
|
38
38
|
#
|
39
|
+
# The design of this class may look ugly, since it has a large number of
|
40
|
+
# methods, each of which corresponds to a different type of a +Term+. A much
|
41
|
+
# better design would definitely involve many classes, one per each type
|
42
|
+
# of a term. It's not done this way because of an experimental nature of
|
43
|
+
# the project. Most probably we should keep current design intact, since it
|
44
|
+
# works well and is rather simple to extend (by adding new term types).
|
45
|
+
# Moreover, it looks like the number of possible term types is rather limited
|
46
|
+
# and currently we implement most of them.
|
47
|
+
#
|
39
48
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
40
49
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
41
50
|
# License:: MIT
|
@@ -57,7 +66,9 @@ class Factbase::Term
|
|
57
66
|
def evaluate(fact, maps)
|
58
67
|
send(@op, fact, maps)
|
59
68
|
rescue NoMethodError => e
|
60
|
-
raise "Term '#{@op}' is not defined: #{e.message}"
|
69
|
+
raise "Term '#{@op}' is not defined at #{self}: #{e.message}"
|
70
|
+
rescue StandardError => e
|
71
|
+
raise "#{e.message} at #{self}"
|
61
72
|
end
|
62
73
|
|
63
74
|
# Simplify it if possible.
|
@@ -102,19 +113,19 @@ class Factbase::Term
|
|
102
113
|
|
103
114
|
def not(fact, maps)
|
104
115
|
assert_args(1)
|
105
|
-
!only_bool(the_values(0, fact, maps))
|
116
|
+
!only_bool(the_values(0, fact, maps), 0)
|
106
117
|
end
|
107
118
|
|
108
119
|
def or(fact, maps)
|
109
120
|
(0..@operands.size - 1).each do |i|
|
110
|
-
return true if only_bool(the_values(i, fact, maps))
|
121
|
+
return true if only_bool(the_values(i, fact, maps), 0)
|
111
122
|
end
|
112
123
|
false
|
113
124
|
end
|
114
125
|
|
115
126
|
def and(fact, maps)
|
116
127
|
(0..@operands.size - 1).each do |i|
|
117
|
-
return false unless only_bool(the_values(i, fact, maps))
|
128
|
+
return false unless only_bool(the_values(i, fact, maps), 0)
|
118
129
|
end
|
119
130
|
true
|
120
131
|
end
|
@@ -184,6 +195,19 @@ class Factbase::Term
|
|
184
195
|
before
|
185
196
|
end
|
186
197
|
|
198
|
+
def unique(fact, _maps)
|
199
|
+
@uniques = [] if @uniques.nil?
|
200
|
+
assert_args(1)
|
201
|
+
vv = by_symbol(0, fact)
|
202
|
+
return false if vv.nil?
|
203
|
+
vv = [vv] unless vv.is_a?(Array)
|
204
|
+
vv.each do |v|
|
205
|
+
return false if @uniques.include?(v)
|
206
|
+
@uniques << v
|
207
|
+
end
|
208
|
+
true
|
209
|
+
end
|
210
|
+
|
187
211
|
def many(fact, maps)
|
188
212
|
assert_args(1)
|
189
213
|
v = the_values(0, fact, maps)
|
@@ -317,6 +341,7 @@ class Factbase::Term
|
|
317
341
|
end
|
318
342
|
|
319
343
|
def agg(_fact, maps)
|
344
|
+
assert_args(2)
|
320
345
|
selector = @operands[0]
|
321
346
|
raise "A term expected, but #{selector} provided" unless selector.is_a?(Factbase::Term)
|
322
347
|
term = @operands[1]
|
@@ -352,10 +377,12 @@ class Factbase::Term
|
|
352
377
|
v
|
353
378
|
end
|
354
379
|
|
355
|
-
def only_bool(val)
|
380
|
+
def only_bool(val, pos)
|
356
381
|
val = val[0] if val.is_a?(Array)
|
357
382
|
return false if val.nil?
|
358
|
-
|
383
|
+
unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
384
|
+
raise "Boolean expected, while #{val.class} received from #{@operands[pos]}"
|
385
|
+
end
|
359
386
|
val
|
360
387
|
end
|
361
388
|
|
data/lib/factbase.rb
CHANGED
@@ -79,12 +79,13 @@ 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.41'
|
83
83
|
|
84
84
|
# An exception that may be thrown in a transaction, to roll it back.
|
85
85
|
class Rollback < StandardError; end
|
86
86
|
|
87
87
|
# Constructor.
|
88
|
+
# @param [Array<Hash>] facts Array of facts to start with
|
88
89
|
def initialize(facts = [])
|
89
90
|
@maps = facts
|
90
91
|
@mutex = Mutex.new
|
data/test/factbase/test_query.rb
CHANGED
@@ -57,6 +57,9 @@ class TestQuery < Minitest::Test
|
|
57
57
|
'(not (exists hello))' => 3,
|
58
58
|
'(eq "Integer" (type num))' => 2,
|
59
59
|
'(when (eq num 0) (exists time))' => 2,
|
60
|
+
'(unique num)' => 2,
|
61
|
+
'(unique name)' => 2,
|
62
|
+
'(unique pi)' => 1,
|
60
63
|
'(many num)' => 1,
|
61
64
|
'(one num)' => 2,
|
62
65
|
'(gt num (minus 1 (either (at 0 (prev num)) 0)))' => 3,
|
data/test/factbase/test_term.rb
CHANGED
@@ -183,6 +183,22 @@ class TestTerm < Minitest::Test
|
|
183
183
|
assert_equal([42], t.evaluate(fact('foo' => 4), []))
|
184
184
|
end
|
185
185
|
|
186
|
+
def test_report_missing_term
|
187
|
+
t = Factbase::Term.new(:something, [])
|
188
|
+
msg = assert_raises do
|
189
|
+
t.evaluate(fact, [])
|
190
|
+
end.message
|
191
|
+
assert(msg.include?('not defined at (something)'), msg)
|
192
|
+
end
|
193
|
+
|
194
|
+
def test_report_other_error
|
195
|
+
t = Factbase::Term.new(:at, [])
|
196
|
+
msg = assert_raises do
|
197
|
+
t.evaluate(fact, [])
|
198
|
+
end.message
|
199
|
+
assert(msg.include?('at (at)'), msg)
|
200
|
+
end
|
201
|
+
|
186
202
|
private
|
187
203
|
|
188
204
|
def fact(map = {})
|
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.41
|
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-
|
11
|
+
date: 2024-06-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|