factbase 0.0.30 → 0.0.32

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: 58a162c50ba7d2b9d21ca0ff2d62e34acd0c46d2666b596ceffcceb2dc4215c0
4
- data.tar.gz: 61514fa0fa904ea71540c0f9d4776ea75d5405aa5a1b73d32e051c29606b5fdb
3
+ metadata.gz: 53e563c36bf3d61f977bbae719a1b6069973a40b28f37ba4e3222f7fbae7091c
4
+ data.tar.gz: ecd5e6b589ceda68f8a25369e6d7c169500dab90ac6bfca26af5384d0e74bdc9
5
5
  SHA512:
6
- metadata.gz: 7b3841073dba72e3fcc6e1fd089235d93829264504447557b3b23f6eb3fa74a4ebccad0d27062af84dc00ab115ca06d9dcc9621bf245d54e09b121bb21983737
7
- data.tar.gz: aed653f93b7505db410e81d9fefd8fe9d38fc6b89947f89c952992bcdd76dab1d5f9e4a8cd941df426cb7b200855f26fb2981a9698ac35e8e6ea682c693787e7
6
+ metadata.gz: 7fc18cb91cd74af7e9e87b0740b10d1409278ce0dc8a7ea589ef8c45a5b61501dbe0706faf012a49862c911c25859d85a93ad1e6e275421af4c3255bcd5d1b2b
7
+ data.tar.gz: bf4d812092097c9edb40ccd4aeac447b96906bbdbb515696199f9fa229d8e620ba36b2c80435e1d75115c07e5f2bac6029215df0657d03d51ceb42d053f6652c
@@ -0,0 +1,57 @@
1
+ # Copyright (c) 2024 Yegor Bugayenko
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the 'Software'), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+ ---
21
+ name: license
22
+ 'on':
23
+ push:
24
+ branches:
25
+ - master
26
+ pull_request:
27
+ branches:
28
+ - master
29
+ jobs:
30
+ license:
31
+ runs-on: ubuntu-22.04
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - shell: bash
35
+ run: |
36
+ header="Copyright (c) $(date +%Y) Yegor Bugayenko"
37
+ failed="false"
38
+ while IFS= read -r file; do
39
+ if ! grep -q "${header}" "${file}"; then
40
+ failed="true"
41
+ echo "⚠️ Copyright header is not found in: ${file}"
42
+ else
43
+ echo "File looks good: ${file}"
44
+ fi
45
+ done < <(find . -type f \( \
46
+ -name "Dockerfile" -o \
47
+ -name "LICENSE.txt" -o \
48
+ -name "Makefile" -o \
49
+ -name "Rakefile" -o \
50
+ -name "*.sh" -o \
51
+ -name "*.rb" -o \
52
+ -name "*.fe" -o \
53
+ -name "*.yml" \
54
+ \) -print)
55
+ if [ "${failed}" = "true" ]; then
56
+ exit 1
57
+ fi
data/Gemfile CHANGED
@@ -23,10 +23,10 @@
23
23
  source 'https://rubygems.org'
24
24
  gemspec
25
25
 
26
- gem 'minitest', '5.23.0', require: false
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.63.5', require: false
29
+ gem 'rubocop', '1.64.0', 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
@@ -59,7 +59,7 @@ GEM
59
59
  crass (~> 1.0.2)
60
60
  nokogiri (>= 1.12.0)
61
61
  loog (0.5.1)
62
- minitest (5.23.0)
62
+ minitest (5.23.1)
63
63
  mutex_m (0.2.0)
64
64
  nokogiri (1.16.5-arm64-darwin)
65
65
  racc (~> 1.4)
@@ -75,7 +75,7 @@ GEM
75
75
  racc
76
76
  psych (5.1.2)
77
77
  stringio
78
- racc (1.7.3)
78
+ racc (1.8.0)
79
79
  rack (3.0.11)
80
80
  rack-session (2.0.0)
81
81
  rack (>= 3.0.0)
@@ -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.63.5)
128
+ rubocop (1.64.0)
129
129
  json (~> 2.3)
