factbase 0.6.0 → 0.7.0

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: 057bb10953848632e68138ac248e60ddfe6ea8395f2a2a5b7ffdd2fea2cf7b7e
4
- data.tar.gz: fd26cf56e5d42ba80c7a99539bf585a81fcf3dfb82370830c2a178e1eccbe534
3
+ metadata.gz: 61e944b02d2b8110a72ad61a25766dfc85a6fd9996af11d3fff8548a2b7dc8d0
4
+ data.tar.gz: 900e17bafd0abf7cd1a4cc87a003c2315bd22e865166454dd9712afd4d5c822f
5
5
  SHA512:
6
- metadata.gz: 8d0475566890335428e8a654bae3a36eb87f9d5ead1e7618801546c5c968c6ffeafd1927372af354c176de7882586ba1923981d891a8d0fa5f7bf8a6b71afd7f
7
- data.tar.gz: b096503eb48452c75f701b6b88b29ba2916e74dc9e02b9407d44de81eb4bc91d48e2bcb0dcfacd28b70bd5635d80537c580040c44ab96a6f34fb9bb26bf82804
6
+ metadata.gz: f8eb4bf66d83e08c7527a492e224673874071655ecada4fdb163e872cc018d35bd24c445b267ed65a9ac6f3275a40826df9e745279af00f473ed9edde76fed76
7
+ data.tar.gz: 4911055570df61091d8beeb36a7a02d74985c64edbf6246fb5dcd429fc07d4f32e694a42ad856a941c5963a3c30009ca9ea0d1c42c13e5017d6a946aa038a798
@@ -1,5 +1,3 @@
1
- # (The MIT License)
2
- #
3
1
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
2
  # SPDX-License-Identifier: MIT
5
3
  ---
data/.gitignore CHANGED
@@ -6,3 +6,4 @@ coverage/
6
6
  rdoc/
7
7
  doc/
8
8
  .yardoc/
9
+ vendor/
data/.rubocop.yml CHANGED
@@ -44,3 +44,4 @@ Security/MarshalLoad:
44
44
  Enabled: false
45
45
  Layout/MultilineAssignmentLayout:
46
46
  Enabled: true
47
+ require: []
data/README.md CHANGED
@@ -209,18 +209,24 @@ This is the result of the benchmark:
209
209
  <!-- benchmark_begin -->
210
210
  | Action | Seconds | Details |
211
211
  | --- | --: | --- |
212
- | `fb.insert()` | 7.874 | Inserted 100000 facts |
213
- | `(gt time '2024-03-23T03:21:43Z')` | 0.072 | Found 100000 fact(s) |
214
- | `(gt cost 50)` | 0.069 | Found 50030 fact(s) |
215
- | `(eq title 'Object Thinking 5000')` | 0.051 | Found 1 fact(s) |
216
- | `(and (eq foo 42.998) (or (gt bar 200) (absent zzz)))` | 0.059 | Found 2 fact(s) |
217
- | `(eq id (agg (always) (max id)))` | 0.131 | Found 1 fact(s) |
218
- | `(join "c<=cost,b<=bar" (eq id (agg (always) (max id))))` | 0.695 | Found 100000 fact(s) |
219
- | `.export()` + `.import()` | 1.931 | 11407636 bytes |
212
+ | `fb.insert()` | 1.883 | Inserted 25000 facts |
213
+ | `(gt time '2024-03-23T03:21:43Z')` | 0.201 | 25000 facts x100 |
214
+ | `(gt cost 50)` | 0.175 | 12430 facts x100 |
215
+ | `(eq title 'Object Thinking 5000')` | 0.129 | 1 facts x100 |
216
+ | `(and (eq foo 42.998) (or (gt bar 200) (absent zzz)))` | 0.157 | 0 facts x100 |
217
+ | `(eq id (agg (always) (max id)))` | 0.268 | 1 facts x100 |
218
+ | `(join "c<=cost,b<=bar" (eq id (agg (always) (max id))))` | 1.844 | 25000 facts x100 |
219
+ | txn: `query()` | 19.358 | modified 0 facts |
220
+ | txn: `insert()` | 0.059 | modified 100 facts |
221
+ | txn: `add()` | 16.223 | modified 3 facts |
222
+ | txn: `delete!()` | 3.573 | modified 12439 facts |
223
+ | `.export()` + `.import()` | 0.369 | 1451040 bytes |
224
+ | `(gt cost 3)` | 0.030 | Deleted 12395 fact(s) |
225
+ | `(gt bar 1)` | 0.001 | Deleted 363 fact(s) |
220
226
 
