factbase 0.0.40 → 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: 129d17fba6d19bc5131f853d6b90c2e963187e84626270be4f7766e926d4c2c9
4
- data.tar.gz: 4196611d541b81bdc1a73bf31f4b1fe15077ad702d8d1ec558f0f61d5e530396
3
+ metadata.gz: 42066d9d82538ecb6f0ec8df6cc711f4328e1ba496f00366fe67719ca8a1185e
4
+ data.tar.gz: d40e21b7e426af5184b9b03f515da4508b1e1e9e52995f566b16c778eefefca3
5
5
  SHA512:
6
- metadata.gz: 1d87ddb11c2252043c963c6e086a617f216bc15a79441cfa121a0e3de3157f31705cfd1bfea7b6b89996488c76805ee27152f4be00627c7d3f3e1d506636c303
7
- data.tar.gz: 5e913f0d675f19fc03cc83c80301af5fd390de8cdaf730a0ea2612768ae0056286be7f5f42686616ee0b1fc69cf22adc0009e9df5315251971e9c948da5ee739
6
+ metadata.gz: ff04d1f7e3eb1b18b36544ea8fb61280cda250a5e9920ba7b858092ae7c13111efce537d39acd7119f2f70c8704a89d06c98bf54b5a4639b4f187599f61eecbc
7
+ data.tar.gz: '09ba7d04f933f758c1db716c9490d464a2202644560a594bfae0a77e8bee8d26dcc38605101417a062ccd5c9645b5187e7f069e8f31e6bc782e48dc79775a347'
data/Gemfile CHANGED
@@ -26,9 +26,9 @@ gemspec
26
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.64.0', require: false
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
@@ -70,7 +70,7 @@ GEM
70
70
  nokogiri (1.16.5-x86_64-linux)
71
71
  racc (~> 1.4)
72
72
  parallel (1.24.0)
73
- parser (3.3.1.0)
73
+ parser (3.3.2.0)
74
74
  ast (~> 2.4.1)
75
75
  racc
76
76
  psych (5.1.2)
@@ -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.64.0)
128
+ rubocop (1.64.1)
129
129
  json (~> 2.3)
130
130
  language_server-protocol (>= 3.17.0)
131
131
  parallel (~> 1.10)
@@ -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)
@@ -184,9 +184,9 @@ DEPENDENCIES
184
184
  minitest (= 5.23.1)
185
185
  rake (= 13.2.1)
186
186
  rspec-rails (= 6.1.2)
187
- rubocop (= 1.64.0)
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
 
@@ -26,12 +26,17 @@ require_relative 'fact'
26
26
 
27
27
  # Query.
28
28
  #
29
- # This is an internal class, it is not supposed to be instantiated directly.
29
+ # This is an internal class, it is not supposed to be instantiated directly. It
30
+ # is created by the +query()+ method of the +Factbase+ class.
30
31
  #
31
32
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
32
33
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
33
34
  # License:: MIT
34
35
  class Factbase::Query
36
+ # Constructor.
37
+ # @param [Array<Fact>] maps Array of facts to start with
38
+ # @param [Mutex] mutex Mutex to sync all modifications to the +maps+
39
+ # @param [String] query The query as a string
35
40
  def initialize(maps, mutex, query)
36
41
  @maps = maps
37
42
  @mutex = mutex
@@ -48,7 +53,9 @@ class Factbase::Query
48
53
  @maps.each do |m|
49
54
  f = Factbase::Fact.new(@mutex, m)
50
55
  r = term.evaluate(f, @maps)
51
- raise 'Unexpected evaluation result, must be boolean' unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
56
+ unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
57
+ raise "Unexpected evaluation result (#{r.class}), must be Boolean at #{@query}"
58
+ end
52
59
  next unless r
53
60
  yield f
54
61
  yielded += 1
@@ -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
@@ -66,7 +66,9 @@ class Factbase::Term
66
66
  def evaluate(fact, maps)
67
67
  send(@op, fact, maps)
68
68
  rescue NoMethodError => e
69
- raise "Term '#{@op}' is not defined: #{e.message}"
69
+ raise "Term '#{@op}' is not defined at #{self}: #{e.message}"
70
+ rescue StandardError => e
71
+ raise "#{e.message} at #{self} (#{e.backtrace[0]})"
70
72
  end
71
73
 
72
74
  # Simplify it if possible.
@@ -111,19 +113,19 @@ class Factbase::Term
111
113
 
112
114
  def not(fact, maps)
113
115
  assert_args(1)
