factbase 0.0.50 → 0.0.52

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: f62ac48c3d2c181b0064e519947fa2fed0ecb0b81660663c91319233eb2fc529
4
- data.tar.gz: f3131120095c0ece6cb01313bad9f55d58145ee289dfc4a4f8bd7e949778521b
3
+ metadata.gz: a70fcc45481bd1611d6bf2ed5c41ef40fe6f1ce67494abfa769c3b859b215960
4
+ data.tar.gz: 609bd76eaca6f4668bbbf5678514503edb1f9184e9abd62321dc02cdd614f80c
5
5
  SHA512:
6
- metadata.gz: d7325ad6538622ef5e8764547314ac2970827b44eb70dd37acde333505bd630370bf4ccfe7d1b35916592b89b4dd346c82bcb0c2f2d7532994cfbef0d2a23467
7
- data.tar.gz: eb8554fb580ec058551d6f4f43e47dac22ff13ee427195595a1d9f40caf7f51c59c0c697a849136618a625addf82f726b658752caf755b7eab84b3482a0a8b28
6
+ metadata.gz: 97e8cc3c9f53349aaf85aa3f95ba9f0e2525dd85f242ade079083b4ffab042dbb0b3f1ad776573646f7fae0107e709404b70e340fc9660b99ef8770a313aa8a9
7
+ data.tar.gz: e4d9404781d0314d4e6aa22f93affc3260fc053121c9e70204f8690d30de34feb26533254f15519b9e2562a5562e93398779d890ab78c3acc3afcd8ed8fba782
data/Gemfile CHANGED
@@ -23,12 +23,12 @@
23
23
  source 'https://rubygems.org'
24
24
  gemspec
25
25
 
26
- gem 'minitest', '5.23.1', require: false
26
+ gem 'minitest', '5.24.0', require: false
27
27
  gem 'rake', '13.2.1', require: false
28
- gem 'rspec-rails', '6.1.2', require: false
28
+ gem 'rspec-rails', '6.1.3', require: false
29
29
  gem 'rubocop', '1.64.1', require: false
30
- gem 'rubocop-performance', '1.21.0', require: false
31
- gem 'rubocop-rspec', '2.31.0', require: false
30
+ gem 'rubocop-performance', '1.21.1', require: false
31
+ gem 'rubocop-rspec', '3.0.1', 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
@@ -2,6 +2,7 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  factbase (0.0.0)
5
+ backtrace (~> 0.3)
5
6
  json (~> 2.7)
6
7
  loog (~> 0.2)
7
8
  nokogiri (~> 1.10)
@@ -38,6 +39,7 @@ GEM
38
39
  mutex_m
39
40
  tzinfo (~> 2.0)
40
41
  ast (2.4.2)
42
+ backtrace (0.4.0)
41
43
  base64 (0.2.0)
42
44
  bigdecimal (3.1.8)
43
45
  builder (3.3.0)
@@ -47,11 +49,11 @@ GEM
47
49
  diff-lcs (1.5.1)
48
50
  docile (1.4.0)
49
51
  drb (2.2.1)
50
- erubi (1.12.0)
52
+ erubi (1.13.0)
51
53
  i18n (1.14.5)
52
54
  concurrent-ruby (~> 1.0)
53
55
  io-console (0.7.2)
54
- irb (1.13.1)
56
+ irb (1.13.2)
55
57
  rdoc (>= 4.0.0)
56
58
  reline (>= 0.4.2)
57
59
  json (2.7.2)
@@ -60,15 +62,15 @@ GEM
60
62
  crass (~> 1.0.2)
61
63
  nokogiri (>= 1.12.0)
62
64
  loog (0.5.1)
63
- minitest (5.23.1)
65
+ minitest (5.24.0)
64
66
  mutex_m (0.2.0)
65
- nokogiri (1.16.5-arm64-darwin)
67
+ nokogiri (1.16.6-arm64-darwin)
66
68
  racc (~> 1.4)