221
227
  The results were calculated in [this GHA job][benchmark-gha]
222
- on 2025-01-27 at 16:51,
228
+ on 2025-02-24 at 13:32,
223
229
  on Linux with 4 CPUs.
224
230
  <!-- benchmark_end -->
225
231
 
226
- [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/12994018323
232
+ [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/13499241189
data/REUSE.toml CHANGED
@@ -4,13 +4,21 @@
4
4
  version = 1
5
5
  [[annotations]]
6
6
  path = [
7
+ "**/*.jpg",
8
+ "**/*.json",
9
+ "**/*.md",
10
+ "**/*.pdf",
11
+ "**/*.png",
12
+ "**/*.svg",
13
+ "**/.gitignore",
7
14
  ".gitattributes",
8
15
  ".gitignore",
9
16
  ".gitleaksignore",
10
17
  ".pdd",
18
+ ".xcop",
11
19
  "Gemfile.lock",
12
20
  "README.md",
13
- "renovate.json"
21
+ "renovate.json",
14
22
  ]
15
23
  precedence = "override"
16
24
  SPDX-FileCopyrightText = "Copyright (c) 2025 Yegor Bugayenko"
data/benchmarks/simple.rb CHANGED
@@ -32,7 +32,7 @@ end
32
32
 
33
33
  def query(fb, query)
34
34
  total = 0
35
- runs = 10
35
+ runs = 100
36
36
  time =
37
37
  Benchmark.measure do
38
38
  runs.times do
@@ -41,8 +41,21 @@ def query(fb, query)
41
41
  end
42
42
  {
43
43
  title: "`#{query}`",
44
- time: (time.real / runs).round(6),
45
- details: "Found #{total} fact(s)"
44
+ time: time.real.round(6),
45
+ details: "#{total} facts x#{runs}"
46
+ }
47
+ end
48
+
49
+ def delete(fb, query)
50
+ total = 0
51
+ time =
52
+ Benchmark.measure do
53
+ total = fb.query(query).delete!
54
+ end
55
+ {
56
+ title: "`#{query}`",
57
+ time: time.real.round(6),
58
+ details: "Deleted #{total} fact(s)"
46
59
  }
47
60
  end
48
61
 
@@ -62,16 +75,51 @@ def impex(fb)
62
75
  }
63
76
  end
64
77
 
78
+ def txn(fb, scenario, &block)
79
+ modified = 0
80
+ time =
81
+ Benchmark.measure do
82
+ modified = fb.txn(&block)
83
+ end
84
+ {
85
+ title: "txn: `#{scenario}`",
86
+ time: time.real,
87
+ details: "modified #{modified} facts"
88
+ }
89
+ end
90
+
65
91
  fb = Factbase.new
66
92
  rows = [
67
- insert(fb, 100_000),
93
+ insert(fb, 25_000),
68
94
  query(fb, '(gt time \'2024-03-23T03:21:43Z\')'),
69
95
  query(fb, '(gt cost 50)'),
70
96
  query(fb, '(eq title \'Object Thinking 5000\')'),
71
97
  query(fb, '(and (eq foo 42.998) (or (gt bar 200) (absent zzz)))'),
72
98
  query(fb, '(eq id (agg (always) (max id)))'),
73
99
  query(fb, '(join "c<=cost,b<=bar" (eq id (agg (always) (max id))))'),
74
- impex(fb)
100
+ txn(fb, 'query()') do |fbt|
101
+ 100.times do |i|
102
+ fbt.query("(gt foo #{i})").each.to_a
103
+ end
104
+ end,
105
+ txn(fb, 'insert()') do |fbt|
106
+ 100.times do
107
+ fbt.insert
108
+ end
109
+ end,
110
+ txn(fb, 'add()') do |fbt|
111
+ 100.times do |i|
112
+ fbt.query("(gt foo #{i})").each.to_a.first.bar = 55
113
+ end
114
+ end,
115
+ txn(fb, 'delete!()') do |fbt|
116
+ 50.times do |i|
117
+ fbt.query("(gt foo #{100 - i})").delete!
118
+ end
119
+ end,
120
+ impex(fb),
121
+ delete(fb, '(gt cost 3)'),
122
+ delete(fb, '(gt bar 1)')
75
123
  ].map { |r| "| #{r[:title]} | #{format('%0.3f', r[:time])} | #{r[:details]} |" }
76
124
 
77
125
  puts '| Action | Seconds | Details |'
@@ -41,8 +41,8 @@ class Factbase::Accum
41
41
  kk = args[1].to_s
42
42
  vv = @props[kk].nil? ? [] : @props[kk]
43
43
  vvv = @fact.method_missing(*args)
44
- vvv = [vvv] unless vvv.nil? || vvv.is_a?(Array)
45
- vv += vvv unless vvv.nil?
44
+ vvv = [vvv] unless vvv.nil? || vvv.respond_to?(:each)
45
+ vv += vvv.to_a unless vvv.nil?
46
46
  vv.uniq!
47
47
  vv.empty? ? nil : vv
48
48
  elsif @props[k].nil?
@@ -33,4 +33,12 @@ class Factbase::Churn
33
33
  @added += add
34
34
  end
35
35
  end
36
+
37
+ def +(other)
38
+ Factbase::Churn.new(
39
+ @inserted + other.inserted,
40
+ @deleted + other.deleted,
41
+ @added + other.added
42
+ )
43
+ end
36
44
  end
@@ -50,7 +50,7 @@ class Factbase::Looged
50
50
  if rollback
51
51
  @loog.debug("Txn ##{id} rolled back in #{start.ago}")
52
52
  else
53
- @loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'} the factbase in #{start.ago}")
53
+ @loog.debug("Txn ##{id} #{r.positive? ? 'modified' : 'didn\'t touch'} the factbase in #{start.ago}")
54
54
  end
55
55
  r
56
56
  end
@@ -14,15 +14,18 @@ require_relative '../factbase'
14
14
  class Factbase::Taped
15
15
  attr_reader :inserted, :deleted, :added
16
16
 
17
- def initialize(origin)
17
+ def initialize(origin, lookup: {})
18
18
  @origin = origin
19
19
  @inserted = []
20
20
  @deleted = []
21
21
  @added = []
22
+ @lookup = lookup
22
23
  end
23
24
 
24
- def modified?
25
- !@inserted.empty? || !@deleted.empty? || !@added.empty?
25
+ def find_by_object_id(oid)
26
+ o = @lookup[oid]
27
+ o = @origin.find { |m| m.object_id == oid } if o.nil?
28
+ o
26
29
  end
27
30
 
28
31
  def size
@@ -31,11 +34,17 @@ class Factbase::Taped
31
34
 
32
35
  def <<(map)
33
36
  @origin << (map)
37
+ # rubocop:disable Lint/HashCompareByIdentity
38
+ @lookup[map.object_id] = map
39
+ # rubocop:enable Lint/HashCompareByIdentity
34
40
  @inserted.append(map.object_id)
35
41
  end
36
42
 
37
43
  def each
38
44
  @origin.each do |m|
45
+ # rubocop:disable Lint/HashCompareByIdentity
46
+ @lookup[m.object_id] = m
47
+ # rubocop:enable Lint/HashCompareByIdentity
39
48
  yield TapedHash.new(m, @added)
40
49
  end
41
50
  end
@@ -43,7 +52,10 @@ class Factbase::Taped
43
52
  def delete_if
44
53
  @origin.delete_if do |m|
45
54
  r = yield m
46
- @deleted.append(m.object_id) if r
55
+ if r
56
+ @lookup.delete(m.object_id)
57
+ @deleted.append(m.object_id)
58
+ end
47
59
  r
48
60
  end
49
61
  end
@@ -57,7 +69,7 @@ class Factbase::Taped
57
69
 
58
70
  def [](key)
59
71
  v = @origin[key]
60
- v = TapedArray.new(v, @origin.object_id, @added) if v.is_a?(Array)
72
+ v = TapedArray.new(v, @origin.object_id, @added) if v.respond_to?(:each)
61
73
  v
62
74
  end
63
75
 
@@ -75,6 +87,19 @@ class Factbase::Taped
75
87
  @added = added
76
88
  end
77
89
 
90
+ def each(&)
91
+ @origin.each(&)
92
+ end
93
+
94
+ def to_a
95
+ @origin.to_a
96
+ end
97
+
98
+ def any?(&)
99
+ p 1
100
+ @origin.any?(&)
101
+ end
102
+
78
103
  def <<(item)
79
104
  @added.append(@oid)
80
105
  @origin << (item)
data/lib/factbase/term.rb CHANGED
@@ -177,7 +177,7 @@ class Factbase::Term
177
177
  v = v.evaluate(fact, maps) if v.is_a?(Factbase::Term)
178
178
  v = fact[v.to_s] if v.is_a?(Symbol)
179
179
  return v if v.nil?
180
- v = [v] unless v.is_a?(Array)
180
+ v = [v] unless v.respond_to?(:each)
181
181
  v
182
182
  end
183
183
  end
@@ -50,7 +50,7 @@ module Factbase::Term::Aggregates
50
50
  maps.each do |m|
51
51
  vv = m[k.to_s]
52
52
  next if vv.nil?
53
- vv = [vv] unless vv.is_a?(Array)
53
+ vv = [vv] unless vv.respond_to?(:each)
54
54
  vv.each do |v|
55
55
  sum += v
56
56
  end
@@ -82,7 +82,7 @@ module Factbase::Term::Aggregates
82
82
  maps.each do |m|
83
83
  vv = m[k.to_s]
84
84
  next if vv.nil?
85
- vv = [vv] unless vv.is_a?(Array)
85
+ vv = [vv] unless vv.respond_to?(:each)
86
86
  vv.each do |v|
87
87
  best = v if best.nil? || yield(v, best)
88
88
  end
@@ -77,7 +77,7 @@ module Factbase::Term::Logical
77
77
  end
78
78
 
79
79
  def _only_bool(val, pos)
80
- val = val[0] if val.is_a?(Array)
80
+ val = val[0] if val.respond_to?(:each)
81
81
  return false if val.nil?
82
82
  return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)
