factbase 0.19.11 → 0.19.12

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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -4
  3. data/Gemfile.lock +16 -12
  4. data/README.md +49 -18
  5. data/Rakefile +2 -7
  6. data/factbase.gemspec +11 -11
  7. data/lib/factbase/accum.rb +1 -1
  8. data/lib/factbase/cached/cached_fact.rb +1 -2
  9. data/lib/factbase/cached/cached_factbase.rb +3 -3
  10. data/lib/factbase/cached/cached_query.rb +4 -6
  11. data/lib/factbase/cached/cached_term.rb +1 -2
  12. data/lib/factbase/churn.rb +4 -8
  13. data/lib/factbase/fact.rb +12 -9
  14. data/lib/factbase/flatten.rb +2 -2
  15. data/lib/factbase/impatient.rb +14 -13
  16. data/lib/factbase/indexed/indexed_and.rb +14 -20
  17. data/lib/factbase/indexed/indexed_eq.rb +5 -1
  18. data/lib/factbase/indexed/indexed_fact.rb +1 -4
  19. data/lib/factbase/indexed/indexed_factbase.rb +4 -4
  20. data/lib/factbase/indexed/indexed_gt.rb +3 -1
  21. data/lib/factbase/indexed/indexed_gte.rb +51 -0
  22. data/lib/factbase/indexed/indexed_lt.rb +3 -1
  23. data/lib/factbase/indexed/indexed_lte.rb +51 -0
  24. data/lib/factbase/indexed/indexed_not.rb +1 -1
  25. data/lib/factbase/indexed/indexed_or.rb +2 -2
  26. data/lib/factbase/indexed/indexed_query.rb +6 -7
  27. data/lib/factbase/indexed/indexed_term.rb +10 -6
  28. data/lib/factbase/indexed/indexed_unique.rb +4 -2
  29. data/lib/factbase/inv.rb +3 -3
  30. data/lib/factbase/lazy_taped.rb +10 -13
  31. data/lib/factbase/lazy_taped_hash.rb +2 -1
  32. data/lib/factbase/light.rb +1 -1
  33. data/lib/factbase/logged.rb +37 -34
  34. data/lib/factbase/pre.rb +3 -3
  35. data/lib/factbase/query.rb +4 -5
  36. data/lib/factbase/rules.rb +8 -8
  37. data/lib/factbase/sync/sync_factbase.rb +2 -2
  38. data/lib/factbase/syntax.rb +18 -19
  39. data/lib/factbase/tallied.rb +6 -7
  40. data/lib/factbase/taped.rb +5 -11
  41. data/lib/factbase/tee.rb +2 -2
  42. data/lib/factbase/term.rb +53 -60
  43. data/lib/factbase/terms/agg.rb +3 -4
  44. data/lib/factbase/terms/arithmetic.rb +7 -7
  45. data/lib/factbase/terms/as.rb +2 -2
  46. data/lib/factbase/terms/assert.rb +5 -13
  47. data/lib/factbase/terms/base.rb +6 -7
  48. data/lib/factbase/terms/best.rb +1 -1
  49. data/lib/factbase/terms/boolean.rb +1 -1
  50. data/lib/factbase/terms/compare.rb +2 -1
  51. data/lib/factbase/terms/defn.rb +8 -6
  52. data/lib/factbase/terms/empty.rb +1 -1
  53. data/lib/factbase/terms/first.rb +2 -2
  54. data/lib/factbase/terms/head.rb +3 -3
  55. data/lib/factbase/terms/inverted.rb +2 -2
  56. data/lib/factbase/terms/join.rb +8 -7
  57. data/lib/factbase/terms/matches.rb +4 -4
  58. data/lib/factbase/terms/max.rb +1 -1
  59. data/lib/factbase/terms/min.rb +1 -1
  60. data/lib/factbase/terms/nth.rb +3 -3
  61. data/lib/factbase/terms/plus.rb +1 -1
  62. data/lib/factbase/terms/prev.rb +3 -6
  63. data/lib/factbase/terms/sorted.rb +2 -2
  64. data/lib/factbase/terms/sprintf.rb +5 -4
  65. data/lib/factbase/terms/sum.rb +1 -1
  66. data/lib/factbase/terms/to_float.rb +2 -2
  67. data/lib/factbase/terms/to_integer.rb +2 -2
  68. data/lib/factbase/terms/to_string.rb +1 -1
  69. data/lib/factbase/terms/to_time.rb +2 -2
  70. data/lib/factbase/terms/traced.rb +2 -2
  71. data/lib/factbase/terms/undef.rb +2 -2
  72. data/lib/factbase/terms/unique.rb +3 -7
  73. data/lib/factbase/to_json.rb +1 -1
  74. data/lib/factbase/to_xml.rb +5 -9
  75. data/lib/factbase/to_yaml.rb +1 -1
  76. data/lib/factbase/version.rb +1 -2
  77. data/lib/factbase.rb +27 -10
  78. data/lib/fuzz.rb +3 -3
  79. metadata +3 -1
