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 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