83
83
  raise "Boolean expected, while #{val.class} received from #{@operands[pos]}"
@@ -25,7 +25,7 @@ module Factbase::Term::Meta
25
25
  assert_args(1)
26
26
  v = by_symbol(0, fact)
27
27
  return 0 if v.nil?
28
- return 1 unless v.is_a?(Array)
28
+ return 1 unless v.respond_to?(:each)
29
29
  v.size
30
30
  end
31
31
 
@@ -33,7 +33,7 @@ module Factbase::Term::Meta
33
33
  assert_args(1)
34
34
  v = by_symbol(0, fact)
35
35
  return 'nil' if v.nil?
36
- v = v[0] if v.is_a?(Array) && v.size == 1
36
+ v = v[0] if v.respond_to?(:each) && v.size == 1
37
37
  v.class.to_s
38
38
  end
39
39
 
@@ -24,7 +24,7 @@ module Factbase::Term::Ordering
24
24
  assert_args(1)
25
25
  vv = the_values(0, fact, maps)
26
26
  return false if vv.nil?
27
- vv = [vv] unless vv.is_a?(Array)
27
+ vv = [vv] unless vv.respond_to?(:each)
28
28
  vv.each do |v|
29
29
  return false if @uniques.include?(v)
30
30
  @uniques << v
