dry-struct 0.4.0 → 0.5.0

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
- SHA1:
3
- metadata.gz: 33a4358e2a11c38b8816778acc5722b6d08a4c4d
4
- data.tar.gz: 24bebcf82ec2e1dcb7536216acc397ae9aa6a3c7
2
+ SHA256:
3
+ metadata.gz: 4f15a9185e2b7ee171f632db8fc95c29aafa567d4936fe3e8681b073af810f76
4
+ data.tar.gz: eec2908a47ad4e549a454bf6890dbb2402f689222810ffeab52b64958ff0caff
5
5
  SHA512:
6
- metadata.gz: 332a625be1cde86fefd74efe78e79b375c6c66a15f5ad062f26e736055b9a9e9aef9234299bd0a7ff0272ce2e80218060d0ddb52d0074a5bc7d1ed12a03e1971
7
- data.tar.gz: 757d33e5151cf3f30ffbe28798f1c86ab634f12612d4a7603431aae72513eedf088a3cf483603809b10ac6da870da2dc2f39ef1136e6fe99c42b25ee979ea681
6
+ metadata.gz: f743fe48c3651573fecbf46fa0c81cd4cd2327774ae903bcd1bea60dc98b09dc6c63acbcca506c8021c383a8b6ce9d74725956c94cca976b22d1a5800d34913b
7
+ data.tar.gz: 6f898126cac3c667d60ac352de8fbaacfb24aa417136881f315858c22d337868a3544894d9caea1da6fe9c3cb705c2c45702f0c1228ddbc7bd43d8a44d8256dc
@@ -4,13 +4,16 @@ sudo: required
4
4
  bundler_args: --without benchmarks tools
5
5
  script:
6
6
  - bundle exec rake spec
7
+ before_install:
8
+ - gem update --system
7
9
  after_success:
8
10
  - '[ -d coverage ] && bundle exec codeclimate-test-reporter'
9
11
  rvm:
10
- - 2.2.7
11
- - 2.3.3
12
- - 2.4.1
13
- - jruby-9.1.10.0
12
+ - 2.2.9
13
+ - 2.3.6
14
+ - 2.4.3
15
+ - 2.5.0
16
+ - jruby-9.1.15.0
14
17
  env:
15
18
  global:
16
19
  - COVERAGE=true
