-
-
Notifications
You must be signed in to change notification settings - Fork 63
/
struct.rb
268 lines (255 loc) · 9.42 KB
/
struct.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
require 'dry/core/constants'
require 'dry-types'
require 'dry/struct/version'
require 'dry/struct/errors'
require 'dry/struct/class_interface'
require 'dry/struct/hashify'
module Dry
# Typed {Struct} with virtus-like DSL for defining schema.
#
# ### Differences between dry-struct and virtus
#
# {Struct} look somewhat similar to [Virtus][] but there are few significant differences:
#
# * {Struct}s don't provide attribute writers and are meant to be used
# as "data objects" exclusively.
# * Handling of attribute values is provided by standalone type objects from
# [`dry-types`][].
# * Handling of attribute hashes is provided by standalone hash schemas from
# [`dry-types`][], which means there are different types of constructors in
# {Struct} (see {Dry::Struct::ClassInterface#constructor_type})
# * Struct classes quack like [`dry-types`][], which means you can use them
# in hash schemas, as array members or sum them
#
# {Struct} class can specify a constructor type, which uses [hash schemas][]
# to handle attributes in `.new` method.
# See {ClassInterface#new} for constructor types descriptions and examples.
#
# [`dry-types`]: https://github.com/dry-rb/dry-types
# [Virtus]: https://github.com/solnic/virtus
# [hash schemas]: http://dry-rb.org/gems/dry-types/hash-schemas
#
# @example
# require 'dry-struct'
#
# module Types
# include Dry::Types.module
# end
#
# class Book < Dry::Struct
# attribute :title, Types::Strict::String
# attribute :subtitle, Types::Strict::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: nil
# )
# rom_n_roda.title #=> 'Web Development with ROM and Roda'
# rom_n_roda.subtitle #=> nil
#
# refactoring = Book.new(
# title: 'Refactoring',
# subtitle: 'Improving the Design of Existing Code'
# )
# refactoring.title #=> 'Refactoring'
# refactoring.subtitle #=> 'Improving the Design of Existing Code'
class Struct
include Dry::Core::Constants
extend ClassInterface
# {Dry::Types::Hash} subclass with specific behaviour defined for
# @return [Dry::Types::Hash]
# @see #constructor_type
defines :input
input Types['coercible.hash']
# @return [Hash{Symbol => Dry::Types::Definition, Dry::Struct}]
defines :schema
schema EMPTY_HASH
def self.valid_constructor_types
Dry::Types['symbol'].enum(:permissive, :schema, :strict, :strict_with_defaults)
end
# Sets or retrieves {#constructor} type as a symbol
#
# @note All examples below assume that you have defined {Struct} with
# following attributes and explicitly call only {#constructor_type}:
#
# ```ruby
# class User < Dry::Struct
# attribute :name, Types::Strict::String.default('John Doe')
# attribute :age, Types::Strict::Int
# end
# ```
#
# ### Common constructor types include:
#
# * `:permissive` - the default constructor type, useful for defining
# {Struct}s that are instantiated using data from the database
# (i.e. results of a database query), where you expect *all defined
# attributes to be present* and it's OK to ignore other keys
# (i.e. keys used for joining, that are not relevant from your domain
# {Struct}s point of view). Default values **are not used** otherwise
# you wouldn't notice missing data.
# * `:schema` - missing keys will result in setting them using default
# values, unexpected keys will be ignored.
# * `:strict` - useful when you *do not expect keys other than the ones
# you specified as attributes* in the input hash
# * `:strict_with_defaults` - same as `:strict` but you are OK that some
# values may be nil and you want defaults to be set
#
# To feel the difference between constructor types, look into examples.
# Each of them provide the same attributes' definitions,
# different constructor type, and 4 cases of given input:
#
# 1. Input omits a key for a value that does not have a default
# 2. Input omits a key for a value that has a default
# 3. Input contains nil for a value that specifies a default
# 4. Input includes a key that was not specified in the schema
#
# @example `:permissive` constructor
# class User < Dry::Struct
# constructor_type :permissive
# end
#
# User.new(name: "Jane")
# #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
# User.new(age: 31)
# #=> Dry::Struct::Error: [User.new] :name is missing in Hash input
# User.new(name: nil, age: 31)
# #=> #<User name="John Doe" age=31>
# User.new(name: "Jane", age: 31, unexpected: "attribute")
# #=> #<User name="Jane" age=31>
#
# @example `:schema` constructor
# class User < Dry::Struct
# constructor_type :schema
# end
#
# User.new(name: "Jane") #=> #<User name="Jane" age=nil>
# User.new(age: 31) #=> #<User name="John Doe" age=31>
# User.new(name: nil, age: 31) #=> #<User name="John Doe" age=31>
# User.new(name: "Jane", age: 31, unexpected: "attribute")
# #=> #<User name="Jane" age=31>
#
# @example `:strict` constructor
# class User < Dry::Struct
# constructor_type :strict
# end
#
# User.new(name: "Jane")
# #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
# User.new(age: 31)
# #=> Dry::Struct::Error: [User.new] :name is missing in Hash input
# User.new(name: nil, age: 31)
# #=> Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
# User.new(name: "Jane", age: 31, unexpected: "attribute")
# #=> Dry::Struct::Error: [User.new] unexpected keys [:unexpected] in Hash input
#
# @example `:strict_with_defaults` constructor
# class User < Dry::Struct
# constructor_type :strict_with_defaults
# end
#
# User.new(name: "Jane")
# #=> Dry::Struct::Error: [User.new] :age is missing in Hash input
# User.new(age: 31)
# #=> #<User name="John Doe" age=31>
# User.new(name: nil, age: 31)
# #=> Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
# User.new(name: "Jane", age: 31, unexpected: "attribute")
# #=> Dry::Struct::Error: [User.new] unexpected keys [:unexpected] in Hash input
#
# @see http://dry-rb.org/gems/dry-types/hash-schemas
#
# @overload constructor_type(type)
# Sets the constructor type for {Struct}
# @param [Symbol] type one of constructor types, see above
# @return [Symbol]
#
# @overload constructor_type
# Returns the constructor type for {Struct}
# @return [Symbol] (:strict)
defines :constructor_type, type: valid_constructor_types
constructor_type :permissive
# @return [Dry::Equalizer]
defines :equalizer
# @param [Hash, #each] attributes
def initialize(attributes)
attributes.each { |key, value| instance_variable_set("@#{key}", value) }
end
# Retrieves value of previously defined attribute by its' `name`
#
# @param [String] name
# @return [Object]
#
# @example
# class Book < Dry::Struct
# attribute :title, Types::Strict::String
# attribute :subtitle, Types::Strict::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: nil
# )
# rom_n_roda[:title] #=> 'Web Development with ROM and Roda'
# rom_n_roda[:subtitle] #=> nil
def [](name)
public_send(name)
end
# Converts the {Dry::Struct} to a hash with keys representing
# each attribute (as symbols) and their corresponding values
#
# @return [Hash{Symbol => Object}]
#
# @example
# class Book < Dry::Struct
# attribute :title, Types::Strict::String
# attribute :subtitle, Types::Strict::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: nil
# )
# rom_n_roda.to_hash
# #=> {title: 'Web Development with ROM and Roda', subtitle: nil}
def to_hash
self.class.schema.keys.each_with_object({}) do |key, result|
result[key] = Hashify[self[key]]
end
end
alias_method :to_h, :to_hash
# Create a copy of {Dry::Struct} with overriden attributes
#
# @param [Hash{Symbol => Object}] changeset
#
# @return [Struct]
#
# @example
# class Book < Dry::Struct
# attribute :title, Types::Strict::String
# attribute :subtitle, Types::Strict::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: '2nd edition'
# )
# #=> #<Book title="Web Development with ROM and Roda" subtitle="2nd edition">
#
# rom_n_roda.new(subtitle: '3nd edition')
# #=> #<Book title="Web Development with ROM and Roda" subtitle="3nd edition">
def new(changeset)
self.class[__attributes__.merge(changeset)]
end
alias_method :__new__, :new
# @return[Hash{Symbol => Object}]
# @api private
def __attributes__
self.class.attribute_names.each_with_object({}) do |key, h|
h[key] = instance_variable_get(:"@#{ key }")
end
end
end
end
require 'dry/struct/value'