data/lib/factbase.rb CHANGED
@@ -64,7 +64,7 @@ require 'yaml'
64
64
  # License:: MIT
65
65
  class Factbase
66
66
  # Current version of the gem (changed by .rultor.yml on every release)
67
- VERSION = '0.6.0'
67
+ VERSION = '0.7.0'
68
68
 
69
69
  # An exception that may be thrown in a transaction, to roll it back.
70
70
  class Rollback < StandardError; end
@@ -147,44 +147,52 @@ class Factbase
147
147
  # A the end of this script, the factbase will be empty. No facts will
148
148
  # inserted and all changes that happened in the block will be rolled back.
149
149
  #
150
- # @return [Boolean] TRUE if some changes have been made, FALSE otherwise
150
+ # @return [Integer] How many facts have been changed (zero if rolled back)
151
151
  def txn
152
152
  pairs = {}
153
+ lookup = {}
153
154
  before =
154
155
  @mutex.synchronize do
155
156
  @maps.map do |m|
156
157
  n = m.transform_values(&:dup)
157
158
  # rubocop:disable Lint/HashCompareByIdentity
158
159
  pairs[n.object_id] = m.object_id
160
+ lookup[n.object_id] = n
159
161
  # rubocop:enable Lint/HashCompareByIdentity
160
162
  n
