factbase 0.0.28 → 0.0.30

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: ffdf62af221a65ab9efa6097b01dab23edba0399bc8551757d450ff828f14367
4
- data.tar.gz: 175dd8ead3a933305d46c8c4bb657a46efc507db3ad1045bb7495614dedfc1c2
3
+ metadata.gz: 58a162c50ba7d2b9d21ca0ff2d62e34acd0c46d2666b596ceffcceb2dc4215c0
4
+ data.tar.gz: 61514fa0fa904ea71540c0f9d4776ea75d5405aa5a1b73d32e051c29606b5fdb
5
5
  SHA512:
6
- metadata.gz: ccade318b2db8568b72fcd29dd8d8cc301a791911a749bba8154485e8e95d1a00c2466477ff7e798f8a5146e87e03c69af8493fd05ae38c9463a8ffc9e29da62
7
- data.tar.gz: 30730a5e848923b73c98f188142f30f56fd8eec95d8839561a6be01956922f60d9fe46a9dcf361b2f6b4bffca993fd20471bd51f1e2840659ba7bc252c9853b2
6
+ metadata.gz: 7b3841073dba72e3fcc6e1fd089235d93829264504447557b3b23f6eb3fa74a4ebccad0d27062af84dc00ab115ca06d9dcc9621bf245d54e09b121bb21983737
7
+ data.tar.gz: aed653f93b7505db410e81d9fefd8fe9d38fc6b89947f89c952992bcdd76dab1d5f9e4a8cd941df426cb7b200855f26fb2981a9698ac35e8e6ea682c693787e7
data/.rubocop.yml CHANGED
@@ -41,9 +41,9 @@ Metrics/AbcSize:
41
41
  Metrics/BlockLength:
42
42
  Max: 30
43
43
  Metrics/CyclomaticComplexity:
44
- Max: 20
44
+ Max: 25
45
45
  Metrics/PerceivedComplexity:
46
- Max: 20
46
+ Max: 25
47
47
  Metrics/ClassLength:
48
48
  Enabled: false
49
49
  Layout/EmptyLineAfterGuardClause:
data/README.md CHANGED
@@ -12,15 +12,26 @@
12
12
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/factbase/blob/master/LICENSE.txt)
13
13
 
14
14
  This Ruby gem manages an in-memory database of facts.
15
+ A fact is simply a map of properties and values.
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.
18
+
19
+ **ATTENTION**: The current implemention is naive and,
20
+ because of that, very slow. I will be very happy
21
+ if you suggest a better implementation without the change of the interface.
22
+ The `Factbase::query()` method is what mostly needs performance optimization:
23
+ currently it simply iterates through all facts in the factbase in order
24
+ to find those that match the provided terms. Obviously,
25
+ even a simple indexing may significantly increase performance.
15
26
 