@@ -4,8 +4,8 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative '../../factbase'
7
- require_relative 'best'
8
7
  require_relative 'base'
8
+ require_relative 'best'
9
9
 
10
10
  # The 'max' term.
11
11
  # This term calculates the max value among the evaluated operands.
@@ -4,8 +4,8 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative '../../factbase'
7
- require_relative 'best'
8
7
  require_relative 'base'
8
+ require_relative 'best'
9
9
 
10
10
  # The 'min' term.
11
11
  # This term calculates the minimum value among the evaluated operands.
@@ -23,11 +23,11 @@ class Factbase::Nth < Factbase::TermBase
23
23
  def evaluate(_fact, maps, _fb)
24
24
  assert_args(2)
25
25
  pos = @operands[0]
26
- raise "An integer is expected, but #{pos} provided" unless pos.is_a?(Integer)
26
+ raise(ArgumentError, "An integer is expected, but #{pos} provided") unless pos.is_a?(Integer)
27
27
  k = @operands[1]
28
- raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
28
+ raise(ArgumentError, "A symbol is expected, but #{k} provided") unless k.is_a?(Symbol)
29
29
  m = maps[pos]
30
- return nil if m.nil?
30
+ return if m.nil?
31
31
  m[k.to_s]
32
32
  end
33
33
  end
@@ -3,8 +3,8 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require_relative 'base'
7
6
  require_relative 'arithmetic'
7
+ require_relative 'base'
8
8
 
9
9
  # Represents a Plus term in the Factbase system.
10
10
  # This class is used to perform addition operations on operands.
@@ -4,8 +4,8 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative 'base'
7
- # The Factbase::Unique class provides functionality for evaluating the uniqueness
8
- # of terms based on provided operands and facts.
7
+ # The Factbase::Prev class returns the previous value of a property
8
+ # during iteration, enabling comparisons between consecutive facts.
9
9
  class Factbase::Prev < Factbase::TermBase
10
10
  # Constructor.
11
11
  # @param [Array] operands Operands
@@ -21,9 +21,6 @@ class Factbase::Prev < Factbase::TermBase
21
21
  # @return [Object] The previous value
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
- before = @prev
25
- v = _values(0, fact, maps, fb)
26
- @prev = v
27
- before
24
+ @prev.tap { @prev = _values(0, fact, maps, fb) }
28
25
  end
29
26
  end
@@ -27,9 +27,9 @@ class Factbase::Sorted < Factbase::TermBase
27
27
  def predict(maps, fb, params)
28
28
  assert_args(2)
29
29
  prop = @operands[0]
30
- raise "A symbol is expected as first argument of 'sorted'" unless prop.is_a?(Symbol)
30
+ raise(ArgumentError, "A symbol is expected as first argument of 'sorted'") unless prop.is_a?(Symbol)
31
31
  term = @operands[1]
32
- raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
32
+ raise(ArgumentError, "A term is expected, but '#{term}' provided") unless term.is_a?(Factbase::Term)
33
33
  fb.query(term, maps).each(fb, params).to_a
34
34
  .reject { |m| m[prop].nil? }
35
35
  .sort_by { |m| m[prop].first }
@@ -22,9 +22,10 @@ class Factbase::Sprintf < Factbase::TermBase
22
22
  # @param [Factbase] fb Factbase to use for sub-queries
23
23
  # @return [String] The formatted string
24
24
  def evaluate(fact, maps, fb)
25
- fmt = _values(0, fact, maps, fb)[0]
26
- ops = (1..(@operands.length - 1)).map { |i| _values(i, fact, maps, fb)&.first }
27
- formatted(fmt, ops)
25
+ formatted(
26
+ _values(0, fact, maps, fb)[0],
27
+ (1..(@operands.length - 1)).map { |i| _values(i, fact, maps, fb)&.first }
28
+ )
28
29
  end
