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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ea2fe8670484d4eb3eac59caa947b547863a2106fa6ae771b8e9447b30573dc
4
- data.tar.gz: b45d41378d4b61f0b3d8d01a3454819a4eca7724bd81adda7fe083c9594250ef
3
+ metadata.gz: 42066d9d82538ecb6f0ec8df6cc711f4328e1ba496f00366fe67719ca8a1185e
4
+ data.tar.gz: d40e21b7e426af5184b9b03f515da4508b1e1e9e52995f566b16c778eefefca3
5
5
  SHA512:
6
- metadata.gz: 40d6ca4cce2c4d64fe5e31b609c2fd15d05041358b8ae3be266c991f943ce242217378a8e83f96d8be83c50068fb65e04dff8c8c24708eeea8f79490e283c817
7
- data.tar.gz: d09b19ac55775c511c0619b0a23be1260496f33d623927153e0b5b373b6919570a9f47104c0e2b1125299799f561e2b6fb39b2cb6ea5486b48ab76a05e15bfd1
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.29.2', require: false
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.29.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.29.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?(method, include_private = false)
84
+ def respond_to?(_method, _include_private = false)
85
85
  # rubocop:enable Style/OptionalBooleanParameter
86
- @fact.respond_to?(method, include_private)
86
+ true
87
87
  end
88
88
 
89
- def respond_to_missing?(method, include_private = false)
90
- @fact.respond_to_missing?(method, include_private)
89
+ def respond_to_missing?(_method, _include_private = false)
90
+ true
91
91
  end
92
92
  end
93
93
 
@@ -91,13 +91,13 @@ class Factbase::Looged
91
91
  end
92
92
 
93
93
  # rubocop:disable Style/OptionalBooleanParameter
94
- def respond_to?(method, include_private = false)
94
+ def respond_to?(_method, _include_private = false)
95
95
  # rubocop:enable Style/OptionalBooleanParameter
96
- @fact.respond_to?(method, include_private)
96
+ true
97
97
  end
98
98
 
99
- def respond_to_missing?(method, include_private = false)
100
- @fact.respond_to_missing?(method, include_private)
99
+ def respond_to_missing?(_method, _include_private = false)
100
+ true
101
101
  end
102
102
  end
103
103
 
@@ -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 = Check.new(fb, @rules)
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?(method, include_private = false)
93
+ def respond_to?(_method, _include_private = false)
87
94
  # rubocop:enable Style/OptionalBooleanParameter
88
- @fact.respond_to?(method, include_private)
95
+ true
89
96
  end
90
97
 
91
- def respond_to_missing?(method, include_private = false)
92
- @fact.respond_to_missing?(method, include_private)
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(fb, expr)
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
- raise "The fact is in invalid state: #{fact}"
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), 0)
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), 0)
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 'Too many values at first position, one expected' unless i.size == 1
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 'A symbol expected as first argument of defn' unless fn.is_a?(Symbol)
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 provided: #{k}" unless k.is_a?(Symbol)
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 provided: #{o}" unless o.is_a?(Symbol)
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 provided: #{k}" unless k.is_a?(Symbol)
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]
@@ -45,6 +45,6 @@ class Factbase::ToJSON
45
45
  # @return [String] The factbase in JSON format
46
46
  def json
47
47
  maps = Marshal.load(@fb.export)
48
- maps.to_json
48
+ maps.map { |m| m.sort.to_h }.to_json
49
49
  end
50
50
  end
@@ -55,7 +55,7 @@ class Factbase::ToXML
55
55
  xml.fb(meta) do
56
56
  maps.each do |m|
57
57
  xml.f_ do
58
- m.each do |k, vv|
58
+ m.sort.to_h.each do |k, vv|
59
59
  if vv.is_a?(Array)
60
60
  xml.send(:"#{k}_") do
61
61
  vv.each do |v|
@@ -45,6 +45,6 @@ class Factbase::ToYAML
45
45
  # @return [String] The factbase in YAML format
46
46
  def yaml
47
47
  maps = Marshal.load(@fb.export)
48
- YAML.dump({ 'facts' => maps })
48
+ YAML.dump({ 'facts' => maps.map { |m| m.sort.to_h } })
49
49
  end
50
50
  end
@@ -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. The indexing
40
- # of queries starts from zero.
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.41'
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
@@ -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)
@@ -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
@@ -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.41
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-03 00:00:00.000000000 Z
11
+ date: 2024-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json