16
27
  Here is how you use it (it's thread-safe, by the way):
17
28
 
18
29
  ```ruby
19
30
  fb = Factbase.new
20
31
  f = fb.insert
21
- f.type = 'book'
32
+ f.kind = 'book'
22
33
  f.title = 'Object Thinking'
23
- fb.query('(eq type "book")').each do |f|
34
+ fb.query('(eq kind "book")').each do |f|
24
35
  f.seen = true
25
36
  end
26
37
  fb.insert
@@ -29,17 +40,42 @@ fb.query('(not (exists seen))').each do |f|
29
40
  end
30
41
  ```
31
42
 
32
- You can save the factbase to disc and load it back:
43
+ You can save the factbase to the disc and then load it back:
33
44
 
34
45
  ```ruby
35
46
  file = '/tmp/simple.fb'
36
47
  f1 = Factbase.new
37
- f1.insert
48
+ f = f1.insert
49
+ f.foo = 42
38
50
  File.save(file, f1.export)
39
51
  f2 = Factbase.new
40
52
  f2.import(File.read(file))
53
+ assert(f2.query('(eq foo 42)').each.to_a.size == 1)
41
54
  ```
42
55
 
56
+ All terms available in a query:
57
+
58
+ * `()` is true
59
+ * `(nil)` is false
60
+ * `(not t)` inverses the `t` if it's boolean (exception otherwise)
61
+ * `(or t1 t2 ...)` returns true if at least one argument is true
62
+ * `(and t1 t2 ...)` returns true if all arguments are true
63
+ * `(when t1 t2)` returns true if `t1` is true and `t2` is true or `t1` is false
64
+ * `(exists k)` returns true if `k` property exists in the fact
65
+ * `(absent k)` returns true if `k` property is absent
66
+ * `(eq a b)` returns true if `a` equals to `b`
67
+ * `(lt a b)` returns true if `a` is less than `b`
68
+ * `(gt a b)` returns true if `a` is greater than `b`
69
+ * `(size k)` returns cardinality of `k` property (zero if property is absent)
70
+ * `(type a)` returns type of `a` ("String", "Integer", "Float", or "Time")
71
+ * `(matches a re)` returns true when `a` matches regular expression `re`
72
+ * `(defn foo "self.to_s")` defines a new term using Ruby syntax and returns true
73
+
74
+ There are also terms that match the entire factbase:
75
+
76
+ * `(max k)` returns true if the value of `k` property is the largest in the entire factbase
77
+ * `(min k)` returns true if the value of `k` is the smallest
78
+
43
79
  ## How to contribute
44
80
 
45
81
  Read [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
data/lib/factbase/fact.rb CHANGED
@@ -61,6 +61,7 @@ class Factbase::Fact
61
61
  end
62
62
  @map[kk] = [@map[kk]] unless @map[kk].is_a?(Array)
63
63
  @map[kk] << v
64
+ @map[kk].uniq!
64
65
  end
65
66
  nil
66
67
  elsif k == '[]'
@@ -69,7 +70,7 @@ class Factbase::Fact
69
70
  v = @map[k]
70
71
  if v.nil?
71
72
  raise "Can't get '#{k}', the fact is empty" if @map.empty?
72
- raise "Can't find '#{k}' attribute in [#{@map.keys.join(', ')}]"
73
+ raise "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]"
73
74
  end
74
75
  v.is_a?(Array) ? v[0] : v
75
76
  end
@@ -42,7 +42,7 @@ class Factbase::Looged
42
42
 
43
43
  def insert
44
44
  f = @fb.insert
45
- @loog.debug('Inserted new fact')
45
+ @loog.debug("Inserted new fact ##{@fb.size}")
46
46
  Fact.new(f, @loog)
47
47
  end
48
48
 
@@ -79,6 +79,7 @@ class Factbase::Looged
79
79
  v = args[1]
80
80
  s = v.is_a?(Time) ? v.utc.iso8601 : v.to_s
81
81
  s = v.to_s.inspect if v.is_a?(String)
82
+ s = "#{s[0..40]}...#{s[-40..]}" if s.length > 80
82
83
  @loog.debug("Set '#{k[0..-2]}' to #{s} (#{v.class})") if k.end_with?('=')
83
84
  r
84
85
  end
@@ -103,13 +104,14 @@ class Factbase::Looged
103
104
  end
104
105
 
105
106
  def each(&)
107
+ q = Factbase::Syntax.new(@expr).to_term.to_s
106
108
  if block_given?
107
109
  r = @query.each(&)
108
110
  raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
109
111
  if r.zero?
110
- @loog.debug("Nothing found by '#{@expr}'")
112
+ @loog.debug("Nothing found by '#{q}'")
111
113
  else
112
- @loog.debug("Found #{r} fact(s) by '#{@expr}'")
114
+ @loog.debug("Found #{r} fact(s) by '#{q}'")
113
115
  end
114
116
  r
115
117
  else
@@ -120,9 +122,9 @@ class Factbase::Looged
120
122
  end
121
123
  # rubocop:enable Style/MapIntoArray
122
124
  if array.empty?
123
- @loog.debug("Nothing found by '#{@expr}'")
125
+ @loog.debug("Nothing found by '#{q}'")
124
126
  else
125
- @loog.debug("Found #{array.size} fact(s) by '#{@expr}'")
127
+ @loog.debug("Found #{array.size} fact(s) by '#{q}'")
126
128
  end
127
129
  array
128
130
  end
@@ -40,11 +40,11 @@ class Factbase::Query
40
40
  # @return [Integer] Total number of facts yielded
41
41
  def each
42
42
  return to_enum(__method__) unless block_given?
43
- term = Factbase::Syntax.new(@query).to_term
43
+ term = Factbase::Syntax.new(@query).to_term.on(@maps)
44
44
  yielded = 0
45
45
  @maps.each do |m|
46
46
  f = Factbase::Fact.new(@mutex, m)
47
- next unless term.eval(f)
47
+ next unless term.evaluate(f)
48
48
  yield f
49
49
  yielded += 1
50
50
  end
@@ -59,7 +59,7 @@ class Factbase::Query
59
59
  @mutex.synchronize do
60
60
  @maps.delete_if do |m|
61
61
  f = Factbase::Fact.new(@mutex, m)
62
- if term.eval(f)
62
+ if term.evaluate(f)
63
63
  deleted += 1
64
64
  true
65
65
  else
@@ -117,7 +117,7 @@ class Factbase::Rules
117
117
  end
118
118
 
119
119
  def it(fact)
120
- return if Factbase::Syntax.new(@expr).to_term.eval(fact)
120
+ return if Factbase::Syntax.new(@expr).to_term.evaluate(fact)
121
121
  raise "The fact is in invalid state: #{fact}"
122
122
  end
123
123
  end
@@ -41,9 +41,10 @@ class Factbase::Syntax
41
41
  def to_term
42
42
  @tokens ||= to_tokens
43
43
  @ast ||= to_ast(@tokens, 0)
44
+ raise "Too many terms: #{@query}" if @ast[1] != @tokens.size
44
45
  term = @ast[0]
45
- raise 'No terms found' if term.nil?
46
- raise 'Not a term' unless term.is_a?(Factbase::Term)
46
+ raise "No terms found: #{@query}" if term.nil?
47
+ raise "Not a term: #{@query}" unless term.is_a?(Factbase::Term)
47
48
  term
48
49
  end
49
50
 
@@ -57,7 +58,7 @@ class Factbase::Syntax
57
58
  # is the term/literal and the second one is the position where the
58
59
  # scanning should continue.
59
60
  def to_ast(tokens, at)
60
- raise "Closing too soon at ##{at}" if tokens[at] == :close
61
+ raise "Closing too soon at ##{at}: #{@query}" if tokens[at] == :close
61
62
  return [tokens[at], at + 1] unless tokens[at] == :open
62
63
  at += 1
63
64
  op = tokens[at]
@@ -65,11 +66,11 @@ class Factbase::Syntax
65
66
  operands = []
66
67
  at += 1
67
68
  loop do
68
- raise "End of token stream at ##{at}" if tokens[at].nil?
69
+ raise "End of token stream at ##{at}: #{@query}" if tokens[at].nil?
69
70
  break if tokens[at] == :close
70
71
  (operand, at1) = to_ast(tokens, at)
71
- raise "Stuck at position ##{at}" if at == at1
72
- raise "Jump back at position ##{at}" if at1 < at
72
+ raise "Stuck at position ##{at}: #{@query}" if at == at1
73
+ raise "Jump back at position ##{at}: #{@query}" if at1 < at
73
74
  at = at1
74
75
  operands << operand
75
76
  break if tokens[at] == :close
@@ -109,12 +110,12 @@ class Factbase::Syntax
109
110
  acc += c
110
111
  end
111
112
  end
112
- raise 'String not closed' if string
113
+ raise "String not closed: : #{@query}" if string
113
114
  list.map do |t|
114
115
  if t.is_a?(Symbol)
115
116
  t
116
117
  elsif t.start_with?('\'', '"')
117
- raise 'String literal can\'t be empty' if t.length <= 2
118
+ raise "String literal can't be empty: #{@query}" if t.length <= 2
118
119
  t[1..-2]
119
120
  elsif t.match?(/^[0-9]+$/)
120
121
  t.to_i
@@ -123,6 +124,7 @@ class Factbase::Syntax
123
124
  elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
124
125
  Time.parse(t)
125
126
  else
127
+ raise "Wrong symbol format (#{t}): #{@query}" unless t.match?(/^[a-z][a-zA-Z0-9_]*$/)
126
128
  t.to_sym
127
129
  end
128
130
  end
data/lib/factbase/term.rb CHANGED
@@ -41,10 +41,22 @@ class Factbase::Term
41
41
  # Does it match the fact?
42
42
  # @param [Factbase::Fact] fact The fact
43
43
  # @return [bool] TRUE if matches
44
- def eval(fact)
44
+ def evaluate(fact)
45
45
  send(@op, fact)
46
46
  end
47
47
 
48
+ # Put it into the context: let it see the entire array of maps.
49
+ # @param [Array] maps The maps
50
+ # @return [Factbase::Term] Itself
51
+ def on(maps)
52
+ m = "#{@op}_on"
53
+ send(m, maps) if respond_to?(m, true)
54
+ @operands.each do |o|
55
+ o.on(maps) if o.is_a?(Factbase::Term)
56
+ end
57
+ self
58
+ end
59
+
48
60
  # Turns it into a string.
49
61
  # @return [String] The string of it
50
62
  def to_s
@@ -71,19 +83,19 @@ class Factbase::Term
71
83
 
72
84
  def not(fact)
73
85
  assert_args(1)
74
- !@operands[0].eval(fact)
86
+ !only_bool(the_value(0, fact))
75
87
  end
76
88
 
77
89
  def or(fact)
78
- @operands.each do |o|
79
- return true if o.eval(fact)
90
+ (0..@operands.size - 1).each do |i|
91
+ return true if only_bool(the_value(i, fact))
80
92
  end
81
93
  false
82
94
  end
83
95
 
84
96
  def and(fact)
85
- @operands.each do |o|
86
- return false unless o.eval(fact)
97
+ (0..@operands.size - 1).each do |i|
98
+ return false unless only_bool(the_value(i, fact))
87
99
  end
88
100
  true
89
101
  end
@@ -92,7 +104,7 @@ class Factbase::Term
92
104
  assert_args(2)
93
105
  a = @operands[0]
94
106
  b = @operands[1]
95
- !a.eval(fact) || (a.eval(fact) && b.eval(fact))
107
+ !a.evaluate(fact) || (a.evaluate(fact) && b.evaluate(fact))
96
108
  end
97
109
 
98
110
  def exists(fact)
@@ -132,6 +144,17 @@ class Factbase::Term
132
144
  v.class.to_s
133
145
  end
134
146
 
147
+ def matches(fact)
148
+ assert_args(2)
149
+ str = the_value(0, fact)
150
+ raise 'String is nil' if str.nil?
151
+ raise 'Exactly one string expected' unless str.size == 1
152
+ re = the_value(1, fact)
153
+ raise 'Regexp is nil' if re.nil?
154
+ raise 'Exactly one regexp expected' unless re.size == 1
155
+ str[0].to_s.match?(re[0])
156
+ end
157
+
135
158
  def arithmetic(op, fact)
136
159
  assert_args(2)
137
160
  lefts = the_value(0, fact)
@@ -147,6 +170,36 @@ class Factbase::Term
147
170
  end
148
171
  end
149
172
 
173
+ def defn(_fact)
174
+ fn = @operands[0]
175
+ raise 'A symbol expected as first argument of defn' unless fn.is_a?(Symbol)
176
+ e = "class Factbase::Term\nprivate\ndef #{fn}(fact)\n#{@operands[1]}\nend\nend"
177
+ # rubocop:disable Security/Eval
178
+ eval(e)
179
+ # rubocop:enable Security/Eval
180
+ true
181
+ end
182
+
183
+ def min(fact)
184
+ vv = the_value(0, fact)
185
+ return nil if vv.nil?
186
+ vv.any? { |v| v == @min }
187
+ end
188
+
189
+ def max(fact)
190
+ vv = the_value(0, fact)
191
+ return nil if vv.nil?
192
+ vv.any? { |v| v == @max }
193
+ end
194
+
195
+ def max_on(maps)
196
+ @max = best(maps) { |v, b| v > b }
197
+ end
198
+
199
+ def min_on(maps)
200
+ @min = best(maps) { |v, b| v < b }
201
+ end
202
+
150
203
  def assert_args(num)
151
204
  c = @operands.size
152
205
  raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
@@ -162,10 +215,32 @@ class Factbase::Term
162
215
 
163
216
  def the_value(pos, fact)
164
217
  v = @operands[pos]
165
- v = v.eval(fact) if v.is_a?(Factbase::Term)
218
+ v = v.evaluate(fact) if v.is_a?(Factbase::Term)
166
219
  v = fact[v.to_s] if v.is_a?(Symbol)
167
220
  return v if v.nil?
168
221
  v = [v] unless v.is_a?(Array)
169
222
  v
170
223
  end
224
+
225
+ def only_bool(val)
226
+ val = val[0] if val.is_a?(Array)
227
+ return false if val.nil?
228
+ raise "Boolean expected, while #{val.class} received" unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
229
+ val
230
+ end
231
+
232
+ def best(maps)
233
+ k = @operands[0]
234
+ raise "A symbol expected, but provided: #{k}" unless k.is_a?(Symbol)
235
+ best = nil
236
+ maps.each do |m|
237
+ vv = m[k.to_s]
238
+ next if vv.nil?
239
+ vv = [vv] unless vv.is_a?(Array)
240
+ vv.each do |v|
241
+ best = v if best.nil? || yield(v, best)
242
+ end
243
+ end
244
+ best
245
+ end
171
246
  end
@@ -37,11 +37,13 @@ class Factbase::ToXML
37
37
  # Convert the entire factbase into XML.
38
38
  # @return [String] The factbase in XML format
39
39
  def xml
40
+ bytes = @fb.export
41
+ maps = Marshal.load(bytes)
40
42
  meta = {
41
- factbase_version: Factbase::VERSION,
42
- dob: Time.now.utc.iso8601
43
+ version: Factbase::VERSION,
44
+ dob: Time.now.utc.iso8601,
45
+ size: bytes.size
43
46
  }
44
- maps = Marshal.load(@fb.export)
45
47
  Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
46
48
  xml.fb(meta) do
47
49
  maps.each do |m|
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.28'
32
+ VERSION = '0.0.30'
33
33
 
34
34
  # Constructor.
35
35
  def initialize(facts = [])
@@ -39,6 +39,16 @@ class TestFact < Minitest::Test
39
39
  assert_equal([42, 256], f['foo'], f.to_s)
40
40
  end
41
41
 
42
+ def test_keeps_values_unique
43
+ map = {}
44
+ f = Factbase::Fact.new(Mutex.new, map)
45
+ f.foo = 42
46
+ f.foo = 'Hello'
47
+ assert_equal(2, map['foo'].size)
48
+ f.foo = 42
49
+ assert_equal(2, map['foo'].size)
50
+ end
51
+
42
52
  def test_fails_when_empty
43
53
  f = Factbase::Fact.new(Mutex.new, {})
44
54
  assert_raises do
@@ -74,6 +84,14 @@ class TestFact < Minitest::Test
74
84
  assert_equal(42, f.foo_bar, f.to_s)
75
85
  end
76
86
 
87
+ def test_set_twice_same_value
88
+ map = {}
89
+ f = Factbase::Fact.new(Mutex.new, map)
90
+ f.foo = 42
91
+ f.foo = 42
92
+ assert_equal(42, map['foo'])
93
+ end
94
+
77
95
  def test_time_in_utc
78
96
  f = Factbase::Fact.new(Mutex.new, {})
79
97
  t = Time.now
@@ -69,13 +69,30 @@ class TestLooged < Minitest::Test
69
69
  fb.insert
70
70
  fb.insert.bar = 3
71
71
  fb.insert
72
+ fb.insert.str =
73
+ "Он поскорей звонит. Вбегает
74
+ К нему слуга француз Гильо,
75
+ Халат и туфли предлагает
76
+ И подает ему белье.
77
+ Спешит Онегин одеваться,
78
+ Слуге велит приготовляться
79
+ С ним вместе ехать и с собой
80
+ Взять также ящик боевой.
81
+ Готовы санки беговые.
82
+ Он сел, на мельницу летит.
83
+ Примчались. Он слуге велит
84
+ Лепажа стволы роковые
85
+ Нести за ним, а лошадям
86
+ Отъехать в поле к двум дубкам."
72
87
  fb.query('(exists bar)').each(&:to_s)
73
88
  fb.query('(not (exists bar))').delete!
74
89
  [
75
- 'Inserted new fact',
90
+ 'Inserted new fact #1',
91
+ 'Inserted new fact #2',
76
92
  'Set \'bar\' to 3 (Integer)',
93
+ 'Set \'str\' to "Он поскорей звонит. Вбегает\n К нем...м\n Отъехать в поле к двум дубкам." (String)',
77
94
  'Found 1 fact(s) by \'(exists bar)\'',
78
- 'Deleted 2 fact(s) by \'(not (exists bar))\''
95
+ 'Deleted 3 fact(s) by \'(not (exists bar))\''
79
96
  ].each do |s|
80
97
  assert(log.to_s.include?("#{s}\n"), "#{log}\n")
81
98
  end
@@ -60,6 +60,10 @@ class TestQuery < Minitest::Test
60
60
  '(eq (size hello) 0)' => 3,
61
61
  '(eq num pi)' => 0,
62
62
  '(absent time)' => 2,
63
+ '(max num)' => 1,
64
+ '(and (exists time) (max num))' => 0,
65
+ '(and (exists pi) (max num))' => 1,
66
+ '(min time)' => 1,
63
67
  '(and (absent time) (exists pi))' => 1,
64
68
  "(and (exists time) (not (\t\texists pi)))" => 1,
65
69
  "(or (eq num 66) (lt time #{(Time.now - 200).utc.iso8601}))" => 1
@@ -45,8 +45,9 @@ class TestSyntax < Minitest::Test
45
45
  '(foo)',
46
46
  '(foo (bar) (zz 77) )',
47
47
  "(eq foo \n\n 'Hello, world!'\n)\n",
48
+ "(eq x 'Hello, \\' \n) \\' ( world!')",
48
49
  "# this is a comment\n(eq foo # test\n 42)\n\n# another comment\n",
49
- "(or ( a 4) (b 5) () (and () (c 5) \t\t(r 7 8s 8is 'Foo')))"
50
+ "(or ( a 4) (b 5) () (and () (c 5) \t\t(r 7 w8s w8is 'Foo')))"
50
51
  ].each do |q|
51
52
  Factbase::Syntax.new(q).to_term
52
53
  end
@@ -83,7 +84,7 @@ class TestSyntax < Minitest::Test
83
84
  '(or (eq bar 888) (eq z 1))' => true,
84
85
  "(or (gt bar 100) (eq foo 'Hello, world!'))" => true
85
86
  }.each do |k, v|