29
30
 
30
31
  private
@@ -32,6 +33,6 @@ class Factbase::Sprintf < Factbase::TermBase
32
33
  def formatted(fmt, ops)
33
34
  format(*([fmt] + ops))
34
35
  rescue ArgumentError => e
35
- raise "Cannot format #{ops.inspect} with '#{fmt}' in (sprintf ...): #{e.message}"
36
+ raise(RuntimeError, "Cannot format #{ops.inspect} with '#{fmt}' in (sprintf ...): #{e.message}")
36
37
  end
37
38
  end
@@ -23,7 +23,7 @@ class Factbase::Sum < Factbase::TermBase
23
23
  # @return [Integer] The sum of values for the specified key across all maps
24
24
  def evaluate(_fact, maps, _fb)
25
25
  k = @operands[0]
26
- raise "A symbol is expected, but '#{k}' provided" unless k.is_a?(Symbol)
26
+ raise(ArgumentError, "A symbol is expected, but '#{k}' provided") unless k.is_a?(Symbol)
27
27
  sum = 0
28
28
  maps.each do |m|
29
29
  vv = m[k.to_s]
@@ -22,7 +22,7 @@ class Factbase::ToFloat < Factbase::TermBase
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
- return nil if vv.nil?
26
- vv[0].to_f
25
+ return if vv.nil?
26
+ Float(vv[0])
27
27
  end
28
28
  end
@@ -22,7 +22,7 @@ class Factbase::ToInteger < Factbase::TermBase
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
- return nil if vv.nil?
26
- vv[0].to_i
25
+ return if vv.nil?
26
+ Integer(vv[0])
27
27
  end
28
28
  end
@@ -22,7 +22,7 @@ class Factbase::ToString < Factbase::TermBase
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
- return nil if vv.nil?
25
+ return if vv.nil?
26
26
  vv[0].to_s
27
27
  end
28
28
  end
@@ -22,7 +22,7 @@ class Factbase::ToTime < Factbase::TermBase
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
- return nil if vv.nil?
25
+ return if vv.nil?
26
26
  parse(vv[0])
27
27
  end
28
28
 
@@ -31,6 +31,6 @@ class Factbase::ToTime < Factbase::TermBase
31
31
  def parse(value)
32
32
  Time.parse(value.to_s)
33
33
  rescue ArgumentError => e
34
- raise "Cannot parse '#{value}' as Time in (to_time ...): #{e.message}"
34
+ raise(RuntimeError, "Cannot parse '#{value}' as Time in (to_time ...): #{e.message}")
35
35
  end
36
36
  end
@@ -25,9 +25,9 @@ class Factbase::Traced < Factbase::TermBase
25
25
  def evaluate(fact, maps, fb)
26
26
  assert_args(1)
27
27
  t = @operands[0]
28
- raise "A term is expected, but '#{t}' provided" unless t.is_a?(Factbase::Term)
28
+ raise(ArgumentError, "A term is expected, but '#{t}' provided") unless t.is_a?(Factbase::Term)
29
29
  r = t.evaluate(fact, maps, fb)
30
- puts "#{self} -> #{r}" # rubocop:disable Lint/Debugger
30
+ puts("#{self} -> #{r}") # rubocop:disable Lint/Debugger
31
31
  r
32
32
  end
33
33
  end
@@ -24,9 +24,9 @@ class Factbase::Undef < Factbase::TermBase
24
24
  def evaluate(_fact, _maps, _fb)
25
25
  assert_args(1)
26
26
  fn = @operands[0]
27
- raise "A symbol expected as first argument of 'undef'" unless fn.is_a?(Symbol)
27
+ raise(ArgumentError, "A symbol expected as first argument of 'undef'") unless fn.is_a?(Symbol)
28
28
  if Factbase::Term.private_method_defined?(fn, false)
29
- Factbase::Term.class_eval("undef :#{fn}", __FILE__, __LINE__ - 1) # undef :foo
29
+ Factbase::Term.class_eval("undef :#{fn}", __FILE__, __LINE__ - 1)
30
30
  end
31
31
  true
32
32
  end
@@ -21,14 +21,10 @@ class Factbase::Unique < Factbase::TermBase
21
21
  # @return [Boolean] True if the value is unique, false otherwise
22
22
  def evaluate(fact, maps, fb)
23
23
  @seen = Set.new if @seen.nil?