161
163
  end
162
164
  end
163
165
  require_relative 'factbase/taped'
164
- taped = Factbase::Taped.new(before)
166
+ taped = Factbase::Taped.new(before, lookup:)
165
167
  begin
166
168
  require_relative 'factbase/light'
167
169
  yield Factbase::Light.new(Factbase.new(taped, cache: @cache), @cache)
168
170
  rescue Factbase::Rollback
169
- return false
171
+ return 0
170
172
  end
173
+ modified = []
171
174
  @mutex.synchronize do
172
175
  taped.inserted.each do |oid|
173
- b = before.find { |m| m.object_id == oid }
176
+ b = taped.find_by_object_id(oid)
174
177
  next if b.nil?
175
178
  @maps << b
179
+ modified << oid
176
180
  end
181
+ garbage = []
177
182
  taped.added.each do |oid|
178
- b = before.find { |m| m.object_id == oid }
183
+ b = taped.find_by_object_id(oid)
179
184
  next if b.nil?
180
- @maps.delete_if { |m| m.object_id == pairs[oid] }
185
+ garbage << pairs[oid]
181
186
  @maps << b
187
+ modified << oid
182
188
  end
183
189
  taped.deleted.each do |oid|
184
- @maps.delete_if { |m| m.object_id == pairs[oid] }
190
+ garbage << pairs[oid]
191
+ modified << oid
185
192
  end
186
- taped.modified?
193
+ @maps.delete_if { |m| garbage.include?(m.object_id) }
187
194
  end
195
+ modified.uniq.count
188
196
  end
189
197
 
190
198
  # Export it into a chain of bytes.
@@ -28,4 +28,13 @@ class TestChurn < Minitest::Test
28
28
  c = Factbase::Churn.new
29
29
  assert_predicate(c.dup, :zero?)
30
30
  end
31
+
32
+ def test_concatenates_with_other
33
+ c1 = Factbase::Churn.new
34
+ c1.append(1, 6, 3)
35
+ c2 = Factbase::Churn.new
36
+ c2.append(3, 2, 46)
37
+ c3 = c1 + c2
38
+ assert_equal('4i/8d/49a', c3.to_s)
39
+ end
31
40
  end
@@ -51,7 +51,7 @@ class TestLooged < Minitest::Test
51
51
  def test_with_txn_rollback
52
52
  log = Loog::Buffer.new
53
53
  fb = Factbase::Looged.new(Factbase.new, log)
54
- refute(fb.txn { raise Factbase::Rollback })
54
+ assert_equal(0, fb.txn { raise Factbase::Rollback })
55
55
  assert_equal(0, fb.size)
56
56
  assert_includes(log.to_s, 'rolled back', log)
57
57
  refute_includes(log.to_s, 'didn\'t touch', log)
@@ -61,15 +61,15 @@ class TestLooged < Minitest::Test
61
61
  log = Loog::Buffer.new
62
62
  fb = Factbase::Looged.new(Factbase.new, log)