86
- assert_equal(v, Factbase::Syntax.new(k).to_term.eval(m), k)
87
+ assert_equal(v, Factbase::Syntax.new(k).to_term.evaluate(m), k)
87
88
  end
88
89
  end
89
90
 
@@ -91,11 +92,13 @@ class TestSyntax < Minitest::Test
91
92
  [
92
93
  '',
93
94
  '(foo',
95
+ '(foo 1) (bar 2)',
94
96
  'some text',
95
97
  '"hello, world!',
96
98
  '(foo 7',
97
99
  "(foo 7 'Dude'",
98
100
  '(foo x y z (',
101
+ '(bad-term-name 42)',
99
102
  '(foo x y (z t (f 42 ',
100
103
  ')foo ) y z)',
101
104
  '(x "")',
@@ -103,7 +106,7 @@ class TestSyntax < Minitest::Test
103
106
  ')',
104
107
  '"'
105
108
  ].each do |q|
106
- assert_raises do
109
+ assert_raises(q) do
107
110
  Factbase::Syntax.new(q).to_term
108
111
  end
109
112
  end
@@ -30,95 +30,102 @@ require_relative '../../lib/factbase/term'
30
30
  class TestTerm < Minitest::Test
31
31
  def test_simple_matching
32
32
  t = Factbase::Term.new(:eq, [:foo, 42])