24
- raise "Too few operands for 'unique' (at least 1 expected)" if @operands.empty?
24
+ raise(ArgumentError, "Too few operands for 'unique' (at least 1 expected)") if @operands.empty?
25
25
  vv = (0..(@operands.size - 1)).map { |i| _values(i, fact, maps, fb) }
26
26
  return false if vv.any?(nil)
27
- pass = true
28
- Enumerator.product(*vv).to_a.each do |t|
29
- pass = false if @seen.include?(t)
30
- @seen << t
31
- end
32
- pass
27
+ tuples = Enumerator.product(*vv).to_a
28
+ tuples.none? { |t| @seen.include?(t) }.tap { tuples.each { |t| @seen << t } }
33
29
  end
34
30
  end
@@ -28,6 +28,6 @@ class Factbase::ToJSON
28
28
  # Convert the entire factbase into JSON.
29
29
  # @return [String] The factbase in JSON format
30
30
  def json
31
- Factbase::Flatten.new(Marshal.load(@fb.export), @sorter).it.to_json
31
+ Factbase::Flatten.new(@fb.each.to_a, @sorter).it.to_json
32
32
  end
33
33
  end
@@ -29,24 +29,20 @@ class Factbase::ToXML
29
29
  # Convert the entire factbase into XML.
30
30
  # @return [String] The factbase in XML format
31
31
  def xml
32
- bytes = @fb.export
33
- meta = {
34
- version: Factbase::VERSION,
35
- size: bytes.size
36
- }
32
+ meta = { version: Factbase::VERSION, size: @fb.export.size }
37
33
  Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
38
34
  xml.fb(meta) do
39
- Factbase::Flatten.new(Marshal.load(bytes), @sorter).it.each do |m|
35
+ Factbase::Flatten.new(@fb.each.to_a, @sorter).it.each do |m|
40
36
  xml.f_ do
41
37
  m.sort.to_h.each do |k, vv|
42
38
  if vv.is_a?(Array)
43
- xml.send(:"#{k}_") do
39
+ xml.__send__(:"#{k}_") do
44
40
  vv.each do |v|
45
- xml.send(:v, to_str(v), t: type_of(v))
41
+ xml.__send__(:v, to_str(v), t: type_of(v))
46
42
  end
47
43
  end
48
44
  else
49
- xml.send(:"#{k}_", to_str(vv), t: type_of(vv))
45
+ xml.__send__(:"#{k}_", to_str(vv), t: type_of(vv))
50
46
  end
51
47
  end
52
48
  end
@@ -28,6 +28,6 @@ class Factbase::ToYAML
28
28
  # Convert the entire factbase into YAML.
29
29
  # @return [String] The factbase in YAML format
30
30
  def yaml
31
- YAML.dump(Factbase::Flatten.new(Marshal.load(@fb.export), @sorter).it)
31
+ YAML.dump(Factbase::Flatten.new(@fb.each.to_a, @sorter).it)
32
32
  end
33
33
  end
@@ -8,6 +8,5 @@
8
8
  # Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
9
9
  # License:: MIT
10
10
  class Factbase
11
- # Current version of the gem (changed by .rultor.yml on every release)
12
- VERSION = '0.19.11' unless const_defined?(:VERSION)
11
+ VERSION = '0.19.12' unless const_defined?(:VERSION)
13
12
  end
data/lib/factbase.rb CHANGED
@@ -98,6 +98,13 @@ class Factbase
98
98
  @maps.size
99
99
  end
100
100
 
101
+ # Iterate over all facts yielding plain hashes.
102
+ # @yieldparam [Hash] fact Each fact as a plain Hash
103
+ # @return [Integer, Enumerator] Total number of facts or Enumerator
104
+ def each(&)
105
+ @maps.each(&)
106
+ end
107
+
101
108
  # Insert a new fact and return it.
102
109
  #
103
110
  # A fact, when inserted, is empty. It doesn't contain any properties.
@@ -106,7 +113,7 @@ class Factbase
106
113
  def insert
107
114
  map = {}
108
115
  @maps << map
109
- require_relative 'factbase/fact'
116
+ require_relative('factbase/fact')
110
117
  Factbase::Fact.new(map)
111
118
  end
112
119
 
@@ -132,7 +139,7 @@ class Factbase
132
139
  def query(term, maps = nil)
133
140
  maps ||= @maps
134
141
  term = to_term(term) if term.is_a?(String)