114
- !only_bool(the_values(0, fact, maps))
116
+ !only_bool(the_values(0, fact, maps), 0)
115
117
  end
116
118
 
117
119
  def or(fact, maps)
118
120
  (0..@operands.size - 1).each do |i|
119
- return true if only_bool(the_values(i, fact, maps))
121
+ return true if only_bool(the_values(i, fact, maps), i)
120
122
  end
121
123
  false
122
124
  end
123
125
 
124
126
  def and(fact, maps)
125
127
  (0..@operands.size - 1).each do |i|
126
- return false unless only_bool(the_values(i, fact, maps))
128
+ return false unless only_bool(the_values(i, fact, maps), i)
127
129
  end
128
130
  true
129
131
  end
@@ -177,7 +179,7 @@ class Factbase::Term
177
179
  def at(fact, maps)
178
180
  assert_args(2)
179
181
  i = the_values(0, fact, maps)
180
- 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
181
183
  i = i[0]
182
184
  return nil if i.nil?
183
185
  v = the_values(1, fact, maps)
@@ -299,8 +301,12 @@ class Factbase::Term
299
301
  end
300
302
 
301
303
  def defn(_fact, _maps)
304
+ assert_args(2)
302
305
  fn = @operands[0]
303
- 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_]+$/)
304
310
  e = "class Factbase::Term\nprivate\ndef #{fn}(fact, maps)\n#{@operands[1]}\nend\nend"
305
311
  # rubocop:disable Security/Eval
306
312
  eval(e)
@@ -308,6 +314,16 @@ class Factbase::Term
308
314
  true
309
315
  end
310
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
+
311
327
  def min(_fact, maps)
312
328
  @min ||= best(maps) { |v, b| v < b }
313
329
  end
@@ -324,7 +340,7 @@ class Factbase::Term
324
340
  @sum ||=
325
341
  begin
326
342
  k = @operands[0]
327
- 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)
328
344
  sum = 0
329
345
  maps.each do |m|
330
346
  vv = m[k.to_s]
@@ -339,10 +355,11 @@ class Factbase::Term
339
355
  end
340
356
 
341
357
  def agg(_fact, maps)
358
+ assert_args(2)
342
359
  selector = @operands[0]
343
- 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)
344
361
  term = @operands[1]
345
- 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)
346
363
  subset = maps.select { |m| selector.evaluate(m, maps) }
347
364
  @agg ||=
348
365
  if subset.empty?
@@ -360,7 +377,7 @@ class Factbase::Term
360
377
 
361
378
  def by_symbol(pos, fact)
362
379
  o = @operands[pos]
363
- 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)
364
381
  k = o.to_s
365
382
  fact[k]
366
383
  end
@@ -374,16 +391,18 @@ class Factbase::Term
374
391
  v
375
392
  end
376
393
 
377
- def only_bool(val)
394
+ def only_bool(val, pos)
378
395
  val = val[0] if val.is_a?(Array)
379
396
  return false if val.nil?
380
- raise "Boolean expected, while #{val.class} received" unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
397
+ unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
398
+ raise "Boolean expected, while #{val.class} received from #{@operands[pos]}"
399
+ end
381
400
  val
382
401
  end
383
402
 
384
403
  def best(maps)
385
404
  k = @operands[0]
386
- 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)
387
406
  best = nil
388
407
  maps.each do |m|
389
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,12 +79,13 @@ 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.40'
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
86
86
 
87
87
  # Constructor.
88
+ # @param [Array<Hash>] facts Array of facts to start with
88
89
  def initialize(facts = [])
89
90
  @maps = facts
90
91
  @mutex = Mutex.new
@@ -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), []))
@@ -183,6 +211,22 @@ class TestTerm < Minitest::Test
183
211
  assert_equal([42], t.evaluate(fact('foo' => 4), []))
184
212
  end
185
213
 
214
+ def test_report_missing_term
215
+ t = Factbase::Term.new(:something, [])
216
+ msg = assert_raises do
217
+ t.evaluate(fact, [])
218
+ end.message
219
+ assert(msg.include?('not defined at (something)'), msg)
220
+ end
221
+
222
+ def test_report_other_error
223
+ t = Factbase::Term.new(:at, [])
224
+ msg = assert_raises do
225
+ t.evaluate(fact, [])
226
+ end.message
227
+ assert(msg.include?('at (at)'), msg)
228
+ end
229
+
186
230
  private
187
231
 
188
232
  def fact(map = {})
@@ -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.40
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-05-30 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