delta 1.0.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 +7 -0
- data/README.md +190 -0
- data/lib/delta/base.rb +38 -0
- data/lib/delta/identifier.rb +37 -0
- data/lib/delta/plucker.rb +23 -0
- data/lib/delta/set_operator/active_record.rb +46 -0
- data/lib/delta/set_operator/enumerable.rb +35 -0
- data/lib/delta/set_operator.rb +46 -0
- data/lib/delta.rb +6 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b03dae554a7f32d4fc3826fd35aa36f82549c2b1
|
4
|
+
data.tar.gz: 9b0bf35d479c98b32d2eacc38c4f71128464a2ea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 55edc3a5987c896b0c296db1171a39bf80a24ada940b7bdaa59a5d5ba5464971f4ccbccfc206bd0e4bd5e330684b8c96d21124667eaf812e4bae0ce71eabb4d7
|
7
|
+
data.tar.gz: ad09180100f9018bd3feee520ae5343512d7a174efc6f633bac594650ed3d15af965196321b1cc04efdc3b1bf28739210c6b4323faed9ccb6d507f261eaf69cc
|
data/README.md
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
## Delta
|
2
|
+
|
3
|
+
[](https://travis-ci.org/tuzz/delta)
|
4
|
+
|
5
|
+
Calculates the delta between two collections of objects.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
In its simplest form, Delta calculates the additions and deletions for arbitrary
|
10
|
+
collections of objects:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
delta = Delta.new(
|
14
|
+
from: [pikachu, pidgey, magikarp],
|
15
|
+
to: [raichu, pidgey, butterfree]
|
16
|
+
)
|
17
|
+
|
18
|
+
delta.additions
|
19
|
+
#=> #<Enumerator: ... >
|
20
|
+
|
21
|
+
delta.additions.to_a
|
22
|
+
#=> [
|
23
|
+
#<Pokemon @species="Raichu", @name="Zappy", @type="Electric">,
|
24
|
+
#<Pokemon @species="Butterfree", @name="Flappy", @type="Flying">
|
25
|
+
]
|
26
|
+
|
27
|
+
delta.deletions.to_a
|
28
|
+
#=> [
|
29
|
+
#<Pokemon @species="Pikachu", @name="Zappy", @type="Electric">,
|
30
|
+
#<Pokemon @species="Magikarp", @name="Splashy", @type="Water">
|
31
|
+
]
|
32
|
+
```
|
33
|
+
|
34
|
+
## Pluck
|
35
|
+
|
36
|
+
Sometimes, you'll only be interested in a few of the attributes on an object.
|
37
|
+
Delta supports `pluck`, which selects only the attributes you're interested in,
|
38
|
+
instead of returning full objects:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
delta = Delta.new(
|
42
|
+
from: [pikachu, pidgey, magikarp],
|
43
|
+
to: [raichu, pidgey, butterfree],
|
44
|
+
pluck: [:name, :species]
|
45
|
+
)
|
46
|
+
|
47
|
+
delta.additions.to_a
|
48
|
+
#=> [
|
49
|
+
#<struct name="Zappy", species="Pikachu">,
|
50
|
+
#<struct name="Flappy", species="Butterfree">
|
51
|
+
]
|
52
|
+
|
53
|
+
delta.deletions.to_a
|
54
|
+
#=> [
|
55
|
+
#<struct name="Zappy", species="Raichu">,
|
56
|
+
#<struct name="Splashy", species="Magikarp">
|
57
|
+
]
|
58
|
+
```
|
59
|
+
|
60
|
+
## Modifications
|
61
|
+
|
62
|
+
In most cases, it is more appropriate to think in terms of modifications rather
|
63
|
+
than additions and deletions. In the example above, 'Pikachu' appeared as a
|
64
|
+
deletion and 'Raichu' appeared as an addition. It might make more sense to model
|
65
|
+
this as a modification, i.e. 'Zappy' changing its 'species'.
|
66
|
+
|
67
|
+
Delta supports setting the `keys` that uniquely identify each object in the
|
68
|
+
collection:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
delta = Delta.new(
|
72
|
+
from: [pikachu, pidgey, magikarp],
|
73
|
+
to: [raichu, pidgey, butterfree],
|
74
|
+
pluck: [:species, :name],
|
75
|
+
keys: [:name]
|
76
|
+
)
|
77
|
+
|
78
|
+
delta.additions.to_a
|
79
|
+
#=> [#<struct species="Butterfree", name="Flappy">]
|
80
|
+
|
81
|
+
delta.modifications.to_a
|
82
|
+
#=> [#<struct species="Raichu", name="Zappy">]
|
83
|
+
|
84
|
+
delta.deletions.to_a
|
85
|
+
#=> [#<struct species="Magikarp", name="Splashy">]
|
86
|
+
```
|
87
|
+
|
88
|
+
You should specify which attributes to `pluck` so that Delta can distinguish
|
89
|
+
between objects that have changed and objects that have not changed, but appear
|
90
|
+
in both collections.
|
91
|
+
|
92
|
+
If you do not specifiy any attributes to `pluck`, Delta will fall back to using
|
93
|
+
object equality to determine which objects have changed. For ActiveRecord
|
94
|
+
relations, Delta will use the database id as its fall back as it is more
|
95
|
+
performant to do so.
|
96
|
+
|
97
|
+
## Composite Keys
|
98
|
+
|
99
|
+
In some cases, objects will be uniquely identified by a combination of things.
|
100
|
+
For example, consider an application that enforces the uniqueness of names, but
|
101
|
+
only in the context of a 'type'. This would mean that two Pokemon can have the
|
102
|
+
same name, as long as they are not of the same type (e.g. 'Water').
|
103
|
+
|
104
|
+
In these cases, you can specify multiple keys:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
delta = Delta.new(
|
108
|
+
from: [pikachu, pidgey, magikarp],
|
109
|
+
to: [raichu, pidgey, butterfree],
|
110
|
+
pluck: [:species, :name, :type],
|
111
|
+
keys: [:name, :type] # <--
|
112
|
+
)
|
113
|
+
|
114
|
+
delta.additions.to_a
|
115
|
+
#=> [#<struct species="Butterfree", name="FANG!", type="Flying">]
|
116
|
+
|
117
|
+
delta.modifications.to_a
|
118
|
+
#=> [#<struct species="Raichu", name="Zappy", type="Electric">]
|
119
|
+
|
120
|
+
delta.deletions.to_a
|
121
|
+
#=> [#<struct species="Magikarp", name="FANG!", type="Water">]
|
122
|
+
```
|
123
|
+
|
124
|
+
Consider the alternative where 'name' is used as the only key. This would mean
|
125
|
+
that the Pokemon with species' 'Butterfree' and 'Magikarp' would be considered
|
126
|
+
the same. This is semantically incorrect for this particular domain.
|
127
|
+
|
128
|
+
TODO: What would happen if you did that? Should it raise an error?
|
129
|
+
|
130
|
+
## Many-to-one Deltas
|
131
|
+
|
132
|
+
By combining the use of `pluck` and `keys`, you can build deltas that aren't
|
133
|
+
necessarily related to a single object, but instead span multiple objects within
|
134
|
+
the collection.
|
135
|
+
|
136
|
+
This may be useful if there are objects in your collections that share some
|
137
|
+
property. In this example, all of the Pokemon have a 'type'. We can build a
|
138
|
+
Delta that shows the difference in types between collections, like so:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
delta = Delta.new(
|
142
|
+
from: [pikachu, pidgey, magikarp],
|
143
|
+
to: [raichu, pidgey, butterfree],
|
144
|
+
keys: [:type],
|
145
|
+
pluck: [:type]
|
146
|
+
)
|
147
|
+
|
148
|
+
delta.additions.to_a
|
149
|
+
#=> []
|
150
|
+
|
151
|
+
delta.modifications.to_a
|
152
|
+
#=> []
|
153
|
+
|
154
|
+
delta.deletions.to_a
|
155
|
+
#=> [#<struct type="Water">]
|
156
|
+
```
|
157
|
+
|
158
|
+
In this example, both 'Electric' and 'Flying' types appear in both collections.
|
159
|
+
The only difference is the deletion of all 'Water' type Pokemon as a result of
|
160
|
+
removing 'Magikarp' from the first collection.
|
161
|
+
|
162
|
+
## Rails Support
|
163
|
+
|
164
|
+
Delta has added support for Rails applications. If the given collections are of
|
165
|
+
class ActiveRecord::Relation, Delta will try to improve performance by reducing
|
166
|
+
the number of select queries on the database. This isn't perfect and may not
|
167
|
+
work with old versions of Rails.
|
168
|
+
|
169
|
+
## In the Wild
|
170
|
+
|
171
|
+
So far, we've talked a lot about Pokemon, but how is this useful in the
|
172
|
+
real-world?
|
173
|
+
|
174
|
+
Consider an application that is backed by an external content management system.
|
175
|
+
You have some kind of extract-transform-load process that takes content from the
|
176
|
+
CMS and pushes it into your database. Your application is serving live traffic
|
177
|
+
and needs to remain available at all times.
|
178
|
+
|
179
|
+
To minimise the amount of churn on your database, it makes sense to calculate
|
180
|
+
just the things that have changed, rather than rebuilding the entire set of
|
181
|
+
content each time. To do this, you could push the CMS content into a staging
|
182
|
+
database where you calculate a Delta without touching your live application.
|
183
|
+
|
184
|
+
Once this Delta is calculated, you can then stream the minimal set of changes
|
185
|
+
to the database that backs your application.
|
186
|
+
|
187
|
+
## Contribution
|
188
|
+
|
189
|
+
Thanks for the using this gem. If you think of a great new feature or you find a
|
190
|
+
problem, please open an issue or a pull request and I'll try to get back to you.
|
data/lib/delta/base.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
class Delta
|
2
|
+
def initialize(from:, to:, pluck: nil, keys: nil)
|
3
|
+
identifier = keys ? Identifier.new(keys) : Identifier::Null.new
|
4
|
+
self.set = SetOperator.adapt(a: from, b: to, identifier: identifier)
|
5
|
+
self.plucker = pluck ? Plucker.new(pluck) : Plucker::Null.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def additions
|
9
|
+
Enumerator.new do |y|
|
10
|
+
set.subtract_a_from_b.each do |b|
|
11
|
+
y.yield plucker.pluck(b)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def modifications
|
17
|
+
Enumerator.new do |y|
|
18
|
+
set.intersection.each do |a, b|
|
19
|
+
a_attributes = plucker.pluck(a)
|
20
|
+
b_attributes = plucker.pluck(b)
|
21
|
+
|
22
|
+
y.yield b_attributes unless a_attributes == b_attributes
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def deletions
|
28
|
+
Enumerator.new do |y|
|
29
|
+
set.subtract_b_from_a.each do |a|
|
30
|
+
y.yield plucker.pluck(a)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_accessor :set, :plucker
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Delta
|
2
|
+
class Identifier
|
3
|
+
def initialize(keys)
|
4
|
+
self.keys = keys
|
5
|
+
end
|
6
|
+
|
7
|
+
def identity(object)
|
8
|
+
cache(object) { Hash[keys.map { |k| [k, object.public_send(k)] }] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def identities(collection)
|
12
|
+
cache(collection) { Hash[keys.map { |k| [k, collection.pluck(k).uniq] }] }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_accessor :keys
|
18
|
+
|
19
|
+
def cache(key)
|
20
|
+
@cache ||= {}
|
21
|
+
@cache.key?(key) ? @cache.fetch(key) : @cache[key] = yield
|
22
|
+
end
|
23
|
+
|
24
|
+
class Null < Identifier
|
25
|
+
def initialize
|
26
|
+
end
|
27
|
+
|
28
|
+
def identity(object)
|
29
|
+
object
|
30
|
+
end
|
31
|
+
|
32
|
+
def identities(collection)
|
33
|
+
cache(collection) { { id: collection.pluck(:id) } }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Delta
|
2
|
+
class Plucker
|
3
|
+
def initialize(pluck)
|
4
|
+
self.array = pluck
|
5
|
+
self.struct = Struct.new(*array)
|
6
|
+
end
|
7
|
+
|
8
|
+
def pluck(object)
|
9
|
+
attributes = array.map { |k| object.public_send(k) }
|
10
|
+
struct.new(*attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_accessor :array, :struct
|
16
|
+
|
17
|
+
class Null
|
18
|
+
def pluck(object)
|
19
|
+
object
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Delta
|
2
|
+
class SetOperator
|
3
|
+
class ActiveRecord < SetOperator
|
4
|
+
def self.compatible?(a, b)
|
5
|
+
a.is_a?(b.class) && a.class.name.include?("ActiveRecord")
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def subtract(a, b)
|
11
|
+
a.where.not(identifier.identities(b))
|
12
|
+
end
|
13
|
+
|
14
|
+
def intersect(a, b)
|
15
|
+
keys = identity_keys(a)
|
16
|
+
|
17
|
+
a_records = left_of_intersection(a, b)
|
18
|
+
a_records = unique_records(a_records, keys)
|
19
|
+
|
20
|
+
Enumerator.new do |y|
|
21
|
+
a_records.each do |record|
|
22
|
+
y.yield find_pair!(record, keys)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def identity_keys(collection)
|
28
|
+
identifier.identities(collection).keys
|
29
|
+
end
|
30
|
+
|
31
|
+
def left_of_intersection(a, b)
|
32
|
+
b_identities = identifier.identities(b)
|
33
|
+
a.where(b_identities)
|
34
|
+
end
|
35
|
+
|
36
|
+
def unique_records(collection, keys)
|
37
|
+
collection.select(*keys).distinct
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_pair!(record, keys)
|
41
|
+
attributes = record.slice(*keys)
|
42
|
+
[a.find_by!(attributes), b.find_by!(attributes)]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Delta
|
2
|
+
class SetOperator
|
3
|
+
class Enumerable < SetOperator
|
4
|
+
def self.compatible?(_a, _b)
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(a:, b:, identifier:)
|
9
|
+
super
|
10
|
+
|
11
|
+
self.a = a.lazy
|
12
|
+
self.b = b.lazy
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def subtract(a, b)
|
18
|
+
a.reject { |a_object| other_object(b, a_object) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def intersect(a, b)
|
22
|
+
a.map { |a_object| [a_object, other_object(b, a_object)] }
|
23
|
+
.select { |_, b_object| b_object }
|
24
|
+
end
|
25
|
+
|
26
|
+
def other_object(collection, object)
|
27
|
+
identity = identifier.identity(object)
|
28
|
+
|
29
|
+
collection.find do |other_object|
|
30
|
+
identity == identifier.identity(other_object)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Delta
|
2
|
+
class SetOperator
|
3
|
+
ADAPTERS = [ActiveRecord, Enumerable]
|
4
|
+
|
5
|
+
def self.adapt(a:, b:, identifier:)
|
6
|
+
adapter = ADAPTERS.find { |klass| klass.compatible?(a, b) }
|
7
|
+
adapter = ADAPTERS.last unless adapter
|
8
|
+
|
9
|
+
adapter.new(a: a, b: b, identifier: identifier)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(a:, b:, identifier:)
|
13
|
+
self.a = a
|
14
|
+
self.b = b
|
15
|
+
self.identifier = identifier
|
16
|
+
end
|
17
|
+
|
18
|
+
def subtract_a_from_b
|
19
|
+
subtract(b, a)
|
20
|
+
end
|
21
|
+
|
22
|
+
def subtract_b_from_a
|
23
|
+
subtract(a, b)
|
24
|
+
end
|
25
|
+
|
26
|
+
def intersection
|
27
|
+
intersect(a, b)
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
attr_accessor :a, :b, :identifier
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# a - b
|
37
|
+
def subtract(_a, _b)
|
38
|
+
fail NotImplementedError, "override me"
|
39
|
+
end
|
40
|
+
|
41
|
+
# a & b
|
42
|
+
def intersect(_a, _b)
|
43
|
+
fail NotImplementedError, "override me"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/delta.rb
ADDED
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: delta
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Patuzzo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.4'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.2'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.31'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.31'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activerecord
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
description: Calculates the delta between two collections of objects.
|
98
|
+
email: chris@patuzzo.co.uk
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- README.md
|
104
|
+
- lib/delta.rb
|
105
|
+
- lib/delta/base.rb
|
106
|
+
- lib/delta/identifier.rb
|
107
|
+
- lib/delta/plucker.rb
|
108
|
+
- lib/delta/set_operator.rb
|
109
|
+
- lib/delta/set_operator/active_record.rb
|
110
|
+
- lib/delta/set_operator/enumerable.rb
|
111
|
+
homepage: https://github.com/tuzz/delta
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.4.5
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Delta
|
135
|
+
test_files: []
|