paper_trail_diff 0.11.0 → 0.12.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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +24 -3
- data/lib/paper_trail_diff/nested_comparator.rb +22 -11
- data/lib/paper_trail_diff/value_objects.rb +66 -0
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff.rb +3 -1
- data/sig/generated/paper_trail_diff/nested_comparator.rbs +16 -11
- data/sig/generated/paper_trail_diff/value_objects.rbs +45 -0
- data/sig/generated/paper_trail_diff.rbs +6 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e747358edb04ac2ea49e43b11eb8b5b8e811c426b50b017ed6d1c72162bb4632
|
|
4
|
+
data.tar.gz: b330e3bb80e43e478a0e88ff7afeb2b6576d3550af0a54075948fb0e60dc045d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8bcf171950fd74de94c8118dabf3f911e8db3923ce4ed48463c505b3b5748bafbb9c39819b7af49841bf3ea3f6635e797c9536f5d845a42f0a9df9eb1018d1de
|
|
7
|
+
data.tar.gz: 2eb4d0d286b9daf4ed91e1d0d674c6835569ef3912a2712034fad27d31648c5eeed1e410f34fbfbec3d59808b5d82f178df84b896a233eaec570c3b24b6b52ab
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file. The
|
|
4
4
|
project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.12.0] - 2026-08-19
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Report an array inside a JSON column by membership rather than as one opaque
|
|
11
|
+
value. `nested_changes` now returns a `PaperTrailDiff::ArrayChange` for a pair
|
|
12
|
+
of arrays, carrying `added`, `removed` and `reordered?` alongside the whole
|
|
13
|
+
`from` and `to` it already had. Position is deliberately not reported:
|
|
14
|
+
elements carry no identity, so an insertion at the front makes every later
|
|
15
|
+
index look changed and one insertion reads as several edits. Membership is
|
|
16
|
+
answerable without claiming any pairing. Matching is by value and respects
|
|
17
|
+
duplicates, so `["a", "a"]` to `["a"]` reports one removal. The one thing
|
|
18
|
+
membership cannot see -- the same elements in a new order -- is named rather
|
|
19
|
+
than passed over. An element that is itself an object is reported whole,
|
|
20
|
+
since saying which of its fields changed would require pairing before- and
|
|
21
|
+
after-elements that nothing in the value licenses.
|
|
22
|
+
|
|
6
23
|
## [0.11.0] - 2026-08-18
|
|
7
24
|
|
|
8
25
|
### Added
|
data/README.md
CHANGED
|
@@ -386,9 +386,30 @@ PaperTrailDiff.nested_changes({ "a.b" => 1, "a" => { "b" => 1 } },
|
|
|
386
386
|
# => { ["a.b"] => <from 1 to 2>, ["a", "b"] => <from 1 to 3> }
|
|
387
387
|
```
|
|
388
388
|
|
|
389
|
-
**Arrays are reported
|
|
390
|
-
an insertion at the front would make every later index look
|
|
391
|
-
|
|
389
|
+
**Arrays are reported by membership, not by position.** Elements carry no
|
|
390
|
+
identity, so an insertion at the front would make every later index look
|
|
391
|
+
changed — one insertion described as several edits. An `ArrayChange` says what
|
|
392
|
+
was added and removed, matching by value and respecting duplicates:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
change = PaperTrailDiff.nested_changes(
|
|
396
|
+
{ "keywords" => %w[apollo nasa] },
|
|
397
|
+
{ "keywords" => %w[saturn apollo nasa] }
|
|
398
|
+
).fetch(["keywords"])
|
|
399
|
+
|
|
400
|
+
change.added # => ["saturn"]
|
|
401
|
+
change.removed # => []
|
|
402
|
+
change.reordered? # => false
|
|
403
|
+
change.from # => ["apollo", "nasa"] (the whole values remain)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Membership cannot see one thing, so it is named rather than passed over: the
|
|
407
|
+
same elements in a new order report `reordered?` as true with nothing added or
|
|
408
|
+
removed.
|
|
409
|
+
|
|
410
|
+
An element that is itself an object is reported whole. Saying which field of
|
|
411
|
+
which object changed would need a pairing of before-elements with
|
|
412
|
+
after-elements, and nothing in the value licenses that pairing.
|
|
392
413
|
|
|
393
414
|
**An absent key is not a null one.** `{"a": null}` and `{}` mean different things
|
|
394
415
|
in JSON, and an audit trail that showed them alike would be lying about one of
|
|
@@ -16,11 +16,11 @@ module PaperTrailDiff
|
|
|
16
16
|
# names and locales routinely do -- and joining would make `a.b` ambiguous
|
|
17
17
|
# between one key and two.
|
|
18
18
|
#
|
|
19
|
-
# Arrays are
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
19
|
+
# Arrays are reported by membership, not by position -- see ArrayChange. Their
|
|
20
|
+
# elements carry no identity, so an insertion at the front makes every later
|
|
21
|
+
# index look changed, and one insertion reads as several edits. What is added
|
|
22
|
+
# and removed can be answered without claiming any pairing; what cannot is the
|
|
23
|
+
# same elements in a new order, so that is named rather than passed over.
|
|
24
24
|
#
|
|
25
25
|
# An absent key is not a null one. `{"a": null}` and `{}` mean different
|
|
26
26
|
# things in JSON and an audit trail that conflated them would be lying about
|
|
@@ -32,7 +32,7 @@ module PaperTrailDiff
|
|
|
32
32
|
def ABSENT.to_s = 'absent'
|
|
33
33
|
ABSENT.freeze
|
|
34
34
|
|
|
35
|
-
#: (untyped, untyped) ->
|
|
35
|
+
#: (untyped, untyped) -> nested_changes
|
|
36
36
|
def self.call(from_value, to_value)
|
|
37
37
|
new(from_value, to_value).call
|
|
38
38
|
end
|
|
@@ -46,12 +46,12 @@ module PaperTrailDiff
|
|
|
46
46
|
# Returns the changed paths, or an empty hash when the pair is not two
|
|
47
47
|
# structures this can look inside. An empty result therefore means "nothing
|
|
48
48
|
# to report at this depth", and the caller still has the whole-value change.
|
|
49
|
-
#: () ->
|
|
49
|
+
#: () -> nested_changes
|
|
50
50
|
def call
|
|
51
51
|
from_structure, to_structure = structures
|
|
52
52
|
return {} unless from_structure && to_structure
|
|
53
53
|
|
|
54
|
-
changes = {} #:
|
|
54
|
+
changes = {} #: nested_changes
|
|
55
55
|
walk(from_structure, to_structure, [], changes)
|
|
56
56
|
changes.freeze
|
|
57
57
|
end
|
|
@@ -82,22 +82,33 @@ module PaperTrailDiff
|
|
|
82
82
|
parsed if parsed.is_a?(Hash)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
#: (Hash[untyped, untyped], Hash[untyped, untyped], Array[String],
|
|
85
|
+
#: (Hash[untyped, untyped], Hash[untyped, untyped], Array[String], nested_changes) -> void
|
|
86
86
|
def walk(from_hash, to_hash, path, changes)
|
|
87
87
|
keys(from_hash, to_hash).each do |key|
|
|
88
88
|
from_item = from_hash.key?(key) ? from_hash[key] : ABSENT
|
|
89
89
|
to_item = to_hash.key?(key) ? to_hash[key] : ABSENT
|
|
90
90
|
next if from_item == to_item
|
|
91
91
|
|
|
92
|
-
here = [*path, key.to_s]
|
|
92
|
+
here = [*path, key.to_s].freeze
|
|
93
93
|
if from_item.is_a?(Hash) && to_item.is_a?(Hash)
|
|
94
94
|
walk(from_item, to_item, here, changes)
|
|
95
95
|
else
|
|
96
|
-
changes[here
|
|
96
|
+
changes[here] = change_for(from_item, to_item)
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# Two arrays are a membership change; anything else is a change to the
|
|
102
|
+
# value as a whole, including an array facing something that is not one.
|
|
103
|
+
#: (untyped, untyped) -> nested_change
|
|
104
|
+
def change_for(from_item, to_item)
|
|
105
|
+
if from_item.is_a?(Array) && to_item.is_a?(Array)
|
|
106
|
+
ArrayChange.new(from: from_item, to: to_item)
|
|
107
|
+
else
|
|
108
|
+
ValueChange.new(from: from_item, to: to_item)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
101
112
|
# Sorted so a report reads the same twice, and stringified because a hash
|
|
102
113
|
# loaded from JSON and one built in Ruby can key the same field differently.
|
|
103
114
|
#: (Hash[untyped, untyped], Hash[untyped, untyped]) -> Array[untyped]
|
|
@@ -20,6 +20,72 @@ module PaperTrailDiff
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# A change to an array inside a value the database stores whole, reported by
|
|
24
|
+
# membership rather than by position.
|
|
25
|
+
#
|
|
26
|
+
# Position would be the obvious thing to report and the wrong one: elements
|
|
27
|
+
# here carry no identity, so inserting at the front makes every later index
|
|
28
|
+
# look changed, and one insertion is described as several edits. Membership is
|
|
29
|
+
# answerable without claiming any pairing -- "gained \"saturn v\"" is true
|
|
30
|
+
# whether the array is a set, a queue, or a ranked list.
|
|
31
|
+
#
|
|
32
|
+
# That leaves one case membership cannot see, so it is named rather than
|
|
33
|
+
# hidden: the same elements in a different order. `reordered?` says so.
|
|
34
|
+
#
|
|
35
|
+
# An element that is itself a Hash or Array is reported whole. Saying which
|
|
36
|
+
# field of which object changed would require pairing before-elements with
|
|
37
|
+
# after-elements, and nothing in the value licenses that pairing.
|
|
38
|
+
class ArrayChange
|
|
39
|
+
attr_reader :from #: Array[untyped]
|
|
40
|
+
attr_reader :to #: Array[untyped]
|
|
41
|
+
attr_reader :added #: Array[untyped]
|
|
42
|
+
attr_reader :removed #: Array[untyped]
|
|
43
|
+
|
|
44
|
+
#: (from: Array[untyped], to: Array[untyped]) -> void
|
|
45
|
+
def initialize(from:, to:)
|
|
46
|
+
@from = Support.immutable_copy(from)
|
|
47
|
+
@to = Support.immutable_copy(to)
|
|
48
|
+
@added = Support.immutable_copy(ArrayChange.surplus(to, from))
|
|
49
|
+
@removed = Support.immutable_copy(ArrayChange.surplus(from, to))
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Elements of `left` with no counterpart left in `right`, matching by value
|
|
54
|
+
# and respecting duplicates, so ["a", "a"] -> ["a"] reports one removal
|
|
55
|
+
# rather than none.
|
|
56
|
+
#: (Array[untyped], Array[untyped]) -> Array[untyped]
|
|
57
|
+
def self.surplus(left, right)
|
|
58
|
+
pool = right.dup
|
|
59
|
+
left.reject do |item|
|
|
60
|
+
index = pool.index(item)
|
|
61
|
+
index ? pool.delete_at(index) || true : false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Same elements, different sequence. Only meaningful for a change that was
|
|
66
|
+
# recorded at all, which this only is when the two sides differ.
|
|
67
|
+
#: () -> bool
|
|
68
|
+
def reordered?
|
|
69
|
+
added.empty? && removed.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#: () -> bool
|
|
73
|
+
def empty?
|
|
74
|
+
from == to
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#: () -> Hash[Symbol, untyped]
|
|
78
|
+
def to_h
|
|
79
|
+
{
|
|
80
|
+
from: Support.serialize(from),
|
|
81
|
+
to: Support.serialize(to),
|
|
82
|
+
added: Support.serialize(added),
|
|
83
|
+
removed: Support.serialize(removed),
|
|
84
|
+
reordered: reordered?
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
23
89
|
# Attribute and nested-association changes for a record whose identity did not change.
|
|
24
90
|
class RecordChange
|
|
25
91
|
attr_reader :record #: RecordReference
|
data/lib/paper_trail_diff.rb
CHANGED
|
@@ -92,6 +92,8 @@ require_relative 'paper_trail_diff/paper_trail_adapter'
|
|
|
92
92
|
# type attribute_changes = Hash[String, ValueChange]
|
|
93
93
|
# type association_diff = ToOneAssociationDiff | CollectionAssociationDiff
|
|
94
94
|
# type association_diffs = Hash[String, association_diff]
|
|
95
|
+
# type nested_change = ValueChange | ArrayChange
|
|
96
|
+
# type nested_changes = Hash[Array[String], nested_change]
|
|
95
97
|
# type association_snapshots = Hash[String, AssociationSnapshot]
|
|
96
98
|
# type identity = Array[untyped]
|
|
97
99
|
# type comparison_input = Hash[String | Symbol, untyped]
|
|
@@ -344,7 +346,7 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
|
344
346
|
# list that merely shifted would otherwise look changed throughout. A key
|
|
345
347
|
# that was absent reads as `NestedComparator::ABSENT` rather than nil, which
|
|
346
348
|
# JSON uses for a present null.
|
|
347
|
-
#: (untyped, ?untyped) ->
|
|
349
|
+
#: (untyped, ?untyped) -> nested_changes
|
|
348
350
|
def nested_changes(change, to_value = nil)
|
|
349
351
|
# Tested by type rather than by responding to `from`: ActiveSupport gives
|
|
350
352
|
# String#from, so duck-typing here quietly reads a plain string as a pair.
|
|
@@ -13,11 +13,11 @@ module PaperTrailDiff
|
|
|
13
13
|
# names and locales routinely do -- and joining would make `a.b` ambiguous
|
|
14
14
|
# between one key and two.
|
|
15
15
|
#
|
|
16
|
-
# Arrays are
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
16
|
+
# Arrays are reported by membership, not by position -- see ArrayChange. Their
|
|
17
|
+
# elements carry no identity, so an insertion at the front makes every later
|
|
18
|
+
# index look changed, and one insertion reads as several edits. What is added
|
|
19
|
+
# and removed can be answered without claiming any pairing; what cannot is the
|
|
20
|
+
# same elements in a new order, so that is named rather than passed over.
|
|
21
21
|
#
|
|
22
22
|
# An absent key is not a null one. `{"a": null}` and `{}` mean different
|
|
23
23
|
# things in JSON and an audit trail that conflated them would be lying about
|
|
@@ -30,8 +30,8 @@ module PaperTrailDiff
|
|
|
30
30
|
|
|
31
31
|
def to_s: () -> untyped
|
|
32
32
|
|
|
33
|
-
# : (untyped, untyped) ->
|
|
34
|
-
def self.call: (untyped, untyped) ->
|
|
33
|
+
# : (untyped, untyped) -> nested_changes
|
|
34
|
+
def self.call: (untyped, untyped) -> nested_changes
|
|
35
35
|
|
|
36
36
|
# : (untyped, untyped) -> void
|
|
37
37
|
def initialize: (untyped, untyped) -> void
|
|
@@ -39,8 +39,8 @@ module PaperTrailDiff
|
|
|
39
39
|
# Returns the changed paths, or an empty hash when the pair is not two
|
|
40
40
|
# structures this can look inside. An empty result therefore means "nothing
|
|
41
41
|
# to report at this depth", and the caller still has the whole-value change.
|
|
42
|
-
# : () ->
|
|
43
|
-
def call: () ->
|
|
42
|
+
# : () -> nested_changes
|
|
43
|
+
def call: () -> nested_changes
|
|
44
44
|
|
|
45
45
|
private
|
|
46
46
|
|
|
@@ -57,8 +57,13 @@ module PaperTrailDiff
|
|
|
57
57
|
# : (untyped) -> Hash[untyped, untyped]?
|
|
58
58
|
def structure: (untyped) -> Hash[untyped, untyped]?
|
|
59
59
|
|
|
60
|
-
# : (Hash[untyped, untyped], Hash[untyped, untyped], Array[String],
|
|
61
|
-
def walk: (Hash[untyped, untyped], Hash[untyped, untyped], Array[String],
|
|
60
|
+
# : (Hash[untyped, untyped], Hash[untyped, untyped], Array[String], nested_changes) -> void
|
|
61
|
+
def walk: (Hash[untyped, untyped], Hash[untyped, untyped], Array[String], nested_changes) -> void
|
|
62
|
+
|
|
63
|
+
# Two arrays are a membership change; anything else is a change to the
|
|
64
|
+
# value as a whole, including an array facing something that is not one.
|
|
65
|
+
# : (untyped, untyped) -> nested_change
|
|
66
|
+
def change_for: (untyped, untyped) -> nested_change
|
|
62
67
|
|
|
63
68
|
# Sorted so a report reads the same twice, and stringified because a hash
|
|
64
69
|
# loaded from JSON and one built in Ruby can key the same field differently.
|
|
@@ -14,6 +14,51 @@ module PaperTrailDiff
|
|
|
14
14
|
def to_h: () -> Hash[Symbol, untyped]
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# A change to an array inside a value the database stores whole, reported by
|
|
18
|
+
# membership rather than by position.
|
|
19
|
+
#
|
|
20
|
+
# Position would be the obvious thing to report and the wrong one: elements
|
|
21
|
+
# here carry no identity, so inserting at the front makes every later index
|
|
22
|
+
# look changed, and one insertion is described as several edits. Membership is
|
|
23
|
+
# answerable without claiming any pairing -- "gained \"saturn v\"" is true
|
|
24
|
+
# whether the array is a set, a queue, or a ranked list.
|
|
25
|
+
#
|
|
26
|
+
# That leaves one case membership cannot see, so it is named rather than
|
|
27
|
+
# hidden: the same elements in a different order. `reordered?` says so.
|
|
28
|
+
#
|
|
29
|
+
# An element that is itself a Hash or Array is reported whole. Saying which
|
|
30
|
+
# field of which object changed would require pairing before-elements with
|
|
31
|
+
# after-elements, and nothing in the value licenses that pairing.
|
|
32
|
+
class ArrayChange
|
|
33
|
+
attr_reader from: Array[untyped]
|
|
34
|
+
|
|
35
|
+
attr_reader to: Array[untyped]
|
|
36
|
+
|
|
37
|
+
attr_reader added: Array[untyped]
|
|
38
|
+
|
|
39
|
+
attr_reader removed: Array[untyped]
|
|
40
|
+
|
|
41
|
+
# : (from: Array[untyped], to: Array[untyped]) -> void
|
|
42
|
+
def initialize: (from: Array[untyped], to: Array[untyped]) -> void
|
|
43
|
+
|
|
44
|
+
# Elements of `left` with no counterpart left in `right`, matching by value
|
|
45
|
+
# and respecting duplicates, so ["a", "a"] -> ["a"] reports one removal
|
|
46
|
+
# rather than none.
|
|
47
|
+
# : (Array[untyped], Array[untyped]) -> Array[untyped]
|
|
48
|
+
def self.surplus: (Array[untyped], Array[untyped]) -> Array[untyped]
|
|
49
|
+
|
|
50
|
+
# Same elements, different sequence. Only meaningful for a change that was
|
|
51
|
+
# recorded at all, which this only is when the two sides differ.
|
|
52
|
+
# : () -> bool
|
|
53
|
+
def reordered?: () -> bool
|
|
54
|
+
|
|
55
|
+
# : () -> bool
|
|
56
|
+
def empty?: () -> bool
|
|
57
|
+
|
|
58
|
+
# : () -> Hash[Symbol, untyped]
|
|
59
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
60
|
+
end
|
|
61
|
+
|
|
17
62
|
# Attribute and nested-association changes for a record whose identity did not change.
|
|
18
63
|
class RecordChange
|
|
19
64
|
attr_reader record: RecordReference
|
|
@@ -99,8 +99,8 @@ module PaperTrailDiff
|
|
|
99
99
|
# list that merely shifted would otherwise look changed throughout. A key
|
|
100
100
|
# that was absent reads as `NestedComparator::ABSENT` rather than nil, which
|
|
101
101
|
# JSON uses for a present null.
|
|
102
|
-
# : (untyped, ?untyped) ->
|
|
103
|
-
def self.nested_changes: (untyped, ?untyped) ->
|
|
102
|
+
# : (untyped, ?untyped) -> nested_changes
|
|
103
|
+
def self.nested_changes: (untyped, ?untyped) -> nested_changes
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
module PaperTrailDiff
|
|
@@ -114,6 +114,10 @@ module PaperTrailDiff
|
|
|
114
114
|
|
|
115
115
|
type association_diffs = Hash[String, association_diff]
|
|
116
116
|
|
|
117
|
+
type nested_change = ValueChange | ArrayChange
|
|
118
|
+
|
|
119
|
+
type nested_changes = Hash[Array[String], nested_change]
|
|
120
|
+
|
|
117
121
|
type association_snapshots = Hash[String, AssociationSnapshot]
|
|
118
122
|
|
|
119
123
|
type identity = Array[untyped]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: paper_trail_diff
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Williams
|
|
@@ -212,11 +212,11 @@ licenses:
|
|
|
212
212
|
metadata:
|
|
213
213
|
allowed_push_host: https://rubygems.org
|
|
214
214
|
bug_tracker_uri: https://github.com/aheathwilliams/paper_trail_diff/issues
|
|
215
|
-
changelog_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.
|
|
216
|
-
documentation_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.
|
|
215
|
+
changelog_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.12.0/CHANGELOG.md
|
|
216
|
+
documentation_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.12.0/README.md
|
|
217
217
|
homepage_uri: https://github.com/aheathwilliams/paper_trail_diff
|
|
218
218
|
rubygems_mfa_required: 'true'
|
|
219
|
-
source_code_uri: https://github.com/aheathwilliams/paper_trail_diff/tree/v0.
|
|
219
|
+
source_code_uri: https://github.com/aheathwilliams/paper_trail_diff/tree/v0.12.0
|
|
220
220
|
rdoc_options: []
|
|
221
221
|
require_paths:
|
|
222
222
|
- lib
|