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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ff1db55d5a25e5036d3a98423caedc960f8323681e2227c82e0a397727067c0
4
- data.tar.gz: b3000b708bd2eb69005998a722011a296384d5cea66641b975471e611b93e4d0
3
+ metadata.gz: 0ea2fe8670484d4eb3eac59caa947b547863a2106fa6ae771b8e9447b30573dc
4
+ data.tar.gz: b45d41378d4b61f0b3d8d01a3454819a4eca7724bd81adda7fe083c9594250ef
5
5
  SHA512:
6
- metadata.gz: a377a359a877b1f50c83f13c451e095036b4087cf88e39c04a0e5d64e86337bfef458883646009daff4b2b90446abbdbd563a4921b3ed275b88b7408b69ef0de
7
- data.tar.gz: 26a5ca088df353e0c526080213aa4d87b23805dd522617d52d2d738b1881c40ae4c659fa886db4fbe19a30a131ff6b5a7e19aadf5dbafcbd0f6b81954386bd65
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.0', require: false
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.1.0)
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.0)
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.0)
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
- # Fact.
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
- # It is possible to use for testing directly, for example to make a
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
@@ -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 = @query.each(&)
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
- # rubocop:disable Style/MapIntoArray
128
- @query.each do |f|
129
- array << f
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 = @query.delete!
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
@@ -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
- raise 'Unexpected evaluation result, must be boolean' unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
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
- raise "Boolean expected, while #{val.class} received" unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
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.39'
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
@@ -94,7 +94,7 @@ class TestLooged < Minitest::Test
94
94
  'Found 1 fact(s) by \'(exists bar)\'',
95
95
  'Deleted 3 fact(s) by \'(not (exists bar))\''
96
96
  ].each do |s|
97
- assert(log.to_s.include?("#{s}\n"), "#{log}\n")
97
+ assert(log.to_s.include?(s), "#{log}\n")
98
98
  end
99
99
  end
100
100
  end
@@ -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,
@@ -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.39
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-05-30 00:00:00.000000000 Z
11
+ date: 2024-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json