63
63
  fb.insert.foo = 1
64
- refute(fb.txn { |fbt| fbt.query('(always)').each.to_a }, log)
65
- assert(fb.txn { |fbt| fbt.query('(always)').each.to_a[0].foo = 42 })
64
+ assert_equal(0, fb.txn { |fbt| fbt.query('(always)').each.to_a }, log)
65
+ assert_equal(1, fb.txn { |fbt| fbt.query('(always)').each.to_a[0].foo = 42 })
66
66
  assert_includes(log.to_s, 'modified', log)
67
67
  end
68
68
 
69
69
  def test_with_empty_txn
70
70
  log = Loog::Buffer.new
71
71
  fb = Factbase::Looged.new(Factbase.new, log)
72
- refute(fb.txn { |fbt| fbt.query('(always)').each.to_a })
72
+ assert_equal(0, fb.txn { |fbt| fbt.query('(always)').each.to_a })
73
73
  assert_includes(log.to_s, 'didn\'t touch', log)
74
74
  end
75
75
 
@@ -66,6 +66,6 @@ class TestTallied < Minitest::Test
66
66
  assert_equal(0, fb.size)
67
67
  assert_equal(t, fb.churn.inserted)
68
68
  assert_equal(t, fb.churn.deleted)
69
- assert_equal(t, fb.churn.added)
69
+ assert_equal(t * 2, fb.churn.added)
70
70
  end
71
71
  end
@@ -16,7 +16,6 @@ class TestTaped < Minitest::Test
16
16
  t = Factbase::Taped.new([])
17
17
  t << {}
18
18
  assert_equal(1, t.inserted.size)
19
- assert_predicate(t, :modified?)
20
19
  end
21
20
 
22
21
  def test_tracks_deletion
@@ -104,12 +104,11 @@ class TestFactbase < Minitest::Test
104
104
 
105
105
  def test_txn_returns_boolean
106
106
  fb = Factbase.new
107
- assert_kind_of(FalseClass, fb.txn { true })
108
- assert_kind_of(TrueClass, fb.txn(&:insert))
109
- assert(fb.txn { |fbt| fbt.insert.bar = 42 })
110
- refute(fb.txn { |fbt| fbt.query('(always)').each.to_a })
111
- assert(fb.txn { |fbt| fbt.query('(always)').each { |f| f.hello = 33 } })
112
- assert(fb.txn { |fbt| fbt.query('(always)').each.to_a[0].zzz = 33 })
107
+ assert_equal(1, fb.txn(&:insert))
108
+ assert_equal(1, fb.txn { |fbt| fbt.insert.bar = 42 })
109
+ assert_equal(0, fb.txn { |fbt| fbt.query('(always)').each.to_a })
110
+ assert_equal(2, fb.txn { |fbt| fbt.query('(always)').each { |f| f.hello = 33 } })
111
+ assert_equal(1, fb.txn { |fbt| fbt.query('(always)').each.to_a[0].zzz = 33 })
113
112
  end
114
113
 
115
114
  def test_appends_in_txn
@@ -136,6 +135,16 @@ class TestFactbase < Minitest::Test
136
135
  assert_equal(2, fb.size)
137
136
  end
138
137
 
138
+ def test_deals_with_arrays_in_txn
139
+ fb = Factbase.new
140
+ f = fb.insert
141
+ f.foo = 1
142
+ f.foo = 2
143
+ fb.txn do |fbt|
144
+ assert_equal(1, fbt.query('(gt foo 0)').each.to_a.size)
145
+ end
146
+ end
147
+
139
148
  def test_run_txn_via_query
140
149
  fb = Factbase.new
141
150
  fb.insert.foo = 1
@@ -205,7 +214,7 @@ class TestFactbase < Minitest::Test
205
214
  fbt.insert.bar = 33
206
215
  raise Factbase::Rollback
207
216
  end
208
- refute(modified)
217
+ assert_equal(0, modified)
209
218
  assert_equal(0, fb.query('(always)').each.to_a.size)
210
219
  end
211
220
 
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-21 00:00:00.000000000 Z
11
+ date: 2025-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backtrace