factbase 0.0.40 → 0.0.42

Sign up to get free protection for your applications and to get access to all the features.
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