67
- nokogiri (1.16.5-x64-mingw-ucrt)
69
+ nokogiri (1.16.6-x64-mingw-ucrt)
68
70
  racc (~> 1.4)
69
- nokogiri (1.16.5-x86_64-darwin)
71
+ nokogiri (1.16.6-x86_64-darwin)
70
72
  racc (~> 1.4)
71
- nokogiri (1.16.5-x86_64-linux)
73
+ nokogiri (1.16.6-x86_64-linux)
72
74
  racc (~> 1.4)
73
75
  parallel (1.25.1)
74
76
  parser (3.3.3.0)
@@ -111,13 +113,13 @@ GEM
111
113
  strscan
112
114
  rspec-core (3.13.0)
113
115
  rspec-support (~> 3.13.0)
114
- rspec-expectations (3.13.0)
116
+ rspec-expectations (3.13.1)
115
117
  diff-lcs (>= 1.2.0, < 2.0)
116
118
  rspec-support (~> 3.13.0)
117
119
  rspec-mocks (3.13.1)
118
120
  diff-lcs (>= 1.2.0, < 2.0)
119
121
  rspec-support (~> 3.13.0)
120
- rspec-rails (6.1.2)
122
+ rspec-rails (6.1.3)
121
123
  actionpack (>= 6.1)
122
124
  activesupport (>= 6.1)
123
125
  railties (>= 6.1)
@@ -139,19 +141,10 @@ GEM
139
141
  unicode-display_width (>= 2.4.0, < 3.0)
140
142
  rubocop-ast (1.31.3)
141
143
  parser (>= 3.3.1.0)
142
- rubocop-capybara (2.21.0)
143
- rubocop (~> 1.41)
144
- rubocop-factory_bot (2.26.1)
145
- rubocop (~> 1.61)
146
- rubocop-performance (1.21.0)
144
+ rubocop-performance (1.21.1)
147
145
  rubocop (>= 1.48.1, < 2.0)
148
146
  rubocop-ast (>= 1.31.1, < 2.0)
149
- rubocop-rspec (2.31.0)
150
- rubocop (~> 1.40)
151
- rubocop-capybara (~> 2.17)
152
- rubocop-factory_bot (~> 2.22)
153
- rubocop-rspec_rails (~> 2.28)
154
- rubocop-rspec_rails (2.29.1)
147
+ rubocop-rspec (3.0.1)
155
148
  rubocop (~> 1.61)
156
149
  ruby-progressbar (1.13.0)
157
150
  simplecov (0.22.0)
@@ -163,7 +156,7 @@ GEM
163
156
  simplecov (~> 0.19)
164
157
  simplecov-html (0.12.3)
165
158
  simplecov_json_formatter (0.1.4)
166
- stringio (3.1.0)
159
+ stringio (3.1.1)
167
160
  strscan (3.1.0)
168
161
  tago (0.0.2)
169
162
  thor (1.3.1)
@@ -173,7 +166,7 @@ GEM
173
166
  webrick (1.8.1)
174
167
  yaml (0.3.0)
175
168
  yard (0.9.36)
176
- zeitwerk (2.6.15)
169
+ zeitwerk (2.6.16)
177
170
 
178
171
  PLATFORMS
179
172
  arm64-darwin-22
@@ -183,12 +176,12 @@ PLATFORMS
183
176
 
184
177
  DEPENDENCIES
185
178
  factbase!
186
- minitest (= 5.23.1)
179
+ minitest (= 5.24.0)
187
180
  rake (= 13.2.1)
188
- rspec-rails (= 6.1.2)
181
+ rspec-rails (= 6.1.3)
189
182
  rubocop (= 1.64.1)
190
- rubocop-performance (= 1.21.0)
191
- rubocop-rspec (= 2.31.0)
183
+ rubocop-performance (= 1.21.1)
184
+ rubocop-rspec (= 3.0.1)
192
185
  simplecov (= 0.22.0)