33
- assert(t.eval(fact('foo' => [42])))
34
- assert(!t.eval(fact('foo' => 'Hello!')))
35
- assert(!t.eval(fact('bar' => ['Hello!'])))
33
+ assert(t.evaluate(fact('foo' => [42])))
34
+ assert(!t.evaluate(fact('foo' => 'Hello!')))
35
+ assert(!t.evaluate(fact('bar' => ['Hello!'])))
36
36
  end
37
37
 
38
38
  def test_eq_matching
39
39
  t = Factbase::Term.new(:eq, [:foo, 42])
40
- assert(t.eval(fact('foo' => 42)))
41
- assert(t.eval(fact('foo' => [10, 5, 6, -8, 'hey', 42, 9, 'fdsf'])))
42
- assert(!t.eval(fact('foo' => [100])))
43
- assert(!t.eval(fact('foo' => [])))
44
- assert(!t.eval(fact('bar' => [])))
40
+ assert(t.evaluate(fact('foo' => 42)))
41
+ assert(t.evaluate(fact('foo' => [10, 5, 6, -8, 'hey', 42, 9, 'fdsf'])))
42
+ assert(!t.evaluate(fact('foo' => [100])))
43
+ assert(!t.evaluate(fact('foo' => [])))
44
+ assert(!t.evaluate(fact('bar' => [])))
45
45
  end
