dry-struct 0.4.0 → 0.5.0

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