├── .codeclimate.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── .travis.yml
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── evil-struct.gemspec
├── lib
├── evil-struct.rb
└── evil
│ ├── struct.rb
│ └── struct
│ ├── attributes.rb
│ └── utils.rb
└── spec
├── features
├── attributes_spec.rb
├── constructor_aliases_spec.rb
├── constructor_arguments_spec.rb
├── deep_merge_spec.rb
├── equalizer_spec.rb
├── hashifier_spec.rb
├── merge_spec.rb
└── shared_options_spec.rb
└── spec_helper.rb
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 | engines:
3 | bundler-audit:
4 | enabled: true
5 | duplication:
6 | enabled: true
7 | config:
8 | languages:
9 | - ruby
10 | fixme:
11 | enabled: true
12 | rubocop:
13 | enabled: true
14 | ratings:
15 | paths:
16 | - "**.rb"
17 | exclude_paths:
18 | - spec/
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | ---
2 | AllCops:
3 | DisplayCopNames: true
4 | DisplayStyleGuide: true
5 | StyleGuideCopsOnly: true
6 | TargetRubyVersion: 2.3
7 |
8 | Style/Alias:
9 | Enabled: false
10 |
11 | Style/ClassAndModuleChildren:
12 | EnforcedStyle: compact
13 |
14 | Style/FileName:
15 | Enabled: false
16 |
17 | Style/FrozenStringLiteralComment:
18 | Enabled: false
19 |
20 | Style/ModuleFunction:
21 | Enabled: false
22 |
23 | Style/StringLiterals:
24 | EnforcedStyle: double_quotes
25 |
26 | Style/StringLiteralsInInterpolation:
27 | EnforcedStyle: double_quotes
28 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: ruby
3 | sudo: false
4 | cache: bundler
5 | bundler_args: --without benchmarks tools
6 | before_install:
7 | - gem install bundler --no-ri --no-rdoc
8 | - gem update --system
9 | script:
10 | - bundle exec rake spec
11 | rvm:
12 | - 2.3.0
13 | - 2.4.0
14 | - jruby-9.1.0.0
15 | - rbx-2
16 | - rbx-3
17 | - ruby-head
18 | env:
19 | global:
20 | - JRUBY_OPTS='--dev -J-Xmx1024M'
21 | matrix:
22 | allow_failures:
23 | - rvm: rbx-2
24 | - rvm: rbx-3
25 | - rvm: ruby-head
26 | - rvm: jruby-head
27 | include:
28 | - rvm: jruby-head
29 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in evil-struct.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Andrew Kozin (nepalez), Evil Martians
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Evil::Struct
2 |
3 | Nested structure with type constraints, based on the [dry-initializer][dry-initializer] DSL.
4 |
5 |
6 |
7 |
8 | [![Gem Version][gem-badger]][gem]
9 | [![Build Status][travis-badger]][travis]
10 | [![Dependency Status][gemnasium-badger]][gemnasium]
11 | [![Inline docs][inch-badger]][inch]
12 |
13 | ## Installation
14 |
15 | Add this line to your application's Gemfile:
16 |
17 | ```ruby
18 | gem 'evil-struct'
19 | ```
20 |
21 | And then execute:
22 |
23 | $ bundle
24 |
25 | Or install it yourself as:
26 |
27 | $ gem install evil-struct
28 |
29 | ## Synopsis
30 |
31 | The structure is like [dry-struct][dry-struct] except for it controls optional attributes and default values aside of type constraints.
32 |
33 | Its DSL is taken from [dry-initializer][dry-initializer]. Its method `attribute` is just an alias for `option`.
34 |
35 | ```ruby
36 | require "evil-struct"
37 | require "dry-types"
38 |
39 | class Product < Evil::Struct
40 | attribute :title
41 | attribute :price, Dry::Types["coercible.float"]
42 | attribute :quantity, Dry::Types["coercible.int"], default: proc { 0 }
43 |
44 | # shared options
45 | attributes optional: true do
46 | attribute :subtitle
47 | attribute :description
48 | end
49 | end
50 |
51 | # Accepts both symbolic and string keys.
52 | # Class methods `[]`, `call`, and `load` are just aliases for `new`
53 | product = Product[title: "apple", "price" => "10.9", description: "a fruit"]
54 |
55 | # Attributes are available via methods or `[]`
56 | product.title # => "apple"
57 | product[:price] # => 10.9
58 | product["quantity"] # => 0
59 | product.description # => "a fruit"
60 |
61 | # unassigned value differs from `nil`
62 | product.subtitle # => Dry::Initializer::UNDEFINED
63 |
64 | # Raises in case a mandatory value not assigned
65 | Product.new # BOOM! because neither title, nor price are assigned
66 |
67 | # Hashifies all attributes except for undefined subtitle
68 | # You can use `dump` as an alias for `to_h`
69 | product.to_h
70 | # => { title: "apple", price: 10.9, description: "a fruit", quantity: 0 }
71 |
72 | # The structure is comparable to any object that responds to `#to_h`
73 | product == { title: "apple", price: 10.9, description: "a fruit", quantity: 0 }
74 | # => true
75 | ```
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 |
107 | ## Compatibility
108 |
109 | Tested under rubies [compatible to MRI 2.2+](.travis.yml).
110 |
111 | ## Contributing
112 |
113 | * [Fork the project](https://github.com/dry-rb/dry-initializer)
114 | * Create your feature branch (`git checkout -b my-new-feature`)
115 | * Add tests for it
116 | * Commit your changes (`git commit -am '[UPDATE] Add some feature'`)
117 | * Push to the branch (`git push origin my-new-feature`)
118 | * Create a new Pull Request
119 |
120 | ## License
121 |
122 | The gem is available as open source under the terms of the [MIT License](./LICENSE.txt).
123 |
124 | [dry-initializer]: https://rom-rb.org/gems/dry-initializer
125 | [dry-struct]: https://rom-rb.org/gems/dry-struct
126 | [gem-badger]: https://img.shields.io/gem/v/evil-struct.svg?style=flat
127 | [gem]: https://rubygems.org/gems/evil-struct
128 | [gemnasium-badger]: https://img.shields.io/gemnasium/evilmartians/evil-struct.svg?style=flat
129 | [gemnasium]: https://gemnasium.com/evilmartians/evil-struct
130 | [inch-badger]: http://inch-ci.org/github/evilmartians/evil-struct.svg
131 | [inch]: https://inch-ci.org/github/evilmartians/evil-struct
132 | [travis-badger]: https://img.shields.io/travis/evilmartians/evil-struct/master.svg?style=flat
133 | [travis]: https://travis-ci.org/evilmartians/evil-struct
134 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rspec/core/rake_task"
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task default: :spec
7 |
--------------------------------------------------------------------------------
/evil-struct.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |gem|
2 | gem.name = "evil-struct"
3 | gem.version = "0.0.4"
4 | gem.author = "Andrew Kozin (nepalez)"
5 | gem.email = "andrew.kozin@gmail.com"
6 | gem.homepage = "https://github.com/evilmartians/evil-struct"
7 | gem.summary = "Structure with type constraints based on dry-initializer"
8 | gem.license = "MIT"
9 |
10 | gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
11 | gem.test_files = gem.files.grep(/^spec/)
12 | gem.extra_rdoc_files = Dir["README.md", "LICENSE", "CHANGELOG.md"]
13 |
14 | gem.required_ruby_version = ">= 2.3"
15 |
16 | gem.add_runtime_dependency "dry-initializer", "~> 1.1"
17 |
18 | gem.add_development_dependency "dry-types", "> 0.9"
19 | gem.add_development_dependency "rspec", "~> 3.0"
20 | gem.add_development_dependency "rake", "> 11"
21 | gem.add_development_dependency "rubocop", ">= 0.44"
22 | end
23 |
--------------------------------------------------------------------------------
/lib/evil-struct.rb:
--------------------------------------------------------------------------------
1 | # Namespace for gems created by Evil Martians (http://evilmartians.com)
2 | module Evil
3 | require_relative "evil/struct"
4 | end
5 |
--------------------------------------------------------------------------------
/lib/evil/struct.rb:
--------------------------------------------------------------------------------
1 | require "dry-initializer"
2 |
3 | # Nested structure with type constraints, based on the `dry-initializer` DSL
4 | class Evil::Struct
5 | extend Dry::Initializer::Mixin
6 |
7 | require_relative "struct/attributes"
8 | require_relative "struct/utils"
9 |
10 | class << self
11 | # Builds a struct from value that respond to `to_h` or `to_hash`
12 | #
13 | # @param [#to_h, #to_hash] value (nil)
14 | # @return [Evil::Struct]
15 | #
16 | # @alias :call
17 | # @alias :[]
18 | # @alias :load
19 | #
20 | def new(value = {})
21 | value if value.instance_of? self.class
22 |
23 | hash = value if value.is_a? Hash
24 | hash ||= value.to_h if value.respond_to? :to_h
25 | hash ||= value.to_hash if value.respond_to? :to_hash
26 |
27 | hash_with_symbolic_keys = hash.each_with_object({}) do |(key, val), obj|
28 | obj[key.to_sym] = val
29 | end
30 | super hash_with_symbolic_keys
31 | end
32 | alias_method :call, :new
33 | alias_method :[], :new
34 | alias_method :load, :new
35 |
36 | # @!method attributes(options)
37 | # Shares options between definitions made inside the block
38 | #
39 | # @example
40 | # attributes optional: true do
41 | # attribute :foo
42 | # attribute :bar
43 | # end
44 | #
45 | # @option options (see #attribute)
46 | # @return [self] itself
47 | #
48 | def attributes(**options, &block)
49 | Attributes.call(self, options, &block)
50 | self
51 | end
52 |
53 | # Returns the list of defined attributes
54 | #
55 | # @return [Array]
56 | #
57 | def list_of_attributes
58 | @list_of_attributes ||= []
59 | end
60 |
61 | # @!method attribute(name, type = nil, options)
62 | # Declares the attribute
63 | #
64 | # @param [#to_sym] name The name of the key
65 | # @param [#call] type (nil) The type constraint
66 | # @option options [#call] :type Type constraint (alternative syntax)
67 | # @option options [#to_sym] :as The name of the attribute
68 | # @option options [Proc] :default Block returning a default value
69 | # @option options [Boolean] :optional (nil) Whether key is optional
70 | # @return [self]
71 | #
72 | # @alias :option
73 | # @alias :param
74 | #
75 | def option(name, type = nil, as: nil, **opts)
76 | super.tap { list_of_attributes << (as || name).to_sym }
77 | self
78 | end
79 | alias_method :attribute, :option
80 | alias_method :param, :option
81 |
82 | private
83 |
84 | def inherited(klass)
85 | super
86 | klass.instance_variable_set :@list_of_attributes, list_of_attributes.dup
87 | end
88 | end
89 |
90 | # Checks an equality to other object that respond to `to_h` or `to_hash`
91 | #
92 | # @param [Object] other
93 | # @return [Boolean]
94 | #
95 | def ==(other)
96 | if other&.respond_to?(:to_h)
97 | to_h == other.to_h
98 | elsif other.respond_to?(:to_hash)
99 | to_h == other.to_hash
100 | else
101 | false
102 | end
103 | end
104 |
105 | # Converts nested structure to hash
106 | #
107 | # Makes conversion through nested hashes, arrays, enumerables, as well
108 | # as trhough values that respond to `to_a`, `to_h`, and `to_hash`.
109 | # Doesn't convert `nil`.
110 | #
111 | # @return [Hash]
112 | #
113 | # @alias :to_hash
114 | # @alias :dump
115 | #
116 | def to_h
117 | self.class.list_of_attributes.each_with_object({}) do |key, hash|
118 | val = instance_variable_get :"@#{key}"
119 | hash[key] = Utils.hashify(val) unless val == Dry::Initializer::UNDEFINED
120 | end
121 | end
122 | alias_method :to_hash, :to_h
123 | alias_method :dump, :to_h
124 |
125 | # @!method [](key)
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"
137 | #
138 | # @param [Symbol, String] key The name of the attribute
139 | # @return [Object] A value of the attribute
140 | #
141 | alias_method :[], :send
142 |
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
162 |
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)]
189 | end
190 | alias_method :deep_merge, :merge_deeply
191 | end
192 |
--------------------------------------------------------------------------------
/lib/evil/struct/attributes.rb:
--------------------------------------------------------------------------------
1 | # Handler for shared options
2 | class Evil::Struct::Attributes
3 | # @private
4 | def self.call(*args, &block)
5 | new(*args).instance_eval(&block)
6 | end
7 |
8 | private
9 |
10 | def initialize(klass, **options)
11 | @klass = klass
12 | @options = options
13 | end
14 |
15 | # Declares an attribute that shares options of the block
16 | # @param (see Struct.attribute)
17 | # @option (see Struct.attribute)
18 | # @return (see Struct.attribute)
19 | def attribute(name, type = nil, **options)
20 | @klass.send :attribute, name, type, @options.merge(options)
21 | end
22 | alias_method :option, :attribute
23 | alias_method :param, :attribute
24 | end
25 |
--------------------------------------------------------------------------------
/lib/evil/struct/utils.rb:
--------------------------------------------------------------------------------
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] source
33 | # @param [Hash, #to_h, #to_hash] target
34 | # @return [Hash]
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
86 |
--------------------------------------------------------------------------------
/spec/features/attributes_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "attributes" do
4 | before do
5 | class Test::Foo < Evil::Struct
6 | attribute :"some argument", as: "qux"
7 | end
8 | end
9 |
10 | let(:struct) { Test::Foo.new "some argument": "bar" }
11 |
12 | it "accessible via method" do
13 | expect(struct.qux).to eq "bar"
14 | end
15 |
16 | it "accessible by symbolic key" do
17 | expect(struct[:qux]).to eq "bar"
18 | end
19 |
20 | it "accessible by string key" do
21 | expect(struct["qux"]).to eq "bar"
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/features/constructor_aliases_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "constructor aliases" do
4 | it ".new" do
5 | class Test::Foo < Evil::Struct
6 | attribute :foo
7 | attribute :baz, default: proc { "qux" }
8 | end
9 |
10 | expect(Test::Foo.new foo: "bar").to eq foo: "bar", baz: "qux"
11 | end
12 |
13 | it ".call" do
14 | class Test::Foo < Evil::Struct
15 | attribute :foo
16 | attribute :baz, default: proc { "qux" }
17 | end
18 |
19 | expect(Test::Foo.call foo: "bar").to eq foo: "bar", baz: "qux"
20 | end
21 |
22 | it ".load" do
23 | class Test::Foo < Evil::Struct
24 | attribute :foo
25 | attribute :baz, default: proc { "qux" }
26 | end
27 |
28 | expect(Test::Foo.load foo: "bar").to eq foo: "bar", baz: "qux"
29 | end
30 |
31 | it ".[]" do
32 | class Test::Foo < Evil::Struct
33 | attribute :foo
34 | attribute :baz, default: proc { "qux" }
35 | end
36 |
37 | expect(Test::Foo[foo: "bar"]).to eq foo: "bar", baz: "qux"
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/features/constructor_arguments_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "constructor" do
4 | before do
5 | class Test::Foo < Evil::Struct
6 | attribute :foo, default: proc { "qux" }
7 | end
8 | end
9 |
10 | it "accepts hash with symbolic keys" do
11 | expect(Test::Foo.new foo: "bar").to eq foo: "bar"
12 | end
13 |
14 | it "accepts hash with string keys" do
15 | expect(Test::Foo.new "foo" => "bar").to eq foo: "bar"
16 | end
17 |
18 | it "accepts nil" do
19 | expect(Test::Foo.new nil).to eq foo: "qux"
20 | end
21 |
22 | it "accepts no arguments" do
23 | expect(Test::Foo.new).to eq foo: "qux"
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/features/deep_merge_spec.rb:
--------------------------------------------------------------------------------
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
29 |
--------------------------------------------------------------------------------
/spec/features/equalizer_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "equalizer" do
4 | it "uses #to_h for comparison" do
5 | class Test::Foo < Evil::Struct
6 | attribute :foo
7 | end
8 |
9 | expect(Test::Foo.new foo: "bar").to eq foo: "bar"
10 | expect(Test::Foo.new foo: "bar").to eq double(to_h: { foo: "bar" })
11 | end
12 |
13 | it "makes struct not equal to nil" do
14 | class Test::Foo < Evil::Struct
15 | attribute :foo, optional: true
16 | end
17 |
18 | expect(Test::Foo.new).to eq({})
19 | expect(Test::Foo.new).not_to eq nil
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/features/hashifier_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "hashifier" do
4 | before do
5 | class Test::Foo < Evil::Struct
6 | attribute :foo
7 | attribute :bar, optional: true
8 | end
9 | end
10 |
11 | it "converts struct to hash" do
12 | result = Test::Foo[foo: "bar", bar: "baz"].to_h
13 |
14 | expect(result).to be_instance_of Hash
15 | expect(result).to eq foo: "bar", bar: "baz"
16 | end
17 |
18 | it "hides unassigned values" do
19 | result = Test::Foo[foo: "bar"].to_h
20 |
21 | expect(result).to eq foo: "bar"
22 | end
23 |
24 | it "has alias .to_hash" do
25 | result = Test::Foo[foo: "bar"].to_hash
26 |
27 | expect(result).to eq foo: "bar"
28 | end
29 |
30 | it "has alias .dump" do
31 | result = Test::Foo[foo: "bar"].dump
32 |
33 | expect(result).to eq foo: "bar"
34 | end
35 |
36 | it "applied deeply" do
37 | data = { foo: double(to_h: { baz: [double(to_hash: { qux: nil })] }) }
38 |
39 | expect(Test::Foo[data].to_h).to eq foo: { baz: [{ qux: nil }] }
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/features/merge_spec.rb:
--------------------------------------------------------------------------------
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
49 |
--------------------------------------------------------------------------------
/spec/features/shared_options_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "shared options" do
4 | it "supported via .attributes" do
5 | class Test::Foo < Evil::Struct
6 | attributes type: Dry::Types["strict.string"], default: proc { "bar" } do
7 | attribute :foo
8 | attribute :baz, default: proc { "qux" }
9 | end
10 | end
11 |
12 | expect(Test::Foo.new).to eq foo: "bar", baz: "qux"
13 | expect { Test::Foo.new foo: 1 }.to raise_error(TypeError)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require "pry"
3 | rescue LoadError
4 | nil
5 | end
6 |
7 | require "evil-struct"
8 | require "dry-types"
9 |
10 | RSpec.configure do |config|
11 | config.order = :random
12 | config.filter_run focus: true
13 | config.run_all_when_everything_filtered = true
14 |
15 | # Prepare the Test namespace for constants defined in specs
16 | config.around(:each) do |example|
17 | Test = Class.new(Module)
18 | example.run
19 | Object.send :remove_const, :Test
20 | end
21 | end
22 |
--------------------------------------------------------------------------------