@@ -1,3 +1,73 @@
1
+ # v0.5.0 2018-05-03
2
+
3
+ ## BREAKING CHANGES
4
+
5
+ * `constructor_type` was removed, use `transform_types` and `transform_keys` as a replacement (see below)
6
+ * Default types are evaluated _only_ on missing values. Again, use `tranform_types` as a work around for `nil`s
7
+ * Values are now stored within a single instance variable names `@attributes`, this sped up struct creation and improved support for reserved attribute names such as `hash`, they don't get a getter but still can be read via `#[]`
8
+ * Ruby 2.3 is a minimal supported version
9
+
10
+ ## Added
11
+
12
+ * `Dry::Struct.transform_types` accepts a block which is yielded on every type to add. Since types are `dry-types`' objects that come with a robust DSL it's rather simple to restore the behavior of `constructor_type`. See https://github.com/dry-rb/dry-struct/pull/64 for details (flash-gordon)
13
+
14
+ Example: evaluate defaults on `nil` values
15
+
16
+ ```ruby
17
+ class User < Dry::Struct
18
+ transform_types do |type|
19
+ type.constructor { |value| value.nil? ? Undefined : value }
20
+ end
21
+ end
22
+ ```
23
+
24
+ * `Data::Struct.transform_keys` accepts a block/proc that transforms keys of input hashes. The most obvious usage is simbolization but arbitrary transformations are allowed (flash-gordon)
25
+
26
+ * `Dry.Struct` builds a struct by a hash of attribute names and types (citizen428)
27
+
28
+ ```ruby
29
+ User = Dry::Struct(name: 'strict.string') do
30
+ attribute :email, 'strict.string'
31
+ end
32
+ ```
33
+
34
+ * Support for `Struct.meta`, note that `.meta` returns a _new class_ (flash-gordon)
35
+
36
+ ```ruby
37
+ class User < Dry::Struct
38
+ attribute :name, Dry::Types['strict.string']
39
+ end
40
+
41
+ UserWithMeta = User.meta(foo: :bar)
42
+
43
+ User.new(name: 'Jade').class == UserWithMeta.new(name: 'Jade').class # => false
44
+ ```
45
+
46
+ * `Struct.attribute` yields a block with definition for nested structs. It defines a nested constant for the new struct and supports arrays (AMHOL + flash-gordon)
47
+
48
+ ```ruby
49
+ class User < Dry::Struct
50
+ attribute :name, Types::Strict::String
51
+ attribute :address do
52
+ attribute :country, Types::Strict::String
53
+ attribute :city, Types::Strict::String
54
+ end
55
+ attribute :accounts, Types::Strict::Array do
56
+ attribute :currency, Types::Strict::String
57
+ attribute :balance, Types::Strict::Decimal
58
+ end
59
+ end
60
+
61
+ # ^This automatically defines User::Address and User::Account
62
+ ```
63
+
64
+ ## Fixed
65
+
66
+ * Adding a new attribute invalidates `attribute_names` (flash-gordon)
67
+ * Struct classes track subclasses and define attributes in them, now it doesn't matter whether you define attributes first and _then_ subclass or vice versa. Note this can lead to memory leaks in Rails environment when struct classes are reloaded (flash-gordon)
68
+
69
+ [Compare v0.4.0...v0.5.0](https://github.com/dry-rb/dry-struct/compare/v0.4.0...v0.5.0)
70
+
1
71
  # v0.4.0 2017-11-04
2
72
 
3
73
  ## Changed
@@ -6,11 +6,11 @@ If you found a bug, report an issue and describe what's the expected behavior ve
6
6
 
7
7
  ## Reporting feature requests
8
8
 
9
- Report a feature request **only after discussing it first on [discuss.dry-rb.org](https://discuss.dry-rb.org)** where it was accepted. Please provide a concise description of the feature, don't link to a discussion thread, and instead summarize what was discussed.
9
+ Report a feature request **only after discourseing it first on [discourse.dry-rb.org](https://discourse.dry-rb.org)** where it was accepted. Please provide a concise description of the feature, don't link to a discourseion thread, and instead summarize what was discourseed.
10
10
 
11
11
  ## Reporting questions, support requests, ideas, concerns etc.
12
12
 
13
- **PLEASE DON'T** - use [discuss.dry-rb.org](http://discuss.dry-rb.org) instead.
13
+ **PLEASE DON'T** - use [discourse.dry-rb.org](https://discourse.dry-rb.org) instead.
14
14
 
15
15
  # Pull Request Guidelines
16
16
 
@@ -26,4 +26,4 @@ Other requirements:
26
26
 
27
27
  # Asking for help
28
28
 
29
- If these guidelines aren't helpful, and you're stuck, please post a message on [discuss.dry-rb.org](https://discuss.dry-rb.org).
29
+ If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.dry-rb.org](https://discourse.dry-rb.org).
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  gem 'dry-types', git: 'https://github.com/dry-rb/dry-types'
6
+ gem 'dry-inflector', git: 'https://github.com/dry-rb/dry-inflector'
6
7
 
7
8
  group :test do
8
9
  gem 'codeclimate-test-reporter', platform: :mri, require: false
@@ -11,7 +12,8 @@ group :test do
11
12
  end
12
13
 
13
14
  group :tools do
14
- gem 'byebug', platform: :mri
15
+ gem 'pry-byebug', platform: :mri
16
+ gem 'pry', platform: :jruby
15
17
  gem 'mutant'
16
18
  gem 'mutant-rspec'
17
19
  end
@@ -3,12 +3,10 @@
3
3
  require 'bundler/setup'
4
4
  require 'dry/struct'
5
5
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
6
+ require 'irb'
8
7
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
8
+ module Types
9
+ include Dry::Types.module
10
+ end
12
11
 
13
- require 'irb'
14
- IRB.start
12
+ binding.irb
@@ -27,8 +27,8 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ['lib']
28
28
 
29
29
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
30
- spec.add_runtime_dependency 'dry-types', '~> 0.12', '>= 0.12.2'
31
- spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.1'
30
+ spec.add_runtime_dependency 'dry-types', '~> 0.13'
31
+ spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.3'
32
32
  spec.add_runtime_dependency 'ice_nine', '~> 0.11'
33
33
 
34
34
  spec.add_development_dependency 'bundler', '~> 1.6'
@@ -1,12 +1,38 @@
1
- require 'dry/core/constants'
2
1
  require 'dry-types'
2
+ require 'dry-equalizer'
3
+ require 'dry/core/constants'
3
4
 
4
5
  require 'dry/struct/version'
5
6
  require 'dry/struct/errors'
6
7
  require 'dry/struct/class_interface'
7
8
  require 'dry/struct/hashify'
9
+ require 'dry/struct/struct_builder'
8
10
 
9
11
  module Dry
12
+ # Constructor method for easily creating a {Dry::Struct}.
13
+ # @return [Dry::Struct]
14
+ # @example
15
+ # require 'dry-struct'
16
+ #
17
+ # module Types
18
+ # include Dry::Types.module
19
+ # end
20
+ #
21
+ # Person = Dry.Struct(name: Types::Strict::String, age: Types::Strict::Int)
22
+ # matz = Person.new(name: "Matz", age: 52)
23
+ # matz.name #=> "Matz"
24
+ # matz.age #=> 52
25
+ #
26
+ # Test = Dry.Struct(expected: Types::Strict::String) { input(input.strict) }
27
+ # Test[expected: "foo", unexpected: "bar"]
28
+ # #=> Dry::Struct::Error: [Test.new] unexpected keys [:unexpected] in Hash input
29
+ def self.Struct(attributes = Dry::Core::Constants::EMPTY_HASH, &block)
30
+ Class.new(Dry::Struct) do
31
+ attributes.each { |a, type| attribute a, type }
32
+ instance_eval(&block) if block
33
+ end
34
+ end
35
+
10
36
  # Typed {Struct} with virtus-like DSL for defining schema.
11
37
  #
12
38
  # ### Differences between dry-struct and virtus
@@ -18,14 +44,12 @@ module Dry
18
44
  # * Handling of attribute values is provided by standalone type objects from
19
45
  # [`dry-types`][].
20
46
  # * Handling of attribute hashes is provided by standalone hash schemas from
21
- # [`dry-types`][], which means there are different types of constructors in
22
- # {Struct} (see {Dry::Struct::ClassInterface#constructor_type})
47
+ # [`dry-types`][].
23
48
  # * Struct classes quack like [`dry-types`][], which means you can use them
24
49
  # in hash schemas, as array members or sum them
25
50
  #
26
51
  # {Struct} class can specify a constructor type, which uses [hash schemas][]
27
52
  # to handle attributes in `.new` method.
28
- # See {ClassInterface#new} for constructor types descriptions and examples.
29
53
  #
30
54
  # [`dry-types`]: https://github.com/dry-rb/dry-types
31
55
  # [Virtus]: https://github.com/solnic/virtus
@@ -60,127 +84,22 @@ module Dry
60
84
  include Dry::Core::Constants
61
85
  extend ClassInterface
62
86
 
63
- # {Dry::Types::Hash} subclass with specific behaviour defined for
64
- # @return [Dry::Types::Hash]
65
- # @see #constructor_type
66
- defines :input
67
- input Types['coercible.hash']
68
-
69
- # @return [Hash{Symbol => Dry::Types::Definition, Dry::Struct}]
70
- defines :schema
71
- schema EMPTY_HASH
87
+ include Dry::Equalizer(:__attributes__)
72
88
 
73
- CONSTRUCTOR_TYPE = Dry::Types['symbol'].enum(:permissive, :schema, :strict, :strict_with_defaults)
89
+ # {Dry::Types::Hash::Schema} subclass with specific behaviour defined for
90
+ # @return [Dry::Types::Hash::Schema]
91
+ defines :input
92
+ input Types['coercible.hash'].schema(EMPTY_HASH)
74
93
 
75
- # Sets or retrieves {#constructor} type as a symbol
76
- #
77
- # @note All examples below assume that you have defined {Struct} with
78
- # following attributes and explicitly call only {#constructor_type}:
79
- #
80
- # ```ruby
81
- # class User < Dry::Struct
82
- # attribute :name, Types::Strict::String.default('John Doe')
83
- # attribute :age, Types::Strict::Int
84
- # end
85
- # ```
86
- #
87
- # ### Common constructor types include:
88
- #
89
- # * `:permissive` - the default constructor type, useful for defining
90
- # {Struct}s that are instantiated using data from the database
91
- # (i.e. results of a database query), where you expect *all defined
92
- # attributes to be present* and it's OK to ignore other keys
93
- # (i.e. keys used for joining, that are not relevant from your domain
94
- # {Struct}s point of view). Default values **are not used** otherwise
95
- # you wouldn't notice missing data.
96
- # * `:schema` - missing keys will result in setting them using default
97
- # values, unexpected keys will be ignored.
98
- # * `:strict` - useful when you *do not expect keys other than the ones
99
- # you specified as attributes* in the input hash
100
- # * `:strict_with_defaults` - same as `:strict` but you are OK that some
101
- # values may be nil and you want defaults to be set
102
- #
103
- # To feel the difference between constructor types, look into examples.
104
- # Each of them provide the same attributes' definitions,
105
- # different constructor type, and 4 cases of given input:
106
- #
107
- # 1. Input omits a key for a value that does not have a default
108
- # 2. Input omits a key for a value that has a default
109
- # 3. Input contains nil for a value that specifies a default
110
- # 4. Input includes a key that was not specified in the schema
111
- #
112
- # @example `:permissive` constructor
113
- # class User < Dry::Struct
114
- # constructor_type :permissive
115
- # end
116
- #
117
- # User.new(name: "Jane")
118
- # #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
119
- # User.new(age: 31)
120
- # #=> Dry::Struct::Error: [User.new] :name is missing in Hash input
121
- # User.new(name: nil, age: 31)
122
- # #=> #<User name="John Doe" age=31>
123
- # User.new(name: "Jane", age: 31, unexpected: "attribute")
124
- # #=> #<User name="Jane" age=31>
125
- #
126
- # @example `:schema` constructor
127
- # class User < Dry::Struct
128
- # constructor_type :schema
129
- # end
130
- #
131
- # User.new(name: "Jane") #=> #<User name="Jane" age=nil>
132
- # User.new(age: 31) #=> #<User name="John Doe" age=31>
133
- # User.new(name: nil, age: 31) #=> #<User name="John Doe" age=31>
134
- # User.new(name: "Jane", age: 31, unexpected: "attribute")
135
- # #=> #<User name="Jane" age=31>
136
- #
137
- # @example `:strict` constructor
138
- # class User < Dry::Struct
139
- # constructor_type :strict
140
- # end
141
- #
142
- # User.new(name: "Jane")
143
- # #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
144
- # User.new(age: 31)
145
- # #=> Dry::Struct::Error: [User.new] :name is missing in Hash input
146
- # User.new(name: nil, age: 31)
147
- # #=> Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
148
- # User.new(name: "Jane", age: 31, unexpected: "attribute")
149
- # #=> Dry::Struct::Error: [User.new] unexpected keys [:unexpected] in Hash input
150
- #
151
- # @example `:strict_with_defaults` constructor
152
- # class User < Dry::Struct
153
- # constructor_type :strict_with_defaults
154
- # end
155
- #
156
- # User.new(name: "Jane")
157
- # #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
158
- # User.new(age: 31)
159
- # #=> #<User name="John Doe" age=31>
160
- # User.new(name: nil, age: 31)
161
- # #=> Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
162
- # User.new(name: "Jane", age: 31, unexpected: "attribute")
163
- # #=> Dry::Struct::Error: [User.new] unexpected keys [:unexpected] in Hash input
164
- #
165
- # @see http://dry-rb.org/gems/dry-types/hash-schemas
166
- #
167
- # @overload constructor_type(type)
168
- # Sets the constructor type for {Struct}
169
- # @param [Symbol] type one of constructor types, see above
170
- # @return [Symbol]
171
- #
172
- # @overload constructor_type
173
- # Returns the constructor type for {Struct}
174
- # @return [Symbol] (:strict)
175
- defines :constructor_type, type: CONSTRUCTOR_TYPE
176
- constructor_type :permissive
94
+ @meta = EMPTY_HASH
177
95
 
178
- # @return [Dry::Equalizer]
179
- defines :equalizer
96
+ # @!attribute [Hash{Symbol => Object}] attributes
97
+ attr_reader :attributes
98
+ alias_method :__attributes__, :attributes
180
99
 
181
100
  # @param [Hash, #each] attributes
182
101
  def initialize(attributes)
183
- attributes.each { |key, value| instance_variable_set("@#{key}", value) }
102
+ @attributes = attributes
184
103
  end
185
104
 
186
105
  # Retrieves value of previously defined attribute by its' `name`
@@ -201,7 +120,9 @@ module Dry
201
120
  # rom_n_roda[:title] #=> 'Web Development with ROM and Roda'
202
121
  # rom_n_roda[:subtitle] #=> nil
203
122
  def [](name)
204
- public_send(name)
123
+ @attributes.fetch(name)
124
+ rescue KeyError
125
+ raise MissingAttributeError.new(name)
205
126
  end
206
127
 
207
128
  # Converts the {Dry::Struct} to a hash with keys representing
@@ -223,7 +144,7 @@ module Dry
223
144
  # #=> {title: 'Web Development with ROM and Roda', subtitle: nil}
224
145
  def to_hash
225
146
  self.class.schema.keys.each_with_object({}) do |key, result|
226
- result[key] = Hashify[self[key]]
147
+ result[key] = Hashify[self[key]] if attributes.key?(key)
227
148
  end
228
149
  end
229
150
  alias_method :to_h, :to_hash
@@ -253,12 +174,11 @@ module Dry
253
174
  end
254
175
  alias_method :__new__, :new
255
176
 
256
- # @return[Hash{Symbol => Object}]
257
- # @api private
258
- def __attributes__
259
- self.class.attribute_names.each_with_object({}) do |key, h|
260
- h[key] = instance_variable_get(:"@#{ key }")
261
- end
177
+ # @return [String]
178
+ def inspect
179
+ klass = self.class
180
+ attrs = klass.attribute_names.map { |key| " #{key}=#{@attributes[key].inspect}" }.join
181
+ "#<#{ klass.name || klass.inspect }#{ attrs }>"
262
182
  end
263
183
  end
264
184
  end
@@ -1,8 +1,10 @@
1
1
  require 'dry/core/class_attributes'
2
- require 'dry/equalizer'
2
+ require 'dry/core/inflector'
3
+ require 'dry/core/descendants_tracker'
3
4
 
4
5
  require 'dry/struct/errors'
5
6
  require 'dry/struct/constructor'
7
+ require 'dry/struct/sum'
6
8
 
7
9
  module Dry
8
10
  class Struct
@@ -17,30 +19,97 @@ module Dry
17
19
  def inherited(klass)
18
20
  super
19
21
 
20
- klass.equalizer Equalizer.new(*schema.keys)
21
- klass.send(:include, klass.equalizer)
22
+ base = self
23
+
24
+ klass.class_eval do
25
+ @meta = base.meta
26
+
27
+ unless equal?(Value)
28
+ extend Dry::Core::DescendantsTracker
29
+ end
30
+ end
22
31
  end
23
32
 
24
33
  # Adds an attribute for this {Struct} with given `name` and `type`
25
34
  # and modifies {.schema} accordingly.
26
35
  #
27
36
  # @param [Symbol] name name of the defined attribute
28
- # @param [Dry::Types::Definition] type
37
+ # @param [Dry::Types::Definition, nil] type or superclass of nested type
29
38
  # @return [Dry::Struct]
39
+ # @yield
40
+ # If a block is given, it will be evaluated in the context of
41
+ # a new struct class, and set as a nested type for the given
42
+ # attribute. A class with a matching name will also be defined for
43
+ # the nested type.
30
44
  # @raise [RepeatedAttributeError] when trying to define attribute with the
31
45
  # same name as previously defined one
32
46
  #
33
- # @example
47
+ # @example with nested structs
48
+ # class Language < Dry::Struct
49
+ # attribute :name, Types::String
50
+ # attribute :details, Dry::Struct do
51
+ # attribute :type, Types::String
52
+ # end
53
+ # end
54
+ #
55
+ # Language.schema
56
+ # #=> {
57
+ # :name=>#<Dry::Types::Definition primitive=String options={} meta={}>,
58
+ # :details=>Language::Details
59
+ # }
60
+ #
61
+ # ruby = Language.new(name: 'Ruby', details: { type: 'OO' })
62
+ # ruby.name #=> 'Ruby'
63
+ # ruby.details #=> #<Language::Details type="OO">
64
+ # ruby.details.type #=> 'OO'
65
+ #
66
+ # @example with a nested array of structs
34
67
  # class Language < Dry::Struct
35
68
  # attribute :name, Types::String
69
+ # array :versions, Types::String
70
+ # array :celebrities, Types::Array.of(Dry::Struct) do
71
+ # attribute :name, Types::String
72
+ # attribute :pseudonym, Types::String
73
+ # end
36
74
  # end
37
75
  #
38
76
  # Language.schema
39
- # #=> {name: #<Dry::Types::Definition primitive=String options={}>}
77
+ # #=> {
78
+ # :name=>#<Dry::Types::Definition primitive=String options={} meta={}>,
79
+ # :versions=>#<Dry::Types::Array::Member primitive=Array options={:member=>#<Dry::Types::Definition primitive=String options={} meta={}>} meta={}>,
80
+ # :celebrities=>#<Dry::Types::Array::Member primitive=Array options={:member=>Language::Celebrity} meta={}>
81
+ # }
40
82
  #
41
- # ruby = Language.new(name: 'Ruby')
83
+ # ruby = Language.new(
84
+ # name: 'Ruby',
85
+ # versions: %w(1.8.7 1.9.8 2.0.1),
86
+ # celebrities: [
87
+ # { name: 'Yukihiro Matsumoto', pseudonym: 'Matz' },
88
+ # { name: 'Aaron Patterson', pseudonym: 'tenderlove' }
89
+ # ]
90
+ # )
42
91
  # ruby.name #=> 'Ruby'
43
- def attribute(name, type)
92
+ # ruby.versions #=> ['1.8.7', '1.9.8', '2.0.1']
93
+ # ruby.celebrities
94
+ # #=> [
95
+ # #<Language::Celebrity name='Yukihiro Matsumoto' pseudonym='Matz'>,
96
+ # #<Language::Celebrity name='Aaron Patterson' pseudonym='tenderlove'>
97
+ # ]
98
+ # ruby.celebrities[0].name #=> 'Yukihiro Matsumoto'
99
+ # ruby.celebrities[0].pseudonym #=> 'Matz'
100
+ # ruby.celebrities[1].name #=> 'Aaron Patterson'
101
+ # ruby.celebrities[1].pseudonym #=> 'tenderlove'
102
+ def attribute(name, type = nil, &block)
103
+ if block
104
+ type = Dry::Types[type] if type.is_a?(String)
105
+ type = struct_builder.(name, type, &block)
106
+ elsif type.nil?
107
+ raise(
108
+ ArgumentError,
109
+ 'you must supply a type or a block to `Dry::Struct.attribute`'
110
+ )
111
+ end
112
+
44
113
  attributes(name => type)
45
114
  end
46
115
 
@@ -50,7 +119,7 @@ module Dry
50
119
  # same name as previously defined one
51
120
  # @see #attribute
52
121
  # @example
53
- # class Book1 < Dry::Struct
122
+ # class Book < Dry::Struct
54
123
  # attributes(
55
124
  # title: Types::String,
56
125
  # author: Types::String
@@ -63,18 +132,60 @@ module Dry
63
132
  def attributes(new_schema)
64
133
  check_schema_duplication(new_schema)
65
134
 
66
- schema schema.merge(new_schema)
67
- input Types['coercible.hash'].public_send(constructor_type, schema)
135
+ input input.schema(new_schema)
68
136
 
69
137
  new_schema.each_key do |key|
70
- attr_reader(key) unless instance_methods.include?(key)
138
+ next if instance_methods.include?(key)
139
+ class_eval(<<-RUBY)
140
+ def #{ key }
141
+ @attributes[#{ key.inspect }]
142
+ end
143
+ RUBY
71
144
  end
72
145
 
73
- equalizer.instance_variable_get('@keys').concat(new_schema.keys)
146
+ @attribute_names = nil
147
+
148
+ descendants.
149
+ select { |d| d.superclass == self }.
150
+ each { |d| d.attributes(new_schema.reject { |k, _| d.schema.key?(k) }) }
74
151
 
75
152
  self
76
153
  end
77
154
 
155
+ # Add an arbitrary transformation for new attribute types.
156
+ #
157
+ # @param [#call,nil] proc
158
+ # @param [#call,nil] block
159
+ # @example
160
+ # class Book < Dry::Struct
161
+ # transform_types { |t| t.meta(struct: :Book) }
162
+ #
163
+ # attribute :title, Types::Strict::String
164
+ # end
165
+ #
166
+ # Book.schema[:title].meta # => { struct: :Book }
167
+ #
168
+ def transform_types(proc = nil, &block)
169
+ input input.with_type_transform(proc || block)
170
+ end
171
+
172
+ # Add an arbitrary transformation for input hash keys.
173
+ #
174
+ # @param [#call,nil] proc
175
+ # @param [#call,nil] block
176
+ # @example
177
+ # class Book < Dry::Struct
178
+ # transform_keys(&:to_sym)
179
+ #
180
+ # attribute :title, Types::Strict::String
181
+ # end
182
+ #
183
+ # Book.new('title' => "The Old Man and the Sea")
184
+ # # => #<Book title="The Old Man and the Sea">
185
+ def transform_keys(proc = nil, &block)
186
+ input input.with_key_transform(proc || block)
187
+ end
188
+
78
189
  # @param [Hash{Symbol => Dry::Types::Definition, Dry::Struct}] new_schema
79
190
  # @raise [RepeatedAttributeError] when trying to define attribute with the
80
191
  # same name as previously defined one
@@ -87,7 +198,6 @@ module Dry
87
198
 
88
199
  # @param [Hash{Symbol => Object},Dry::Struct] attributes
89
200
  # @raise [Struct::Error] if the given attributes don't conform {#schema}
90
- # with given {#constructor_type}
91
201
  def new(attributes = default_attributes)
92
202
  if attributes.instance_of?(self)
93
203
  attributes
@@ -110,34 +220,14 @@ module Dry
110
220
  alias_method :[], :call
111
221
 
112
222
  # @param [#call,nil] constructor
113
- # @param [Hash] options
223
+ # @param [Hash] _options
114
224
  # @param [#call,nil] block
115
225
  # @return [Dry::Struct::Constructor]
116
226
  def constructor(constructor = nil, **_options, &block)
117
227
  Struct::Constructor.new(self, fn: constructor || block)
118
228
  end
119
229
 
120
- # Retrieves default attributes from defined {.schema}.
121
- # Used in a {Struct} constructor if no attributes provided to {.new}
122
- #
123
- # @return [Hash{Symbol => Object}]
124
- def default_attributes
125
- check_invalid_schema_keys
126
- schema.each_with_object({}) { |(name, type), result|
127
- result[name] = type.evaluate if type.default?
128
- }
129
- end
130
-
131
- def check_invalid_schema_keys
132
- invalid_keys = schema.select { |name, type| type.instance_of?(String) }
133
- raise ArgumentError, argument_error_msg(invalid_keys.keys) if invalid_keys.any?
134
- end
135
-
136
- def argument_error_msg(keys)
137
- "Invaild argument for #{keys.join(', ')}"
138
- end
139
-
140
- # @param [Hash{Symbol => Object}] input
230
+ # @param [Hash{Symbol => Object},Dry::Struct] input
141
231
  # @yieldparam [Dry::Types::Result::Failure] failure
142
232
  # @yieldreturn [Dry::Types::ResultResult]
143
233
  # @return [Dry::Types::Result]
@@ -148,6 +238,17 @@ module Dry
148
238
  block_given? ? yield(failure) : failure
149
239
  end
150
240
 
241
+ # @param [Hash{Symbol => Object},Dry::Struct] input
242
+ # @return [Dry::Types::Result]
243
+ # @private
244
+ def try_struct(input)
245
+ if input.is_a?(self)
246
+ Types::Result::Success.new(input)
247
+ else
248
+ yield
249
+ end
250
+ end
251
+
151
252
  # @param [({Symbol => Object})] args
152
253
  # @return [Dry::Types::Result::Success]
153
254
  def success(*args)
@@ -157,7 +258,7 @@ module Dry
157
258
  # @param [({Symbol => Object})] args
158
259
  # @return [Dry::Types::Result::Failure]
159
260
  def failure(*args)
160
- result(Types::Result::Failure, *args)
261
+ result(::Dry::Types::Result::Failure, *args)
161
262
  end
162
263
 
163
264
  # @param [Class] klass
@@ -200,12 +301,66 @@ module Dry
200
301
  schema.key?(key)
201
302
  end
202
303
 
304
+ # @return [Hash{Symbol => Dry::Types::Definition, Dry::Struct}]
305
+ def schema
306
+ input.member_types
307
+ end
308
+
203
309
  # Gets the list of attribute names
204
310
  #
205
311
  # @return [Array<Symbol>]
206
312
  def attribute_names
207
313
  @attribute_names ||= schema.keys
208
314
  end
315
+
316
+ # @return [{Symbol => Object}]
317
+ def meta(meta = Undefined)
318
+ if meta.equal?(Undefined)
319
+ @meta
320
+ else
321
+ Class.new(self) do
322
+ @meta = @meta.merge(meta) unless meta.empty?
323
+ end
324
+ end
325
+ end
326
+
327
+ # Build a sum type
328
+ # @param [Dry::Types::Type] type
329
+ # @return [Dry::Types::Sum]
330
+ def |(type)
331
+ if type.is_a?(Class) && type <= Struct
332
+ Struct::Sum.new(self, type)
333
+ else
334
+ super
335
+ end
336
+ end
337
+
338
+ # Stores an object for building nested struct classes
339
+ # @return [StructBuilder]
340
+ def struct_builder
341
+ @struct_builder ||= StructBuilder.new(self).freeze
342
+ end
343
+ private :struct_builder
344
+
345
+ # Retrieves default attributes from defined {.schema}.
346
+ # Used in a {Struct} constructor if no attributes provided to {.new}
347
+ #
348
+ # @return [Hash{Symbol => Object}]
349
+ def default_attributes(default_schema = schema)
350
+ default_schema.each_with_object({}) do |(name, type), result|
351
+ result[name] = default_attributes(type.schema) if struct?(type)
352
+ end
353
+ end
354
+ private :default_attributes
355
+
356
+ # Checks if the given type is a Dry::Struct
357
+ #
358
+ # @param [Dry::Types::Definition, Dry::Struct] type
359
+ # @return [Boolean]
360
+ def struct?(type)
361
+ type.is_a?(Class) && type <= Struct
362
+ end
363
+ private :struct?
209
364
  end
210
365
  end
211
366
  end
@@ -11,5 +11,12 @@ module Dry
11
11
  super("Attribute :#{key} has already been defined")
12
12
  end
13
13
  end
14
+
15
+ # Raised when a struct doesn't have an attribute
16
+ class MissingAttributeError < KeyError
17
+ def initialize(key)
18
+ super("Missing attribute: #{ key.inspect }")
19
+ end
20
+ end
14
21
  end
15
22
  end
@@ -8,8 +8,8 @@ module Dry
8
8
  def self.[](value)
9
9
  if value.respond_to?(:to_hash)
10
10
  value.to_hash
11
- elsif value.respond_to?(:map)
12
- value.map { |item| self[item] }
11
+ elsif value.respond_to?(:to_ary)
12
+ value.to_ary.map { |item| self[item] }
13
13
  else
14
14
  value
15
15
  end
@@ -0,0 +1,86 @@
1
+ require 'dry/types/compiler'
2
+
3
+ module Dry
4
+ class Struct
5
+ # @private
6
+ class StructBuilder < Dry::Types::Compiler
7
+ attr_reader :struct
8
+
9
+ def initialize(struct)
10
+ super(Dry::Types)
11
+ @struct = struct
12
+ end
13
+
14
+ # @param [Symbol|String] attr_name the name of the nested type
15
+ # @param [Dry::Struct,Dry::Types::Type::Array] type the superclass of the nested struct
16
+ # @yield the body of the nested struct
17
+ def call(attr_name, type, &block)
18
+ const_name = const_name(type, attr_name)
19
+ check_name(const_name)
20
+
21
+ new_type = Class.new(parent(type), &block)
22
+ struct.const_set(const_name, new_type)
23
+
24
+ if array?(type)
25
+ type.of(new_type)
26
+ else
27
+ new_type
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def array?(type)
34
+ type.is_a?(Types::Type) && type.primitive.equal?(Array)
35
+ end
36
+
37
+ def parent(type)
38
+ if array?(type)
39
+ visit(type.to_ast)
40
+ else
41
+ type || default_superclass
42
+ end
43
+ end
44
+
45
+ def default_superclass
46
+ struct < Value ? Value : Struct
47
+ end
48
+
49
+ def const_name(type, attr_name)
50
+ snake_name = if array?(type)
51
+ Dry::Core::Inflector.singularize(attr_name)
52
+ else
53
+ attr_name
54
+ end
55
+
56
+ Dry::Core::Inflector.camelize(snake_name)
57
+ end
58
+
59
+ def check_name(name)
60
+ raise(
61
+ Struct::Error,
62
+ "Can't create nested attribute - `#{struct}::#{name}` already defined"
63
+ ) if struct.const_defined?(name)
64
+ end
65
+
66
+ def visit_constrained(node)
67
+ definition, * = node
68
+ visit(definition)
69
+ end
70
+
71
+ def visit_array(node)
72
+ member, * = node
73
+ member
74
+ end
75
+
76
+ def visit_definition(*)
77
+ default_superclass
78
+ end
79
+
80
+ def visit_constructor(node)
81
+ definition, * = node
82
+ visit(definition)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,42 @@
1
+ require 'dry/types/sum'
2
+
3
+ module Dry
4
+ class Struct
5
+ # A sum type of two or more structs
6
+ # As opposed to Dry::Types::Sum::Constrained
7
+ # this type tries no to coerce data first.
8
+ class Sum < Dry::Types::Sum::Constrained
9
+ # @param [Hash{Symbol => Object},Dry::Struct] input
10
+ # @yieldparam [Dry::Types::Result::Failure] failure
11
+ # @yieldreturn [Dry::Types::ResultResult]
12
+ # @return [Dry::Types::Result]
13
+ def try(input)
14
+ if input.is_a?(Struct)
15
+ try_struct(input) { super }
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ # Build a new sum type
22
+ # @param [Dry::Types::Type] type
23
+ # @return [Dry::Types::Sum]
24
+ def |(type)
25
+ if type.is_a?(Class) && type <= Struct || type.is_a?(Sum)
26
+ self.class.new(self, type)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ # @private
35
+ def try_struct(input)
36
+ left.try_struct(input) do
37
+ right.try_struct(input) { yield }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,6 @@
1
1
  module Dry
2
2
  class Struct
3
- VERSION = '0.4.0'.freeze
3
+ # @private
4
+ VERSION = '0.5.0'.freeze
4
5
  end
5
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-struct
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-04 00:00:00.000000000 Z
11
+ date: 2018-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-equalizer
@@ -30,20 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.12'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 0.12.2
33
+ version: '0.13'
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
38
  - - "~>"
42
39
  - !ruby/object:Gem::Version
43
- version: '0.12'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 0.12.2
40
+ version: '0.13'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: dry-core
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,7 +47,7 @@ dependencies:
53
47
  version: '0.4'
54
48
  - - ">="
55
49
  - !ruby/object:Gem::Version
56
- version: 0.4.1
50
+ version: 0.4.3
57
51
  type: :runtime
58
52
  prerelease: false
59
53
  version_requirements: !ruby/object:Gem::Requirement
@@ -63,7 +57,7 @@ dependencies:
63
57
  version: '0.4'
64
58
  - - ">="
65
59
  - !ruby/object:Gem::Version
66
- version: 0.4.1
60
+ version: 0.4.3
67
61
  - !ruby/object:Gem::Dependency
68
62
  name: ice_nine
69
63
  requirement: !ruby/object:Gem::Requirement
@@ -162,6 +156,8 @@ files:
162
156
  - lib/dry/struct/constructor.rb
163
157
  - lib/dry/struct/errors.rb
164
158
  - lib/dry/struct/hashify.rb
159
+ - lib/dry/struct/struct_builder.rb
160
+ - lib/dry/struct/sum.rb
165
161
  - lib/dry/struct/value.rb
166
162
  - lib/dry/struct/version.rb
167
163
  homepage: https://github.com/dry-rb/dry-struct
@@ -185,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
181
  version: '0'
186
182
  requirements: []
187
183
  rubyforge_project:
188
- rubygems_version: 2.6.11
184
+ rubygems_version: 2.7.6
189
185
  signing_key:
190
186
  specification_version: 4
191
187
  summary: Typed structs and value objects.