46
46
 
47
47
  def test_eq_matching_time
48
48
  now = Time.now
49
49
  t = Factbase::Term.new(:eq, [:foo, Time.parse(now.iso8601)])
50
- assert(t.eval(fact('foo' => now)))
51
- assert(t.eval(fact('foo' => [now, Time.now])))
50
+ assert(t.evaluate(fact('foo' => now)))
51
+ assert(t.evaluate(fact('foo' => [now, Time.now])))
52
52
  end
53
53
 
54
54
  def test_lt_matching
55
55
  t = Factbase::Term.new(:lt, [:foo, 42])
56
- assert(t.eval(fact('foo' => [10])))
57
- assert(!t.eval(fact('foo' => [100])))
58
- assert(!t.eval(fact('foo' => 100)))
59
- assert(!t.eval(fact('bar' => 100)))
56
+ assert(t.evaluate(fact('foo' => [10])))
57
+ assert(!t.evaluate(fact('foo' => [100])))
58
+ assert(!t.evaluate(fact('foo' => 100)))
59
+ assert(!t.evaluate(fact('bar' => 100)))
60
60
  end
61
61
 
62
62
  def test_gt_matching
63
63
  t = Factbase::Term.new(:gt, [:foo, 42])
64
- assert(t.eval(fact('foo' => [100])))
65
- assert(t.eval(fact('foo' => 100)))
66
- assert(!t.eval(fact('foo' => [10])))
67
- assert(!t.eval(fact('foo' => 10)))
68
- assert(!t.eval(fact('bar' => 10)))
64
+ assert(t.evaluate(fact('foo' => [100])))
65
+ assert(t.evaluate(fact('foo' => 100)))
66
+ assert(!t.evaluate(fact('foo' => [10])))
67
+ assert(!t.evaluate(fact('foo' => 10)))
68
+ assert(!t.evaluate(fact('bar' => 10)))
69
69
  end