130
130
  language_server-protocol (>= 3.17.0)
131
131
  parallel (~> 1.10)
@@ -181,10 +181,10 @@ PLATFORMS
181
181
 
182
182
  DEPENDENCIES
183
183
  factbase!
184
- minitest (= 5.23.0)
184
+ minitest (= 5.23.1)
185
185
  rake (= 13.2.1)
186
186
  rspec-rails (= 6.1.2)
187
- rubocop (= 1.63.5)
187
+ rubocop (= 1.64.0)
188
188
  rubocop-performance (= 1.21.0)
189
189
  rubocop-rspec (= 2.29.2)
190
190
  simplecov (= 0.22.0)
data/README.md CHANGED
@@ -14,7 +14,8 @@
14
14
  This Ruby gem manages an in-memory database of facts.
15
15
  A fact is simply a map of properties and values.
16
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.
17
+ It is possible to delete a fact, but impossible to delete a property
18
+ from a fact.
18
19
 
19
20
  **ATTENTION**: The current implemention is naive and,
20
21
  because of that, very slow. I will be very happy
@@ -55,8 +56,7 @@ assert(f2.query('(eq foo 42)').each.to_a.size == 1)
55
56
 
56
57
  All terms available in a query:
57
58
 
58
- * `()` is true
59
- * `(nil)` is false
59
+ * `(always)` and `(never)` are "true" and "false"
60
60
  * `(not t)` inverses the `t` if it's boolean (exception otherwise)
61
61
  * `(or t1 t2 ...)` returns true if at least one argument is true
62
62
  * `(and t1 t2 ...)` returns true if all arguments are true
@@ -73,12 +73,14 @@ All terms available in a query:
73
73
 
74
74
  There are also terms that match the entire factbase:
75
75
 
76
- * `(max k)` returns true if the value of `k` property is the largest in the entire factbase
76
+ * `(max k)` returns true if the value of `k` property
77
+ is the largest in the entire factbase
77
78
  * `(min k)` returns true if the value of `k` is the smallest
78
79
 
79
80
  ## How to contribute
80
81
 