193
186
  simplecov-cobertura (= 2.1.0)
194
187
  yard (= 0.9.36)
data/README.md CHANGED
@@ -55,53 +55,58 @@ assert(f2.query('(eq foo 42)').each.to_a.size == 1)
55
55
  ```
56
56
 
57
57
  There are some boolean terms available in a query
58
- (they return either TRUE or FALSE):
59
-
60
- * `(always)` and `(never)` are "true" and "false"
61
- * `(not t)` inverses the `t` if it's boolean (exception otherwise)
62
- * `(or t1 t2 ...)` returns true if at least one argument is true
63
- * `(and t1 t2 ...)` returns true if all arguments are true
64
- * `(when t1 t2)` returns true if `t1` is true and `t2` is true or `t1` is false
65
- * `(exists k)` returns true if `k` property exists in the fact
66
- * `(absent k)` returns true if `k` property is absent
67
- * `(eq a b)` returns true if `a` equals to `b`
68
- * `(lt a b)` returns true if `a` is less than `b`
69
- * `(gt a b)` returns true if `a` is greater than `b`
70
- * `(many a)` return true if there are many values in the `a` property
71
- * `(one a)` returns true if there is only one value in the `a` property
72
- * `(matches a re)` returns true when `a` matches regular expression `re`
58
+ (they return either `true` or `false`):
59
+
60
+ * `(always)` and `(never)` are `true` and `false`
61
+ * `(nil v)` is `true` if `v` is `nil`
62
+ * `(not b)` is the inverse of `b`
63
+ * `(or b1 b2 ...)` is `true` if at least one argument is `true`
64
+ * `(and b1 b2 ...)` if all arguments are `true`
65
+ * `(when b1 b2)` if `b1` is `true` and `b2` is `true`
66
+ or `b1` is `false`
67
+ * `(exists p)` if `p` property exists
68
+ * `(absent p)` if `p` property is absent
69
+ * `(zero v)` if any `v` equals to zero
70
+ * `(eq v1 v2)` if any `v1` equals to any `v2`
71
+ * `(lt v1 v2)` if any `v1` is less than any `v2`
72
+ * `(gt v1 v2)` if any `v1` is greater than any `v2`
73
+ * `(many v)` — if `v` has many values
74
+ * `(one v)` — if `v` has one value
75
+ * `(matches v s)` — if any `v` matches the `s` regular expression
73
76
 
74
77
  There are a few terms that return non-boolean values:
75
78
 
76
- * `(at i a)` returns the `i`-th value of the `a` property
77
- * `(size k)` returns cardinality of `k` property (zero if property is absent)
78
- * `(type a)` returns type of `a` ("String", "Integer", "Float", or "Time")
79
- * `(either a b)` returns `b` if `a` is `nil`
79
+ * `(at i v)` is the `i`-th value of `v`
80
+ * `(size v)` is the cardinality of `v` (zero if `v` is `nil`)
81
+ * `(type v)` is the type of `v`
82
+ (`"String"`, `"Integer"`, `"Float"`, `"Time"`, or `"Array"`)
83
+ * `(either v1 v1)` is `v2` if `v1` is `nil`
84
+
85
+ It's possible to modify the facts retrieved, on fly:
86
+
87
+ * `(as p v)` adds property `p` with the value `v`
88
+ * `(join s t)` adds properties named by the `s` mask with the values retrieved
89
+ by the `t` term, for example, `(join "x<=foo,y<=bar" (gt x 5))` will add
90
+ `x` and `y` properties, setting them to values found in the `foo` and `bar`
91
+ properties in the facts that match `(gt x 5)`
80
92
 
81
93
  Also, some simple arithmetic:
82
94
 
83
- * `(plus a b)` is a sum of `a` and `b`
84
- * `(minus a b)` is a deducation of `b` from `a`
85
- * `(times a b)` is a multiplication of `a` and `b`
86
- * `(div a b)` is a division of `a` by `b`
95
+ * `(plus v1 v2)` is a sum of `∑v1` and `∑v2`
96
+ * `(minus v1 v2)` is a deducation of `∑v2` from `∑v1`
97
+ * `(times v1 v2)` is a multiplication of `∏v1` and `∏v2`
98
+ * `(div v1 v2)` is a division of `∏v1` by `∏v2`
87
99
 
88
100
  One term is for meta-programming:
89
101
 
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)
102
+ * `(defn f "self.to_s")` defines a new term using Ruby syntax and returns `true`
103
+ * `(undef f)` undefines a term (nothing happens if it's not defined yet),
104
+ returns `true`
92
105
 
93
106
  There are terms that are history of search aware:
94
107
 
95
- * `(prev a)` returns the value of `a` in the previously seen fact
96
- * `(unique k)` returns true if the value of `k` property hasn't been seen yet
97
-
98
- There are also terms that match the entire factbase
99
- and must be used inside the `(agg ..)` term:
100
-
101
- * `(count)` returns the tally of facts
102
- * `(max k)` returns the maximum value of the `k` property in all facts
103
- * `(min k)` returns the minimum
104
- * `(sum k)` returns the arithmetic sum of all values of the `k` property
108
+ * `(prev p)` returns the value of `p` property in the previously seen fact
109
+ * `(unique p)` returns true if the value of `p` property hasn't been seen yet
105
110
 
106
111
  The `agg` term enables sub-queries by evaluating the first argument (term)
107
112
  over all available facts, passing the entire subset to the second argument,
@@ -110,6 +115,23 @@ and then returning the result as an atomic value:
110
115
  * `(lt age (agg (eq gender 'F') (max age)))` selects all facts where
111
116
  the `age` is smaller than the maximum `age` of all women
112
117
  * `(eq id (agg (always) (max id)))` selects the fact with the largest `id`
118
+ * `(eq salary (agg (eq dept $dept) (avg salary)))` selects the facts
119
+ with the salary average in their departments
120
+
121
+ There are also terms that match the entire factbase
122
+ and must be used primarily inside the `(agg ..)` term:
123
+
124
+ * `(nth v p)` returns the `p` property of the _v_-th fact (must be
125
+ a positive integer)
126
+ * `(first p)` returns the `p` property of the first fact
127
+ * `(count)` returns the tally of facts
128
+ * `(max p)` returns the maximum value of the `p` property in all facts
129
+ * `(min p)` returns the minimum
130
+ * `(sum p)` returns the arithmetic sum of all values of the `p` property
131
+
132
+ It's also possible to use a sub-query in a shorter form than with the `agg`:
133
+
134
+ * `(empty q)` is true if the subquery `q` is empty
113
135
 
114
136
  ## How to contribute
115
137
 
data/factbase.gemspec CHANGED
@@ -42,6 +42,7 @@ Gem::Specification.new do |s|
42
42
  s.files = `git ls-files`.split($RS)
43
43
  s.rdoc_options = ['--charset=UTF-8']
44
44
  s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
45
+ s.add_runtime_dependency 'backtrace', '~>0.3'
45
46
  s.add_runtime_dependency 'json', '~>2.7'
46
47
  s.add_runtime_dependency 'loog', '~>0.2'
47
48
  s.add_runtime_dependency 'nokogiri', '~>1.10'
@@ -22,55 +22,55 @@
22
22
 
23
23
  require_relative '../factbase'
24
24
 
25
- # With the help of this class, it's possible to select a few facts
26
- # from a factbase at a time, which depend on each other. For example,
27
- # it's necessary to find a fact where the +name+ is set and then find
28
- # another fact, where the salary is the +salary+ is the same as in the
29
- # first found fact. Here is how:
30
- #
31
- # Factbase::Tuples.new(qt, ['(exists name)', '(eq salary, {f0.salary})']).each do |a, b|
32
- # puts a.name
33
- # puts b.salary
34
- # end
35
- #
36
- # Here, the +{f0.salary}+ is a special substitution place, which is replaced
37
- # by the +salary+ of the fact that is found by the previous query.
38
- #
39
- # The indexing of queries starts from zero.
25
+ # Accumulator of props.
40
26
  #
41
27
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
42
28
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
43
29
  # License:: MIT
44
- class Factbase::Tuples
45
- def initialize(fb, queries)
46
- @fb = fb
47
- @queries = queries
30
+ class Factbase::Accum
31
+ # Ctor.
32
+ # @param [Factbase::Fact] fact The fact to decorate
33
+ # @param [Hash] props Hash of props that were set
34
+ # @param [Boolean] pass TRUE if all "set" operations must go through, to the +fact+
35
+ def initialize(fact, props, pass)
36
+ @fact = fact
37
+ @props = props
38
+ @pass = pass
48
39
  end
49
40
 
50
- # Iterate them one by one.
51
- # @yield [Array<Fact>] Arrays of facts one-by-one
52
- # @return [Integer] Total number of arrays yielded
53
- def each(&)
54
- return to_enum(__method__) unless block_given?
55
- each_rec([], @queries, &)
41
+ def to_s
42
+ "#{@fact} + #{@props}"
56
43
  end
57
44
 
58
- private
59
-
60
- def each_rec(facts, tail, &)
61
- qq = tail.dup
62
- q = qq.shift
63
- return if q.nil?
64
- qt = q.gsub(/\{f([0-9]+).([a-z0-9_]+)\}/) do
65
- facts[Regexp.last_match[1].to_i].send(Regexp.last_match[2])
45
+ def method_missing(*args)
46
+ k = args[0].to_s
47
+ if k.end_with?('=')
48
+ kk = k[0..-2]
49
+ @props[kk] = [] if @props[kk].nil?
50
+ @props[kk] << args[1]
51
+ @fact.method_missing(*args) if @pass
52
+ return
66
53
  end
67
- @fb.query(qt).each do |f|
68
- fs = facts + [f]
69
- if qq.empty?
70
- yield fs
71
- else
72
- each_rec(fs, qq, &)
73
- end
54
+ if k == '[]'
55
+ kk = args[1].to_s
56
+ vv = @props[kk].nil? ? [] : @props[kk]
57
+ vvv = @fact.method_missing(*args)
58
+ vvv = [vvv] unless vvv.nil? || vvv.is_a?(Array)
59
+ vv += vvv unless vvv.nil?
60
+ vv.uniq!
61
+ return vv.empty? ? nil : vv
74
62
  end
63
+ return @props[k][0] unless @props[k].nil?
64
+ @fact.method_missing(*args)
65
+ end
66
+
67
+ # rubocop:disable Style/OptionalBooleanParameter
68
+ def respond_to?(_method, _include_private = false)
69
+ # rubocop:enable Style/OptionalBooleanParameter
70
+ true
71
+ end
72
+
73
+ def respond_to_missing?(_method, _include_private = false)
74
+ true
75
75
  end
76
76
  end
data/lib/factbase/fact.rb CHANGED
@@ -69,14 +69,9 @@ class Factbase::Fact
69
69
  raise "Prop type '#{v.class}' is not allowed" unless [String, Integer, Float, Time].include?(v.class)
70
70
  v = v.utc if v.is_a?(Time)
71
71
  @mutex.synchronize do
72
- before = @map[kk]
73
- return if before == v
74
- if before.nil?
75
- @map[kk] = [v]
76
- else
77
- @map[kk] << v
78
- @map[kk].uniq!
79
- end
72
+ @map[kk] = [] if @map[kk].nil?
73
+ @map[kk] << v
74
+ @map[kk].uniq!
80
75
  end
81
76
  nil
82
77
  elsif k == '[]'
@@ -87,7 +82,7 @@ class Factbase::Fact
87
82
  raise "Can't get '#{k}', the fact is empty" if @map.empty?
88
83
  raise "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]"
89
84
  end
90
- v.is_a?(Array) ? v[0] : v
85
+ v[0]
91
86
  end
92
87
  end
93
88
 
@@ -55,7 +55,6 @@ class Factbase::Looged
55
55
 
56
56
  def txn(this = self, &)
57
57
  start = Time.now
58
- before = @fb.size
59
58
  id = nil
60
59
  rollback = false
61
60
  r = @fb.txn(this) do |fbt|
@@ -68,7 +67,7 @@ class Factbase::Looged
68
67
  if rollback
69
68
  @loog.debug("Txn ##{id} rolled back in #{start.ago}")
70
69
  else
71
- @loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'} #{before} facts in #{start.ago}")
70
+ @loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'} the factbase in #{start.ago}")
72
71
  end
73
72
  r
74
73
  end
@@ -130,12 +129,26 @@ class Factbase::Looged
130
129
  @loog = loog
131
130
  end
132
131
 
133
- def each(&)
132
+ def one(params = {})
133
+ q = Factbase::Syntax.new(@expr).to_term.to_s
134
+ r = nil
135
+ tail = Factbase::Looged.elapsed do
136
+ r = @fb.query(@expr).one(params)
137
+ end
138
+ if r.nil?
139
+ @loog.debug("Nothing found by '#{q}' #{tail}")
140
+ else
141
+ @loog.debug("Found #{r} (#{r.class}) by '#{q}' #{tail}")
142
+ end
143
+ r
144
+ end
145
+
146
+ def each(params = {}, &)
134
147
  q = Factbase::Syntax.new(@expr).to_term.to_s
135
148
  if block_given?
136
149
  r = nil
137
150
  tail = Factbase::Looged.elapsed do
138
- r = @fb.query(@expr).each(&)
151
+ r = @fb.query(@expr).each(params, &)
139
152
  end
140
153
  raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
141
154
  if r.zero?
@@ -147,7 +160,7 @@ class Factbase::Looged
147
160
  else
148
161
  array = []
149
162
  tail = Factbase::Looged.elapsed do
150
- @fb.query(@expr).each do |f|
163
+ @fb.query(@expr).each(params) do |f|
151
164
  array << f
152
165
  end
153
166
  end
@@ -23,12 +23,16 @@
23
23
  require_relative '../factbase'
24
24
  require_relative 'syntax'
25
25
  require_relative 'fact'
26
+ require_relative 'accum'
27
+ require_relative 'tee'
26
28
 
27
29
  # Query.
28
30
  #
29
31
  # This is an internal class, it is not supposed to be instantiated directly. It
30
32
  # is created by the +query()+ method of the +Factbase+ class.
31
33
  #
34
+ # It is NOT thread-safe!
35
+ #
32
36
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
33
37
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
34
38
  # License:: MIT
@@ -43,26 +47,44 @@ class Factbase::Query
43
47
  @query = query
44
48
  end
45
49
 
46
- # Iterate them one by one.
50
+ # Iterate facts one by one.
51
+ # @param [Hash] params Optional params accessible in the query via the "$" symbol
47
52
  # @yield [Fact] Facts one-by-one
48
53
  # @return [Integer] Total number of facts yielded
49
- def each
50
- return to_enum(__method__) unless block_given?
54
+ def each(params = {})
55
+ return to_enum(__method__, params) unless block_given?
51
56
  term = Factbase::Syntax.new(@query).to_term
52
57
  yielded = 0
53
58
  @maps.each do |m|
59
+ extras = {}
54
60
  f = Factbase::Fact.new(@mutex, m)
55
- r = term.evaluate(f, @maps)
61
+ params = params.transform_keys(&:to_s) if params.is_a?(Hash)
62
+ f = Factbase::Tee.new(f, params)
63
+ a = Factbase::Accum.new(f, extras, false)
64
+ r = term.evaluate(a, @maps)
56
65
  unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
57
66
  raise "Unexpected evaluation result (#{r.class}), must be Boolean at #{@query}"
58
67
  end
59
68
  next unless r
60
- yield f
69
+ yield Factbase::Accum.new(f, extras, true)
61
70
  yielded += 1
62
71
  end
63
72
  yielded
64
73
  end
65
74
 
75
+ # Read a single value.
76
+ # @param [Hash] params Optional params accessible in the query via the "$" symbol
77
+ # @return The value evaluated
78
+ def one(params = {})
79
+ term = Factbase::Syntax.new(@query).to_term
80
+ params = params.transform_keys(&:to_s) if params.is_a?(Hash)
81
+ r = term.evaluate(Factbase::Tee.new(nil, params), @maps)
82
+ unless %w[String Integer Float Time Array NilClass].include?(r.class.to_s)
83
+ raise "Incorrect type #{r.class} returned by #{@query}"
84
+ end
85
+ r
86
+ end
87
+
66
88
  # Delete all facts that match the query.
67
89
  # @return [Integer] Total number of facts deleted
68
90
  def delete!
@@ -126,8 +126,12 @@ class Factbase::Rules
126
126
  @check = check
127
127
  end
128
128
 
129
- def each
130
- return to_enum(__method__) unless block_given?
129
+ def one(params = {})
130
+ @query.one(params)
131
+ end
132
+
133
+ def each(params = {})
134
+ return to_enum(__method__, params) unless block_given?
131
135
  @query.each do |f|
132
136
  yield Fact.new(f, @check)
133
137
  end
@@ -44,7 +44,7 @@ class Factbase::Syntax
44
44
  def to_term
45
45
  build.simplify
46
46
  rescue StandardError => e
47
- err = "#{e.message} in #{@query}"
47
+ err = "#{e.message} in \"#{@query}\""
48
48
  err = "#{err}, tokens: #{@tokens}" unless @tokens.nil?
49
49
  raise err
50
50
  end
@@ -142,7 +142,7 @@ class Factbase::Syntax
142
142
  elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
143
143
  Time.parse(t)
144
144
  else
145
- raise "Wrong symbol format (#{t})" unless t.match?(/^[_a-z][a-zA-Z0-9_]*$/)
145
+ raise "Wrong symbol format (#{t})" unless t.match?(/^[_a-z\$][a-zA-Z0-9_]*$/)
146
146
  t.to_sym
147
147
  end
148
148
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2024 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require_relative '../factbase'
24
+
25
+ # Tee of two facts.
26
+ #
27
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
28
+ # Copyright:: Copyright (c) 2024 Yegor Bugayenko
29
+ # License:: MIT
30
+ class Factbase::Tee
31
+ # Ctor.
32
+ # @param [Factbase::Fact] fact Primary fact to use for reading
33
+ # @param [Factbase::Fact] upper Fact to access with a "$" prefix
34
+ def initialize(fact, upper)
35
+ @fact = fact
36
+ @upper = upper
37
+ end
38
+
39
+ def to_s
40
+ @fact.to_s
41
+ end
42
+
43
+ def method_missing(*args)
44
+ return @fact.method_missing(*args) unless args[0].to_s == '[]' && args[1].to_s.start_with?('$')
45
+ n = args[1].to_s
46
+ n = n[1..] unless @upper.is_a?(Factbase::Tee)
47
+ @upper[n]
48
+ end
49
+
50
+ # rubocop:disable Style/OptionalBooleanParameter
51
+ def respond_to?(_method, _include_private = false)
52
+ # rubocop:enable Style/OptionalBooleanParameter
53
+ true
54
+ end
55
+
56
+ def respond_to_missing?(_method, _include_private = false)
57
+ true
58
+ end
59
+ end