70
70
 
71
71
  def test_lt_matching_time
72
72
  t = Factbase::Term.new(:lt, [:foo, Time.now])
73
- assert(t.eval(fact('foo' => [Time.now - 100])))
74
- assert(!t.eval(fact('foo' => [Time.now + 100])))
75
- assert(!t.eval(fact('bar' => [100])))
73
+ assert(t.evaluate(fact('foo' => [Time.now - 100])))
74
+ assert(!t.evaluate(fact('foo' => [Time.now + 100])))
75
+ assert(!t.evaluate(fact('bar' => [100])))
76
76
  end
77
77
 
78
78
  def test_gt_matching_time
79
79
  t = Factbase::Term.new(:gt, [:foo, Time.now])
80
- assert(t.eval(fact('foo' => [Time.now + 100])))
81
- assert(!t.eval(fact('foo' => [Time.now - 100])))
82
- assert(!t.eval(fact('bar' => [100])))
80
+ assert(t.evaluate(fact('foo' => [Time.now + 100])))
81
+ assert(!t.evaluate(fact('foo' => [Time.now - 100])))
82
+ assert(!t.evaluate(fact('bar' => [100])))
83
83
  end
84
84
 
85
85
  def test_not_matching
86
86
  t = Factbase::Term.new(:not, [Factbase::Term.new(:nil, [])])