135
- require_relative 'factbase/query'
142
+ require_relative('factbase/query')
136
143
  Factbase::Query.new(maps, term, self)
137
144
  end
138
145
 
@@ -140,7 +147,7 @@ class Factbase
140
147
  # @param [String] query The query to convert
141
148
  # @return [Factbase::Term] The term
142
149
  def to_term(query)
143
- require_relative 'factbase/syntax'
150
+ require_relative('factbase/syntax')
144
151
  Factbase::Syntax.new(query).to_term
145
152
  end
146
153
 
@@ -161,15 +168,15 @@ class Factbase
161
168
  #
162
169
  # @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
163
170
  def txn
164
- require_relative 'factbase/lazy_taped'
171
+ require_relative('factbase/lazy_taped')
165
172
  taped = Factbase::LazyTaped.new(@maps)
166
- require_relative 'factbase/churn'
173
+ require_relative('factbase/churn')
167
174
  churn = Factbase::Churn.new
168
- catch :commit do
169
- require_relative 'factbase/light'
175
+ catch(:commit) do
176
+ require_relative('factbase/light')
170
177
  commit = false
171
- catch :rollback do
172
- yield Factbase::Light.new(Factbase.new(taped))
178
+ catch(:rollback) do
179
+ yield(Factbase::Light.new(Factbase.new(taped)))
173
180
  commit = true
174
181
  end
175
182
  return churn unless commit
@@ -233,9 +240,19 @@ class Factbase
233
240
  # This method supports both the original format (Array of maps) and
234
241
  # the IndexedFactbase format (Hash with :maps and :idx keys).
235
242
  #
243
+ # SECURITY: the input must come from a source you trust, because it is
244
+ # deserialized with +Marshal.load+. Loading a +Marshal+ stream crafted
245
+ # by an attacker can execute arbitrary code in the calling process,
246
+ # so never call +import+ on bytes received over the network, read from
247
+ # a user-supplied path, or pulled from any other untrusted channel
248
+ # without an out-of-band integrity check. See the official Ruby
249
+ # security notes for +Marshal.load+ at
250
+ # https://docs.ruby-lang.org/en/3.3/security_rdoc.html#label-Marshal.load
251
+ # for details.
252
+ #
236
253
  # @param [String] bytes Binary string to import
237
254
  def import(bytes)
238
- raise 'Empty input, cannot load a factbase' if bytes.empty?
255
+ raise(StandardError, 'Empty input, cannot load a factbase') if bytes.empty?
239
256
  data = Marshal.load(bytes)
240
257
  @maps +=
241
258
  if data.is_a?(Hash) && data.key?(:maps)
data/lib/fuzz.rb CHANGED
@@ -36,18 +36,18 @@ class Factbase::Fuzz
36
36
  def initialize
37
37
  @next_num = 0
38
38
  @max_comments = 10
39
- raise 'Not enough messages for fuzzing' if MESSAGES.size < @max_comments
39
+ raise(StandardError, 'Not enough messages for fuzzing') if MESSAGES.size < @max_comments
40
40
  end
41
41
 
42
42
  def self.make(count = 1000)
43
- raise "Count must be positive: #{count}" if count.negative?
43
+ raise(ArgumentError, "Count must be positive: #{count}") if count.negative?
44
44
  fb = Factbase.new
45
45
  Factbase::Fuzz.new.feed(fb, count)
46
46
  fb
47
47
  end
48
48
 
49
49
  def feed(fb, count = 1)
50
- raise "Count must be positive: #{count}" if count.negative?
50
+ raise(ArgumentError, "Count must be positive: #{count}") if count.negative?
51
51
  count.times do
52
52
  pull_request(fb, @next_num += 1)
53
53
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factbase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.11
4
+ version: 0.19.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -187,7 +187,9 @@ files:
187
187
  - lib/factbase/indexed/indexed_fact.rb
188
188
  - lib/factbase/indexed/indexed_factbase.rb
189
189
  - lib/factbase/indexed/indexed_gt.rb
190
+ - lib/factbase/indexed/indexed_gte.rb
190
191
  - lib/factbase/indexed/indexed_lt.rb
192
+ - lib/factbase/indexed/indexed_lte.rb
191
193
  - lib/factbase/indexed/indexed_not.rb
192
194
  - lib/factbase/indexed/indexed_one.rb
193
195
  - lib/factbase/indexed/indexed_or.rb