evil-struct 0.0.2 → 0.0.3

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
  SHA1:
3
- metadata.gz: 76c9257157a79cbad182f58d41631bfd789e976c
4
- data.tar.gz: 2dd06f599e56d8fd601118b142328e698862389d
3
+ metadata.gz: 78cb8976656ba1e9c186cfc690abd2136120dee3
4
+ data.tar.gz: 33cbb7dcf766ddc4c5f12cf2914b2c8b06b5af68
5
5
  SHA512:
6
- metadata.gz: d6b59bf33f32dc0714cff1568ae715c514798b06328dc04a34cce10b3c875379ec6b1085e4ae05cdf4bc2993fc85ffad652c07937a481f652d690947036e318b
7
- data.tar.gz: 2d434f1a059afabf5b8ecf56ea8959c66614d83e52967b6c4b2de94f59391771fe9f6a910406550447c83d837698be236fadf638fbb239f38daca68c75af5e54
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.2"
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", "~> 0.9"
18
+ gem.add_development_dependency "dry-types", "> 0.9"
19
19
  gem.add_development_dependency "rspec", "~> 3.0"
20
- gem.add_development_dependency "rake", "~> 11"
21
- gem.add_development_dependency "rubocop", "~> 0.44"
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
- # @param [Hash<Symbol, Object>] options Shared options
38
- # @param [Proc] block Block with definitions of attributes
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, **options, &block)
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 :attribute
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
- # Gets the attribute value by name
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
- private
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
- def hashify(value)
127
- if value.is_a? Hash
128
- value.each_with_object({}) { |(key, val), obj| obj[key] = hashify(val) }
129
- elsif value.is_a? Array
130
- value.map { |item| hashify(item) }
131
- elsif value&.respond_to? :to_a
132
- hashify(value.to_a)
133
- elsif value&.respond_to? :to_h
134
- hashify(value.to_h)
135
- elsif value.respond_to? :to_hash
136
- hashify(value.to_hash)
137
- elsif value.is_a? Enumerable
138
- value.map { |item| hashify(item) }
139
- else
140
- value
141
- end
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.2
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-20 00:00:00.000000000 Z
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