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 +5 -5
- data/.travis.yml +7 -4
- data/CHANGELOG.md +70 -0
- data/CONTRIBUTING.md +3 -3
- data/Gemfile +3 -1
- data/bin/console +5 -7
- data/dry-struct.gemspec +2 -2
- data/lib/dry/struct.rb +47 -127
- data/lib/dry/struct/class_interface.rb +192 -37
- data/lib/dry/struct/errors.rb +7 -0
- data/lib/dry/struct/hashify.rb +2 -2
- data/lib/dry/struct/struct_builder.rb +86 -0
- data/lib/dry/struct/sum.rb +42 -0
- data/lib/dry/struct/version.rb +2 -1
- metadata +9 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4f15a9185e2b7ee171f632db8fc95c29aafa567d4936fe3e8681b073af810f76
|
4
|
+
data.tar.gz: eec2908a47ad4e549a454bf6890dbb2402f689222810ffeab52b64958ff0caff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f743fe48c3651573fecbf46fa0c81cd4cd2327774ae903bcd1bea60dc98b09dc6c63acbcca506c8021c383a8b6ce9d74725956c94cca976b22d1a5800d34913b
|
7
|
+
data.tar.gz: 6f898126cac3c667d60ac352de8fbaacfb24aa417136881f315858c22d337868a3544894d9caea1da6fe9c3cb705c2c45702f0c1228ddbc7bd43d8a44d8256dc
|
data/.travis.yml
CHANGED
@@ -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.
|
11
|
-
- 2.3.
|
12
|
-
- 2.4.
|
13
|
-
-
|
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
|
data/CHANGELOG.md
CHANGED
@@ -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
|
data/CONTRIBUTING.md
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
|
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 [
|
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 [
|
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
|
data/bin/console
CHANGED
@@ -3,12 +3,10 @@
|
|
3
3
|
require 'bundler/setup'
|
4
4
|
require 'dry/struct'
|
5
5
|
|
6
|
-
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
6
|
+
require 'irb'
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
module Types
|
9
|
+
include Dry::Types.module
|
10
|
+
end
|
12
11
|
|
13
|
-
|
14
|
-
IRB.start
|
12
|
+
binding.irb
|
data/dry-struct.gemspec
CHANGED
@@ -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.
|
31
|
-
spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.
|
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'
|
data/lib/dry/struct.rb
CHANGED
@@ -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`][]
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
179
|
-
|
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
|
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
|
-
|
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[
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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/
|
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
|
-
|
21
|
-
|
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
|
-
# #=> {
|
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(
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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]
|
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
|
-
#
|
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
|
data/lib/dry/struct/errors.rb
CHANGED
@@ -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
|
data/lib/dry/struct/hashify.rb
CHANGED
@@ -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
|
data/lib/dry/struct/version.rb
CHANGED
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
|
+
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:
|
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.
|
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.
|
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.
|
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.
|
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
|
184
|
+
rubygems_version: 2.7.6
|
189
185
|
signing_key:
|
190
186
|
specification_version: 4
|
191
187
|
summary: Typed structs and value objects.
|