evil-struct 0.0.2 → 0.0.3
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/.rubocop.yml +3 -14
- data/README.md +30 -0
- data/evil-struct.gemspec +4 -4
- data/lib/evil/struct.rb +72 -24
- data/lib/evil/struct/utils.rb +85 -0
- data/spec/features/deep_merge_spec.rb +28 -0
- data/spec/features/merge_spec.rb +48 -0
- metadata +13 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 78cb8976656ba1e9c186cfc690abd2136120dee3
|
4
|
+
data.tar.gz: 33cbb7dcf766ddc4c5f12cf2914b2c8b06b5af68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc7be8a84e68d11f3c4fe88ec211552517c608dadf01027a4390854d3889b58491796adb2d4a81e247bd1d49e2687281d2163309ee6b5c304f2248cf334beb3d
|
7
|
+
data.tar.gz: 472bbdc4b9e8dbffe740e1229ee255977994ec26b8b56fb3def52f056e36bd43d6c1560ec2341f845697317b681781fe0c922639af7ae669a90113d8356cbbad
|
data/.rubocop.yml
CHANGED
@@ -17,22 +17,11 @@ Style/FileName:
|
|
17
17
|
Style/FrozenStringLiteralComment:
|
18
18
|
Enabled: false
|
19
19
|
|
20
|
+
Style/ModuleFunction:
|
21
|
+
Enabled: false
|
22
|
+
|
20
23
|
Style/StringLiterals:
|
21
24
|
EnforcedStyle: double_quotes
|
22
25
|
|
23
26
|
Style/StringLiteralsInInterpolation:
|
24
27
|
EnforcedStyle: double_quotes
|
25
|
-
|
26
|
-
# Settings for Struct#hashify
|
27
|
-
|
28
|
-
Metrics/CyclomaticComplexity:
|
29
|
-
Max: 7
|
30
|
-
|
31
|
-
Metrics/AbcSize:
|
32
|
-
Max: 17.12
|
33
|
-
|
34
|
-
Metrics/MethodLength:
|
35
|
-
Max: 15
|
36
|
-
|
37
|
-
Metrics/PerceivedComplexity:
|
38
|
-
Max: 8
|
data/README.md
CHANGED
@@ -74,6 +74,36 @@ product == { title: "apple", price: 10.9, description: "a fruit", quantity: 0 }
|
|
74
74
|
# => true
|
75
75
|
```
|
76
76
|
|
77
|
+
The structure is designed for immutability. That's why it doesn't contain writers (but you can define them by yourself via `attr_writer`).
|
78
|
+
|
79
|
+
Instead of mutating current instance, you can merge another hash to the object via `merge` or `deep_merge`.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
new_product = product.merge(title: "orange")
|
83
|
+
new_product.class # => Product
|
84
|
+
new_product.to_h
|
85
|
+
# => { title: "orange", price: 10.9, description: "a fruit", quantity: 0 }
|
86
|
+
|
87
|
+
# you can merge any object that responds to `to_h` or `to_hash`
|
88
|
+
other = Product[title: "orange", price: 12]
|
89
|
+
new_product = product.merge(other)
|
90
|
+
new_product.to_h
|
91
|
+
# => { title: "orange", price: 12, description: "a fruit", quantity: 0 }
|
92
|
+
|
93
|
+
# merge_deeply (deep_merge) gracefully merge nested hashes or hashified objects
|
94
|
+
grape = Product.new title: "grape",
|
95
|
+
price: 30,
|
96
|
+
description: { country: "FR", year: 2016, sort: "Merlot" }
|
97
|
+
|
98
|
+
new_grape = grape.merge_deeply description: { year: 2017 }
|
99
|
+
new_grape.to_h
|
100
|
+
# => {
|
101
|
+
# title: "grape",
|
102
|
+
# price: 30,
|
103
|
+
# description: { country: "FR", year: 2017, sort: "Merlot" }
|
104
|
+
# }
|
105
|
+
```
|
106
|
+
|
77
107
|
## Compatibility
|
78
108
|
|
79
109
|
Tested under rubies [compatible to MRI 2.2+](.travis.yml).
|
data/evil-struct.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |gem|
|
2
2
|
gem.name = "evil-struct"
|
3
|
-
gem.version = "0.0.
|
3
|
+
gem.version = "0.0.3"
|
4
4
|
gem.author = "Andrew Kozin (nepalez)"
|
5
5
|
gem.email = "andrew.kozin@gmail.com"
|
6
6
|
gem.homepage = "https://github.com/evilmartians/evil-struct"
|
@@ -15,8 +15,8 @@ Gem::Specification.new do |gem|
|
|
15
15
|
|
16
16
|
gem.add_runtime_dependency "dry-initializer", "~> 0.10"
|
17
17
|
|
18
|
-
gem.add_development_dependency "dry-types", "
|
18
|
+
gem.add_development_dependency "dry-types", "> 0.9"
|
19
19
|
gem.add_development_dependency "rspec", "~> 3.0"
|
20
|
-
gem.add_development_dependency "rake", "
|
21
|
-
gem.add_development_dependency "rubocop", "
|
20
|
+
gem.add_development_dependency "rake", "> 11"
|
21
|
+
gem.add_development_dependency "rubocop", ">= 0.44"
|
22
22
|
end
|
data/lib/evil/struct.rb
CHANGED
@@ -5,6 +5,7 @@ class Evil::Struct
|
|
5
5
|
extend Dry::Initializer::Mixin
|
6
6
|
|
7
7
|
require_relative "struct/attributes"
|
8
|
+
require_relative "struct/utils"
|
8
9
|
|
9
10
|
class << self
|
10
11
|
# Builds a struct from value that respond to `to_h` or `to_hash`
|
@@ -32,14 +33,20 @@ class Evil::Struct
|
|
32
33
|
alias_method :[], :new
|
33
34
|
alias_method :load, :new
|
34
35
|
|
36
|
+
# @!method attributes(options)
|
35
37
|
# Shares options between definitions made inside the block
|
36
38
|
#
|
37
|
-
# @
|
38
|
-
#
|
39
|
+
# @example
|
40
|
+
# attributes optional: true do
|
41
|
+
# attribute :foo
|
42
|
+
# attribute :bar
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @option options (see #attribute)
|
39
46
|
# @return [self] itself
|
40
47
|
#
|
41
48
|
def attributes(**options, &block)
|
42
|
-
Attributes.call(self,
|
49
|
+
Attributes.call(self, options, &block)
|
43
50
|
self
|
44
51
|
end
|
45
52
|
|
@@ -51,16 +58,18 @@ class Evil::Struct
|
|
51
58
|
@list_of_attributes ||= []
|
52
59
|
end
|
53
60
|
|
61
|
+
# @!method attribute(name, type = nil, options)
|
54
62
|
# Declares the attribute
|
55
63
|
#
|
56
64
|
# @param [#to_sym] name The name of the key
|
57
|
-
# @param [#call] type (nil) The constraint
|
65
|
+
# @param [#call] type (nil) The type constraint
|
66
|
+
# @option options [#call] :type Type constraint (alternative syntax)
|
58
67
|
# @option options [#to_sym] :as The name of the attribute
|
59
68
|
# @option options [Proc] :default Block returning a default value
|
60
69
|
# @option options [Boolean] :optional (nil) Whether key is optional
|
61
70
|
# @return [self]
|
62
71
|
#
|
63
|
-
# @alias :
|
72
|
+
# @alias :option
|
64
73
|
# @alias :param
|
65
74
|
#
|
66
75
|
def option(name, type = nil, as: nil, **opts)
|
@@ -107,37 +116,76 @@ class Evil::Struct
|
|
107
116
|
def to_h
|
108
117
|
self.class.list_of_attributes.each_with_object({}) do |key, hash|
|
109
118
|
val = send(key)
|
110
|
-
hash[key] = hashify(val) unless val == Dry::Initializer::UNDEFINED
|
119
|
+
hash[key] = Utils.hashify(val) unless val == Dry::Initializer::UNDEFINED
|
111
120
|
end
|
112
121
|
end
|
113
122
|
alias_method :to_hash, :to_h
|
114
123
|
alias_method :dump, :to_h
|
115
124
|
|
116
125
|
# @!method [](key)
|
117
|
-
#
|
126
|
+
# Gets the attribute value by name
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# class User < Evil::Struct
|
130
|
+
# attribute :name
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# joe = User.new(name: "Joe")
|
134
|
+
# joe.name # => "Joe"
|
135
|
+
# joe[:name] # => "Joe"
|
136
|
+
# joe["name"] # => "Joe"
|
118
137
|
#
|
119
138
|
# @param [Symbol, String] key The name of the attribute
|
120
139
|
# @return [Object] A value of the attribute
|
121
140
|
#
|
122
141
|
alias_method :[], :send
|
123
142
|
|
124
|
-
|
143
|
+
# Shallowly merges other object to the current struct
|
144
|
+
#
|
145
|
+
# @example
|
146
|
+
# class User < Evil::Struct
|
147
|
+
# attribute :name
|
148
|
+
# attribute :age
|
149
|
+
# end
|
150
|
+
# joe_at_3 = User.new(name: "Joe", age: 3)
|
151
|
+
#
|
152
|
+
# joe_at_4 = joe_at_3.merge(age: 4)
|
153
|
+
# joe_at_4.name # => "Joe"
|
154
|
+
# joe_at_4.age # => 4
|
155
|
+
#
|
156
|
+
# @param [Hash, #to_h, #to_hash] other
|
157
|
+
# @return [self.class] new instance of the current class
|
158
|
+
#
|
159
|
+
def merge(other)
|
160
|
+
self.class[Utils.merge(to_h, other)]
|
161
|
+
end
|
125
162
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
163
|
+
# Deeply merges other object to the current struct
|
164
|
+
#
|
165
|
+
# It iterates through hashes and objects responding to `to_h` and `to_hash`.
|
166
|
+
# The iteration stops when any non-hash value reached.
|
167
|
+
#
|
168
|
+
# @example
|
169
|
+
# class User < Evil::Struct
|
170
|
+
# attribute :info
|
171
|
+
# attribute :meta
|
172
|
+
# end
|
173
|
+
# user = User.new info: { names: [{ first: "Joe", last: "Doe" }], age: 33 },
|
174
|
+
# meta: { type: :admin }
|
175
|
+
#
|
176
|
+
# user.merge info: { names: [{ first: "John" }] }, meta: { "role" => :cto }
|
177
|
+
# user.to_h # => {
|
178
|
+
# # info: { names: [{ first: "John" }], age: 33 },
|
179
|
+
# # meta: { type: :admin, role: :cto }
|
180
|
+
# # }
|
181
|
+
#
|
182
|
+
# @param [Hash, #to_h, #to_hash] other
|
183
|
+
# @return [self.class] new instance of the current class
|
184
|
+
#
|
185
|
+
# @alias :deep_merge
|
186
|
+
#
|
187
|
+
def merge_deeply(other)
|
188
|
+
self.class[Utils.merge_deeply(self, other)]
|
142
189
|
end
|
190
|
+
alias_method :deep_merge, :merge_deeply
|
143
191
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Collection of utility methods to hashify and merge structures
|
2
|
+
module Evil::Struct::Utils
|
3
|
+
extend self
|
4
|
+
|
5
|
+
# Converts value to nested hash
|
6
|
+
#
|
7
|
+
# Makes conversion through nested hashes, arrays, enumerables,
|
8
|
+
# and other objects that respond to `to_a`, `to_h`, `to_hash`, or `each`.
|
9
|
+
# Doesn't convert `nil` (even though it responds to `to_h` and `to_a`).
|
10
|
+
#
|
11
|
+
# @param [Object] value
|
12
|
+
# @return [Hash]
|
13
|
+
#
|
14
|
+
def hashify(value)
|
15
|
+
hash = to_h(value)
|
16
|
+
list = to_a(value)
|
17
|
+
|
18
|
+
if hash
|
19
|
+
hash.each_with_object({}) { |(key, val), obj| obj[key] = hashify(val) }
|
20
|
+
elsif list
|
21
|
+
list.map { |item| hashify(item) }
|
22
|
+
else
|
23
|
+
value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Shallowly merges target object to the source hash
|
28
|
+
#
|
29
|
+
# The target object can be hash, or respond to either `to_h`, or `to_hash`.
|
30
|
+
# Before a merge, the keys of the target object are symbolized.
|
31
|
+
#
|
32
|
+
# @param [Hash<Symbol, Object>] source
|
33
|
+
# @param [Hash, #to_h, #to_hash] target
|
34
|
+
# @return [Hash<Symbol, Object>]
|
35
|
+
#
|
36
|
+
def merge(source, target)
|
37
|
+
source.merge(to_h(target))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Deeply merges target object to the source one
|
41
|
+
#
|
42
|
+
# The nesting stops when a first non-hashified value reached.
|
43
|
+
# It do not merge arrays of hashes!
|
44
|
+
#
|
45
|
+
# @param [Object] source
|
46
|
+
# @param [Object] target
|
47
|
+
# @return [Object]
|
48
|
+
#
|
49
|
+
def merge_deeply(source, target)
|
50
|
+
source_hash = to_h(source)
|
51
|
+
target_hash = to_h(target)
|
52
|
+
return target unless source_hash && target_hash
|
53
|
+
|
54
|
+
keys = (source_hash.keys | target_hash.keys)
|
55
|
+
keys.each_with_object(source_hash) do |key, obj|
|
56
|
+
next unless target_hash.key? key
|
57
|
+
obj[key] = merge_deeply source_hash[key], target_hash[key]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def to_h(value)
|
64
|
+
to_hash(value)&.each_with_object({}) do |(key, val), obj|
|
65
|
+
obj[key.to_sym] = val
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_hash(value)
|
70
|
+
if value.is_a? Hash then value
|
71
|
+
elsif value.is_a? Array then nil
|
72
|
+
elsif value.nil? then nil
|
73
|
+
elsif value.respond_to? :to_h then value.to_h
|
74
|
+
elsif value.respond_to? :to_hash then value.to_hash
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_a(value)
|
79
|
+
if value.is_a? Array then value
|
80
|
+
elsif value.is_a? Hash then nil
|
81
|
+
elsif value.nil? then nil
|
82
|
+
elsif value.respond_to? :to_a then value.to_a
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "deep merge" do
|
4
|
+
before do
|
5
|
+
class Test::Foo < Evil::Struct
|
6
|
+
attribute :foo
|
7
|
+
attribute :bar
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:struct) do
|
12
|
+
Test::Foo.new foo: { bar: [{ foo: :FOO }] },
|
13
|
+
bar: { baz: :FOO, qux: :QUX }
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:result) do
|
17
|
+
struct.merge_deeply foo: { bar: [{ qux: :QUX }] },
|
18
|
+
bar: { "qux" => :FOO }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "works" do
|
22
|
+
expect { result }.not_to change { struct }
|
23
|
+
|
24
|
+
expect(result).to be_instance_of Test::Foo
|
25
|
+
expect(result).to eq foo: { bar: [{ qux: :QUX }] },
|
26
|
+
bar: { baz: :FOO, qux: :FOO }
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "merge" do
|
4
|
+
before do
|
5
|
+
class Test::Foo < Evil::Struct
|
6
|
+
attribute :foo
|
7
|
+
attribute :bar
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:struct) { Test::Foo.new foo: :FOO, bar: :BAR }
|
12
|
+
|
13
|
+
it "merges hash with symbol keys" do
|
14
|
+
other = { bar: :BAZ, baz: :QUX }
|
15
|
+
expect { struct.merge(other) }.not_to change { struct }
|
16
|
+
|
17
|
+
result = struct.merge(other)
|
18
|
+
expect(result).to be_instance_of Test::Foo
|
19
|
+
expect(result).to eq foo: :FOO, bar: :BAZ
|
20
|
+
end
|
21
|
+
|
22
|
+
it "merges hash with string keys" do
|
23
|
+
other = { "bar" => :BAZ, "baz" => :QUX }
|
24
|
+
expect { struct.merge(other) }.not_to change { struct }
|
25
|
+
|
26
|
+
result = struct.merge(other)
|
27
|
+
expect(result).to be_instance_of Test::Foo
|
28
|
+
expect(result).to eq foo: :FOO, bar: :BAZ
|
29
|
+
end
|
30
|
+
|
31
|
+
it "merges objects supporting #to_h" do
|
32
|
+
other = double to_h: { bar: :BAZ, baz: :QUX }
|
33
|
+
expect { struct.merge(other) }.not_to change { struct }
|
34
|
+
|
35
|
+
result = struct.merge(other)
|
36
|
+
expect(result).to be_instance_of Test::Foo
|
37
|
+
expect(result).to eq foo: :FOO, bar: :BAZ
|
38
|
+
end
|
39
|
+
|
40
|
+
it "merges objects supporting #to_hash" do
|
41
|
+
other = double to_hash: { bar: :BAZ, baz: :QUX }
|
42
|
+
expect { struct.merge(other) }.not_to change { struct }
|
43
|
+
|
44
|
+
result = struct.merge(other)
|
45
|
+
expect(result).to be_instance_of Test::Foo
|
46
|
+
expect(result).to eq foo: :FOO, bar: :BAZ
|
47
|
+
end
|
48
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: evil-struct
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kozin (nepalez)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-11-
|
11
|
+
date: 2016-11-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-initializer
|
@@ -28,14 +28,14 @@ dependencies:
|
|
28
28
|
name: dry-types
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0.9'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.9'
|
41
41
|
- !ruby/object:Gem::Dependency
|
@@ -56,28 +56,28 @@ dependencies:
|
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '11'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '11'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rubocop
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0.44'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0.44'
|
83
83
|
description:
|
@@ -101,11 +101,14 @@ files:
|
|
101
101
|
- lib/evil-struct.rb
|
102
102
|
- lib/evil/struct.rb
|
103
103
|
- lib/evil/struct/attributes.rb
|
104
|
+
- lib/evil/struct/utils.rb
|
104
105
|
- spec/features/attributes_spec.rb
|
105
106
|
- spec/features/constructor_aliases_spec.rb
|
106
107
|
- spec/features/constructor_arguments_spec.rb
|
108
|
+
- spec/features/deep_merge_spec.rb
|
107
109
|
- spec/features/equalizer_spec.rb
|
108
110
|
- spec/features/hashifier_spec.rb
|
111
|
+
- spec/features/merge_spec.rb
|
109
112
|
- spec/features/shared_options_spec.rb
|
110
113
|
- spec/spec_helper.rb
|
111
114
|
homepage: https://github.com/evilmartians/evil-struct
|
@@ -136,7 +139,9 @@ test_files:
|
|
136
139
|
- spec/features/attributes_spec.rb
|
137
140
|
- spec/features/constructor_aliases_spec.rb
|
138
141
|
- spec/features/constructor_arguments_spec.rb
|
142
|
+
- spec/features/deep_merge_spec.rb
|
139
143
|
- spec/features/equalizer_spec.rb
|
140
144
|
- spec/features/hashifier_spec.rb
|
145
|
+
- spec/features/merge_spec.rb
|
141
146
|
- spec/features/shared_options_spec.rb
|
142
147
|
- spec/spec_helper.rb
|