87
- assert(!t.eval(fact('foo' => [100])))
87
+ assert(!t.evaluate(fact('foo' => [100])))
88
88
  end
89
89
 
90
90
  def test_not_eq_matching
91
91
  t = Factbase::Term.new(:not, [Factbase::Term.new(:eq, [:foo, 100])])
92
- assert(t.eval(fact('foo' => [42, 12, -90])))
93
- assert(!t.eval(fact('foo' => 100)))
92
+ assert(t.evaluate(fact('foo' => [42, 12, -90])))
93
+ assert(!t.evaluate(fact('foo' => 100)))
94
94
  end
95
95
 
96
96
  def test_size_matching
97
97
  t = Factbase::Term.new(:size, [:foo])
98
- assert_equal(3, t.eval(fact('foo' => [42, 12, -90])))
99
- assert_equal(0, t.eval(fact('bar' => 100)))
98
+ assert_equal(3, t.evaluate(fact('foo' => [42, 12, -90])))
99
+ assert_equal(0, t.evaluate(fact('bar' => 100)))
100
100
  end
101
101
 
102
102
  def test_exists_matching
103
103
  t = Factbase::Term.new(:exists, [:foo])
104
- assert(t.eval(fact('foo' => [42, 12, -90])))
105
- assert(!t.eval(fact('bar' => 100)))
104
+ assert(t.evaluate(fact('foo' => [42, 12, -90])))
105
+ assert(!t.evaluate(fact('bar' => 100)))
106
106
  end