81
- Read [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
82
+ Read
83
+ [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
82
84
  Make sure you build is green before you contribute
83
85
  your pull request. You will need to have
84
86
  [Ruby](https://www.ruby-lang.org/en/) 3.2+ and
@@ -39,17 +39,26 @@ class Factbase::Syntax
39
39
  # Convert it to a term.
40
40
  # @return [Term] The term detected
41
41
  def to_term
42
+ build.simplify
43
+ rescue StandardError => e
44
+ raise "#{e.message} in \"#{@query}\""
45
+ end
46
+
47
+ private
48
+
49
+ # Convert it to a term.
50
+ # @return [Term] The term detected
51
+ def build
42
52
  @tokens ||= to_tokens
53
+ raise 'No tokens' if @tokens.empty?
43
54
  @ast ||= to_ast(@tokens, 0)
44
- raise "Too many terms: #{@query}" if @ast[1] != @tokens.size
55
+ raise 'Too many terms' if @ast[1] != @tokens.size
45
56
  term = @ast[0]
46
- raise "No terms found: #{@query}" if term.nil?
47
- raise "Not a term: #{@query}" unless term.is_a?(Factbase::Term)
57
+ raise 'No terms found' if term.nil?
58
+ raise 'Not a term' unless term.is_a?(Factbase::Term)
48
59
  term
49
60
  end
50
61
 
51
- private
52
-
53
62
  # Reads the stream of tokens, starting at the +at+ position. If the
54
63
  # token at the position is not a literal (like 42 or "Hello") but a term,
55
64
  # the function recursively calls itself.
@@ -58,19 +67,19 @@ class Factbase::Syntax
58
67
  # is the term/literal and the second one is the position where the
59
68
  # scanning should continue.
60
69
  def to_ast(tokens, at)
61
- raise "Closing too soon at ##{at}: #{@query}" if tokens[at] == :close
70
+ raise "Closing too soon at ##{at}" if tokens[at] == :close
62
71
  return [tokens[at], at + 1] unless tokens[at] == :open
63
72
  at += 1
64
73
  op = tokens[at]
65
- return [Factbase::Term.new(:nil, []), at + 1] if op == :close
74
+ raise 'No token found' if op == :close
66
75
  operands = []
67
76
  at += 1
68
77
  loop do
69
- raise "End of token stream at ##{at}: #{@query}" if tokens[at].nil?
78
+ raise "End of token stream at ##{at}" if tokens[at].nil?
70
79
  break if tokens[at] == :close
71
80
  (operand, at1) = to_ast(tokens, at)
72
- raise "Stuck at position ##{at}: #{@query}" if at == at1
73
- raise "Jump back at position ##{at}: #{@query}" if at1 < at
81
+ raise "Stuck at position ##{at}" if at == at1
82
+ raise "Jump back at position ##{at}" if at1 < at
74
83
  at = at1
75
84
  operands << operand
76
85
  break if tokens[at] == :close
@@ -110,12 +119,12 @@ class Factbase::Syntax
110
119
  acc += c
111
120
  end
112
121
  end
113
- raise "String not closed: : #{@query}" if string
122
+ raise 'String not closed' if string
114
123
  list.map do |t|
115
124
  if t.is_a?(Symbol)
116
125
  t
117
126
  elsif t.start_with?('\'', '"')
118
- raise "String literal can't be empty: #{@query}" if t.length <= 2
127
+ raise 'String literal can\'t be empty' if t.length <= 2
119
128
  t[1..-2]
120
129
  elsif t.match?(/^[0-9]+$/)
121
130
  t.to_i
@@ -124,7 +133,7 @@ class Factbase::Syntax
124
133
  elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
125
134
  Time.parse(t)
126
135
  else
127
- raise "Wrong symbol format (#{t}): #{@query}" unless t.match?(/^[a-z][a-zA-Z0-9_]*$/)
136
+ raise "Wrong symbol format (#{t})" unless t.match?(/^[a-z][a-zA-Z0-9_]*$/)
128
137
  t.to_sym
129
138
  end
130
139
  end
data/lib/factbase/term.rb CHANGED
@@ -43,6 +43,8 @@ class Factbase::Term
43
43
  # @return [bool] TRUE if matches
44
44
  def evaluate(fact)
45
45
  send(@op, fact)
46
+ rescue NoMethodError => _e
47
+ raise "Term '#{@op}' is not defined"
46
48
  end
47
49
 
48
50
  # Put it into the context: let it see the entire array of maps.
@@ -57,6 +59,17 @@ class Factbase::Term
57
59
  self
58
60
  end
59
61
 
62
+ # Simplify it if possible.
63
+ # @return [Factbase::Term] New term or itself
64
+ def simplify
65
+ m = "#{@op}_simplify"
66
+ if respond_to?(m, true)
67
+ send(m)
68
+ else
69
+ self
70
+ end
71
+ end
72
+
60
73
  # Turns it into a string.
61
74
  # @return [String] The string of it
62
75
  def to_s
@@ -76,11 +89,16 @@ class Factbase::Term
76
89
 
77
90
  private
78
91
 
79
- def nil(_fact)
92
+ def always(_fact)
80
93
  assert_args(0)
81
94
  true
82
95
  end
83
96
 
97
+ def never(_fact)
98
+ assert_args(0)
99
+ false
100
+ end
101
+
84
102
  def not(fact)
85
103
  assert_args(1)
86
104
  !only_bool(the_value(0, fact))
@@ -100,6 +118,28 @@ class Factbase::Term
100
118
  true
101
119
  end
102
120
 
121
+ def and_or_simplify
122
+ strs = []
123
+ ops = []
124
+ @operands.each do |o|
125
+ o = o.simplify
126
+ s = o.to_s
127
+ next if strs.include?(s)
128
+ strs << s
129
+ ops << o
130
+ end
131
+ return ops[0] if ops.size == 1
132
+ Factbase::Term.new(@op, ops)
133
+ end
134
+
135
+ def and_simplify
136
+ and_or_simplify
137
+ end
138
+
139
+ def or_simplify
140
+ and_or_simplify
141
+ end
142
+
103
143
  def when(fact)
104
144
  assert_args(2)
105
145
  a = @operands[0]
data/lib/factbase.rb CHANGED
@@ -29,7 +29,7 @@ require 'yaml'
29
29
  # License:: MIT
30
30
  class Factbase
31
31
  # Current version of the gem (changed by .rultor.yml on every release)
32
- VERSION = '0.0.30'
32
+ VERSION = '0.0.32'
33
33
 
34
34
  # Constructor.
35
35
  def initialize(facts = [])
@@ -42,7 +42,7 @@ class TestInv < Minitest::Test
42
42
  end
43
43
  f.c = 256
44
44
  assert_equal(42, f.a)
45
- assert_equal(1, fb.query('()').each.to_a.size)
45
+ assert_equal(1, fb.query('(always)').each.to_a.size)
46
46
  end
47
47
 
48
48
  def test_pre_and_inv
@@ -48,19 +48,19 @@ class TestLooged < Minitest::Test
48
48
  fb = Factbase.new
49
49
  fb.insert
50
50
  fb.insert
51
- assert_equal(2, Factbase::Looged.new(fb, Loog::NULL).query('()').each(&:to_s))
51
+ assert_equal(2, Factbase::Looged.new(fb, Loog::NULL).query('(always)').each(&:to_s))
52
52
  end
53
53
 
54
54
  def test_returns_int_when_empty
55
55
  fb = Factbase.new
56
- assert_equal(0, Factbase::Looged.new(fb, Loog::NULL).query('()').each(&:to_s))
56
+ assert_equal(0, Factbase::Looged.new(fb, Loog::NULL).query('(always)').each(&:to_s))
57
57
  end
58
58
 
59
59
  def test_logs_when_enumerator
60
60
  fb = Factbase::Looged.new(Factbase.new, Loog::NULL)
61
- assert_equal(0, fb.query('()').each.to_a.size)
61
+ assert_equal(0, fb.query('(always)').each.to_a.size)
62
62
  fb.insert
63
- assert_equal(1, fb.query('()').each.to_a.size)
63
+ assert_equal(1, fb.query('(always)').each.to_a.size)
64
64
  end
65
65
 
66
66
  def test_proper_logging
@@ -33,6 +33,6 @@ class TestPre < Minitest::Test
33
33
  fb = Factbase::Pre.new(Factbase.new) { |f| f.foo = 42 }
34
34
  f = fb.insert
35
35
  assert_equal(42, f.foo)
36
- assert_equal(1, fb.query('()').each.to_a.size)
36
+ assert_equal(1, fb.query('(always)').each.to_a.size)
37
37
  end
38
38
  end
@@ -90,6 +90,16 @@ class TestQuery < Minitest::Test
90
90
  assert_equal(1, maps.size)
91
91
  end
92
92
 
93
+ def test_deleting_nothing
94
+ maps = []
95
+ maps << { 'foo' => [42] }
96
+ maps << { 'bar' => [4, 5] }
97
+ maps << { 'bar' => 5 }
98
+ q = Factbase::Query.new(maps, Mutex.new, '(never)')
99
+ assert_equal(0, q.delete!)
100
+ assert_equal(3, maps.size)
101
+ end
102
+
93
103
  def test_to_array
94
104
  maps = []
95
105
  maps << { 'foo' => [42] }
@@ -41,13 +41,12 @@ class TestSyntax < Minitest::Test
41
41
 
42
42
  def test_simple_parsing
43
43
  [
44
- '()',
45
44
  '(foo)',
46
45
  '(foo (bar) (zz 77) )',
47
46
  "(eq foo \n\n 'Hello, world!'\n)\n",
48
47
  "(eq x 'Hello, \\' \n) \\' ( world!')",
49
48
  "# this is a comment\n(eq foo # test\n 42)\n\n# another comment\n",
50
- "(or ( a 4) (b 5) () (and () (c 5) \t\t(r 7 w8s w8is 'Foo')))"
49
+ "(or ( a 4) (b 5) (always) (and (always) (c 5) \t\t(r 7 w8s w8is 'Foo')))"
51
50
  ].each do |q|
52
51
  Factbase::Syntax.new(q).to_term
53
52
  end
@@ -67,7 +66,7 @@ class TestSyntax < Minitest::Test
67
66
  '(eq t 2024-05-25T19:43:48Z)',
68
67
  '(eq t 3.1415926)',
69
68
  '(eq t 3.0e+21)',
70
- "(foo (x (f (t (y 42 'Hey you'))) (f) (r 3)) y z)"
69
+ "(foo (x (f (t (y 42 'Hey you'))) (never) (r 3)) y z)"
71
70
  ].each do |q|
72
71
  assert_equal(q, Factbase::Syntax.new(q).to_term.to_s)
73
72
  end
@@ -91,6 +90,7 @@ class TestSyntax < Minitest::Test
91
90
  def test_broken_syntax
92
91
  [
93
92
  '',
93
+ '()',
94
94
  '(foo',
95
95
  '(foo 1) (bar 2)',
96
96
  'some text',
@@ -106,9 +106,21 @@ class TestSyntax < Minitest::Test
106
106
  ')',
107
107
  '"'
108
108
  ].each do |q|
109
- assert_raises(q) do
110
- Factbase::Syntax.new(q).to_term
111
- end
109
+ assert(
110
+ assert_raises(q) do
111
+ Factbase::Syntax.new(q).to_term
112
+ end.message.include?(q)
113
+ )
114
+ end
115
+ end
116
+
117
+ def test_simplification
118
+ {
119
+ '(foo)' => '(foo)',
120
+ '(and (foo) (foo))' => '(foo)',
121
+ '(and (foo) (or (and (eq a 1))) (eq a 1) (foo))' => '(and (foo) (eq a 1))'
122
+ }.each do |s, t|
123
+ assert_equal(t, Factbase::Syntax.new(s).to_term.to_s)
112
124
  end
113
125
  end
114
126
  end
@@ -82,8 +82,13 @@ class TestTerm < Minitest::Test
82
82
  assert(!t.evaluate(fact('bar' => [100])))
83
83
  end
84
84
 
85
+ def test_false_matching
86
+ t = Factbase::Term.new(:never, [])
87
+ assert(!t.evaluate(fact('foo' => [100])))
88
+ end
89
+
85
90
  def test_not_matching
86
- t = Factbase::Term.new(:not, [Factbase::Term.new(:nil, [])])
91
+ t = Factbase::Term.new(:not, [Factbase::Term.new(:always, [])])
87
92
  assert(!t.evaluate(fact('foo' => [100])))
88
93
  end
89
94
 
@@ -107,7 +107,7 @@ class TestFactbase < Minitest::Test
107
107
 
108
108
  def test_all_decorators
109
109
  [
110
- Factbase::Rules.new(Factbase.new, '()'),
110
+ Factbase::Rules.new(Factbase.new, '(always)'),
111
111
  Factbase::Inv.new(Factbase.new) { |_, _| true },
112
112
  Factbase::Pre.new(Factbase.new) { |_| true },
113
113
  Factbase::Looged.new(Factbase.new, Loog::NULL),
@@ -126,7 +126,7 @@ class TestFactbase < Minitest::Test
126
126
  end
127
127
  d.import(d.export)
128
128
  assert_equal(4, d.size)
129
- assert_equal(4, d.query('()').each.to_a.size)
129
+ assert_equal(4, d.query('(always)').each.to_a.size)
130
130
  end
131
131
  end
132
132
  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.30
4
+ version: 0.0.32
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-21 00:00:00.000000000 Z
11
+ date: 2024-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -78,6 +78,7 @@ files:
78
78
  - ".gitattributes"
79
79
  - ".github/workflows/actionlint.yml"
80
80
  - ".github/workflows/codecov.yml"
81
+ - ".github/workflows/license.yml"
81
82
  - ".github/workflows/markdown-lint.yml"
82
83
  - ".github/workflows/pdd.yml"
83
84
  - ".github/workflows/rake.yml"