107
107
 
108
108
  def test_absent_matching
109
109
  t = Factbase::Term.new(:absent, [:foo])
110
- assert(t.eval(fact('z' => [42, 12, -90])))
111
- assert(!t.eval(fact('foo' => 100)))
110
+ assert(t.evaluate(fact('z' => [42, 12, -90])))
111
+ assert(!t.evaluate(fact('foo' => 100)))
112
112
  end
113
113
 
114
114
  def test_type_matching
115
115
  t = Factbase::Term.new(:type, [:foo])
116
- assert_equal('Integer', t.eval(fact('foo' => 42)))
117
- assert_equal('Array', t.eval(fact('foo' => [1, 2, 3])))
118
- assert_equal('String', t.eval(fact('foo' => 'Hello, world!')))
119
- assert_equal('Float', t.eval(fact('foo' => 3.14)))
120
- assert_equal('Time', t.eval(fact('foo' => Time.now)))
121
- assert_equal('nil', t.eval(fact))
116
+ assert_equal('Integer', t.evaluate(fact('foo' => 42)))
117
+ assert_equal('Array', t.evaluate(fact('foo' => [1, 2, 3])))
118
+ assert_equal('String', t.evaluate(fact('foo' => 'Hello, world!')))
119
+ assert_equal('Float', t.evaluate(fact('foo' => 3.14)))
120
+ assert_equal('Time', t.evaluate(fact('foo' => Time.now)))
121
+ assert_equal('nil', t.evaluate(fact))
122
+ end
123
+
124
+ def test_regexp_matching
125
+ t = Factbase::Term.new(:matches, [:foo, '[a-z]+'])
126
+ assert(t.evaluate(fact('foo' => 'hello')))
127
+ assert(t.evaluate(fact('foo' => 'hello 42')))
128
+ assert(!t.evaluate(fact('foo' => 42)))
122
129
  end
123
130
 
124
131
  def test_or_matching
@@ -129,9 +136,9 @@ class TestTerm < Minitest::Test
129
136
  Factbase::Term.new(:eq, [:bar, 5])
130
137
  ]
131
138
  )
132
- assert(t.eval(fact('foo' => [4])))
133
- assert(t.eval(fact('bar' => [5])))
134
- assert(!t.eval(fact('bar' => [42])))
139
+ assert(t.evaluate(fact('foo' => [4])))
140
+ assert(t.evaluate(fact('bar' => [5])))
141
+ assert(!t.evaluate(fact('bar' => [42])))
135
142
  end
136
143
 
137
144
  def test_when_matching
@@ -142,9 +149,16 @@ class TestTerm < Minitest::Test
142
149
  Factbase::Term.new(:eq, [:bar, 5])
143
150
  ]
144
151
  )
145
- assert(t.eval(fact('foo' => 4, 'bar' => 5)))
146
- assert(!t.eval(fact('foo' => 4)))
147
- assert(t.eval(fact('foo' => 5, 'bar' => 5)))
152
+ assert(t.evaluate(fact('foo' => 4, 'bar' => 5)))
153
+ assert(!t.evaluate(fact('foo' => 4)))
154
+ assert(t.evaluate(fact('foo' => 5, 'bar' => 5)))
155
+ end
156
+
157
+ def test_defn_simple
158
+ t = Factbase::Term.new(:defn, [:foo, 'self.to_s'])
159
+ assert_equal(true, t.evaluate(fact('foo' => 4)))
160
+ t1 = Factbase::Term.new(:foo, ['hello, world!'])
161
+ assert_equal('(foo \'hello, world!\')', t1.evaluate(fact))
148
162
  end
149
163
 
150
164
  private
@@ -57,7 +57,8 @@ class TestToXML < Minitest::Test
57
57
  to = Factbase::ToXML.new(fb)
58
58
  xml = Nokogiri::XML.parse(to.xml)
59
59
  assert(!xml.xpath('/fb[@dob]').empty?)
60
- assert(!xml.xpath('/fb[@factbase_version]').empty?)
60
+ assert(!xml.xpath('/fb[@version]').empty?)
61
+ assert(!xml.xpath('/fb[@size]').empty?)
61
62
  end
62
63
 
63
64
  def test_to_xml_with_short_names
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.28
4
+ version: 0.0.30
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-19 00:00:00.000000000 Z
11
+ date: 2024-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json