value_semantics 3.5.0 → 3.6.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
2
  SHA256:
3
- metadata.gz: '069ef7cc15f7d9b6eae3af432d486595bfa3f89c0f68ce68ab3eda2fb5d5f9f4'
4
- data.tar.gz: f8d1839f7176743cb9d7a144d5452f999048a03a5d1d86d8b86b81b648a7bfd3
3
+ metadata.gz: 8d3795e585d271a6b0b784ef28931a16ae7f1846b13f7d307bf97d9d175c5384
4
+ data.tar.gz: 8289168c0be7e6cd00e24e59239a9ab8a2026fc42895007f33388d6ffa67add4
5
5
  SHA512:
6
- metadata.gz: cea465484d60d6343815072b4e81639e399b20ce528a12ce643be39e3a06051fdaddb4c58037ffac5237f7c62b823b49d888bc8d6fed925b181515751b4cfb46
7
- data.tar.gz: 553a990a508956e7a2b6f96ea2ac65ee97737bc6ea5da1ea337f9d0d06b294aa61fb3e10ef1d8ce6ac5fff826b3f1bdbf17b9e805ac38b933308152929335efd
6
+ metadata.gz: 1b2520d80b112810e2b257920d591d4657cd9da26007307492cceac3321149b266bb310fba7c49e957d21a31e92bc041668bd907820b906570efa704d02c7948
7
+ data.tar.gz: 9605e0334ff2ff61dbb1925983b44f1275fb3768196bb62dbed6a2ccd9f6590b6e22ff3555101b6283181b18173657d363d397f8f10d329b9e4dea21ef1eb73c
@@ -5,6 +5,55 @@ Notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.6.0] - 2020-09-01
9
+ ### Added
10
+ - `RangeOf` built-in validator, for validating `Range` objects
11
+ - `HashCoercer` built-in coercer for homogeneous `Hash` objects
12
+ ### Changed
13
+ - Optimised speed of value object initialization. It is now roughly 3x
14
+ slower than that of a hand-written class, which is 2-3x faster than
15
+ the previous version.
16
+
17
+ - Optimised memory allocation in object initialization. The happy path
18
+ (no exceptions raised) only allocates a single array object, under
19
+ normal circumstances. Extra allocations are likely caused by custom
20
+ validators, coercers, and default generators.
21
+
22
+ - Exceptions raised when initialising a value object are now
23
+ aggregated. Instead of telling you the problematic attributes one at
24
+ a time, you will get a list of all offending attributes in the
25
+ exception message. This applies to `MissingAttributes`,
26
+ `InvalidValue` and `UnrecognizedAttributes`. These will probably be
27
+ combined into a single exception in v4.0, so you can see all the
28
+ initialization problems at once.
29
+
30
+ - The exceptions `ValueSemantics::MissingAttributes` and
31
+ `ValueSemantics::InvalidValue` are now raised from inside
32
+ `initialize`. They were previously raised from inside of
33
+ `ValueSemantics::Attribute.determine_from!` which is an internal
34
+ implementation detail that is basically gibberish to any developer
35
+ reading it. The stack trace for this exception reads much better.
36
+
37
+ - The exception `ValueSemantics::UnrecognizedAttributes` is now raised
38
+ instead of `ValueSemantics::MissingAttributes` in the situation
39
+ where both exceptions would be raised. This makes it easier to debug
40
+ the problem where you attempt to initialize a value object using a
41
+ hash with string keys instead of symbol keys.
42
+
43
+ - The coercer returned from the `.coercer` class method is now
44
+ smarter. It handles string keys, handles objects that can be
45
+ converted to hashes.
46
+ ### Deprecated
47
+ - `ValueSemantics::Attribute#determine_from!`. This was an internal
48
+ implementation detail, which is no longer used internally. Use the
49
+ `name`, `#coerce`, `#optional?`, `#default_generator` and
50
+ `#validate?` methods directly if you want to extract an attribute
51
+ from a hash.
52
+ - `ValueSemantics::NoDefaultError`. Use `Attribute#optional?` to check
53
+ whether there is a default.
54
+
55
+
56
+
8
57
  ## [3.5.0] - 2020-08-17
9
58
  ### Added
10
59
  - Square bracket attr reader like `person[:name]`
data/README.md CHANGED
@@ -50,7 +50,7 @@ Person.new(name: "Tom", birthday: "2020-12-25")
50
50
  #=> #<Person name="Tom" birthday=#<Date: 2020-12-25 ((2459209j,0s,0n),+0s,2299161j)>>
51
51
 
52
52
  Person.new(birthday: Date.today)
53
- #=> #<Person name="Anon Emous" birthday=#<Date: 2020-08-04 ((2459066j,0s,0n),+0s,2299161j)>>
53
+ #=> #<Person name="Anon Emous" birthday=#<Date: 2020-08-30 ((2459092j,0s,0n),+0s,2299161j)>>
54
54
 
55
55
  Person.new(birthday: nil)
56
56
  #=> #<Person name="Anon Emous" birthday=nil>
@@ -159,7 +159,7 @@ class Cat
159
159
  end
160
160
 
161
161
  Cat.new
162
- #=> #<Cat paws=4 born_at=2020-08-04 00:16:35.15632 +1000>
162
+ #=> #<Cat paws=4 born_at=2020-08-30 22:27:12.237812 +1000>
163
163
  ```
164
164
 
165
165
  The `default` option is a single value.
@@ -190,11 +190,13 @@ end
190
190
 
191
191
  Person.new(name: 'Tom', birthday: '2000-01-01') # works
192
192
  Person.new(name: 5, birthday: '2000-01-01')
193
- #=> !!! ValueSemantics::InvalidValue: Attribute `Person#name` is invalid: 5
193
+ #=> !!! ValueSemantics::InvalidValue: Some attributes of `Person` are invalid:
194
+ #=* - name: 5
194
195
 
195
196
  Person.new(name: 'Tom', birthday: "1970-01-01") # works
196
197
  Person.new(name: 'Tom', birthday: "hello")
197
- #=> !!! ValueSemantics::InvalidValue: Attribute `Person#birthday` is invalid: "hello"
198
+ #=> !!! ValueSemantics::InvalidValue: Some attributes of `Person` are invalid:
199
+ #=* - birthday: "hello"
198
200
  ```
199
201
 
200
202
 
@@ -215,6 +217,9 @@ class LightSwitch
215
217
  # HashOf: validates keys/values of a homogeneous hash
216
218
  toggle_stats HashOf(Symbol => Integer)
217
219
 
220
+ # RangeOf: validates ranges
221
+ levels RangeOf(Integer)
222
+
218
223
  # Either: value must match at least one of a list of validators
219
224
  color Either(Integer, String, nil)
220
225
 
@@ -227,10 +232,11 @@ LightSwitch.new(
227
232
  on?: true,
228
233
  light_ids: [11, 12, 13],
229
234
  toggle_stats: { day: 42, night: 69 },
235
+ levels: (0..10),
230
236
  color: "#FFAABB",
231
237
  wierd_attr: [true, false, true, true],
232
238
  )
233
- #=> #<LightSwitch on?=true light_ids=[11, 12, 13] toggle_stats={:day=>42, :night=>69} color="#FFAABB" wierd_attr=[true, false, true, true]>
239
+ #=> #<LightSwitch on?=true light_ids=[11, 12, 13] toggle_stats={:day=>42, :night=>69} levels=0..10 color="#FFAABB" wierd_attr=[true, false, true, true]>
234
240
  ```
235
241
 
236
242
 
@@ -257,7 +263,8 @@ Server.new(address: '127.0.0.1')
257
263
  #=> #<Server address="127.0.0.1">
258
264
 
259
265
  Server.new(address: '127.0.0.999')
260
- #=> !!! ValueSemantics::InvalidValue: Attribute `Server#address` is invalid: "127.0.0.999"
266
+ #=> !!! ValueSemantics::InvalidValue: Some attributes of `Server` are invalid:
267
+ #=* - address: "127.0.0.999"
261
268
  ```
262
269
 
263
270
  Default attribute values also pass through validation.
@@ -300,7 +307,8 @@ Document.new(path: Pathname.new('~/Documents/whatever.doc'))
300
307
  #=> #<Document path=#<Pathname:~/Documents/whatever.doc>>
301
308
 
302
309
  Document.new(path: 42)
303
- #=> !!! ValueSemantics::InvalidValue: Attribute `Document#path` is invalid: 42
310
+ #=> !!! ValueSemantics::InvalidValue: Some attributes of `Document` are invalid:
311
+ #=* - path: 42
304
312
  ```
305
313
 
306
314
  You can also use any callable object as a coercer.
@@ -350,12 +358,45 @@ For example, the default value could be a string,
350
358
  which would then be coerced into an `Pathname` object.
351
359
 
352
360
 
361
+ ## Built-in Coercers
362
+
363
+ ValueSemantics provides a few built-in coercer objects via the DSL.
364
+
365
+ ```ruby
366
+ class Config
367
+ include ValueSemantics.for_attributes {
368
+ # ArrayCoercer: takes an element coercer
369
+ paths coerce: ArrayCoercer(Pathname.method(:new))
370
+
371
+ # HashCoercer: takes a key and value coercer
372
+ env coerce: HashCoercer(
373
+ keys: :to_sym.to_proc,
374
+ values: :to_i.to_proc,
375
+ )
376
+ }
377
+ end
378
+
379
+ config = Config.new(
380
+ paths: ['/a', '/b'],
381
+ env: { 'AAAA' => '1', 'BBBB' => '2' },
382
+ )
383
+
384
+ config.paths #=> [#<Pathname:/a>, #<Pathname:/b>]
385
+ config.env #=> {:AAAA=>1, :BBBB=>2}
386
+ ```
387
+
388
+
353
389
  ## Nesting
354
390
 
355
391
  It is fairly common to nest value objects inside each other. This
356
- works as expected, but coercion is not automatic. For nested coercion,
357
- use the `.coercer` class method and `ArrayCoercer` DSL method that
358
- ValueSemantics provides.
392
+ works as expected, but coercion is not automatic.
393
+
394
+ For nested coercion, use the `.coercer` class method that
395
+ ValueSemantics provides. It returns a coercer object that accepts
396
+ strings for attribute names, and will ignore attributes that the value
397
+ class does not define, instead of raising an error.
398
+
399
+ This works well in combination with `ArrayCoercer`.
359
400
 
360
401
  ```ruby
361
402
  class CrabClaw
@@ -380,11 +421,12 @@ end
380
421
  ocean = Ocean.new(
381
422
  crabs: [
382
423
  {
383
- left_claw: { size: :small },
384
- right_claw: { size: :small },
424
+ 'left_claw' => { 'size' => :small },
425
+ 'right_claw' => { 'size' => :small },
426
+ voiced_by: 'Samuel E. Wright', # this attr will be ignored
385
427
  }, {
386
- left_claw: { size: :big },
387
- right_claw: { size: :big },
428
+ 'left_claw' => { 'size' => :big },
429
+ 'right_claw' => { 'size' => :big },
388
430
  }
389
431
  ]
390
432
  )
@@ -424,7 +466,7 @@ class Conditional
424
466
  else String
425
467
  }
426
468
  end
427
- #=> !!! SyntaxError: README.md:375: syntax error, unexpected `then'
469
+ #=> !!! SyntaxError: README.md:461: syntax error, unexpected `then'
428
470
  #=* then String
429
471
  #=* ^~~~
430
472
 
@@ -468,7 +510,7 @@ I'm happy to accept PRs that:
468
510
 
469
511
  - Improve error messages for a better developer experience, especially those
470
512
  that support a TDD workflow.
471
- - Add new, helpful validators
513
+ - Add new and helpful built-in validators and coercers
472
514
  - Implement automatic freezing of value objects (must be opt-in)
473
515
 
474
516
  ## License
@@ -1,11 +1,35 @@
1
+ %w(
2
+ anything
3
+ array_coercer
4
+ array_of
5
+ attribute
6
+ bool
7
+ class_methods
8
+ dsl
9
+ either
10
+ hash_of
11
+ hash_coercer
12
+ instance_methods
13
+ range_of
14
+ recipe
15
+ struct
16
+ value_object_coercer
17
+ version
18
+ ).each do |filename|
19
+ require_relative "value_semantics/#{filename}"
20
+ end
21
+
1
22
  module ValueSemantics
2
23
  class Error < StandardError; end
3
24
  class UnrecognizedAttributes < Error; end
4
- class NoDefaultValue < Error; end
5
25
  class MissingAttributes < Error; end
6
26
  class InvalidValue < ArgumentError; end
7
27
 
8
- NOT_SPECIFIED = Object.new.freeze
28
+ # @deprecated Use {Attribute::NOT_SPECIFIED} instead
29
+ NOT_SPECIFIED = Attribute::NOT_SPECIFIED
30
+
31
+ # @deprecated Use {Attribute#optional?} to check if there is a default or not
32
+ class NoDefaultValue < Error; end
9
33
 
10
34
  #
11
35
  # Creates a module via the DSL
@@ -75,495 +99,4 @@ module ValueSemantics
75
99
  private :value_semantics
76
100
  end
77
101
  end
78
-
79
- #
80
- # All the class methods available on ValueSemantics classes
81
- #
82
- # When a ValueSemantics module is included into a class,
83
- # the class is extended by this module.
84
- #
85
- module ClassMethods
86
- #
87
- # @return [Recipe] the recipe used to build the ValueSemantics module that
88
- # was included into this class.
89
- #
90
- def value_semantics
91
- if block_given?
92
- # caller is trying to use the monkey-patched Class method
93
- raise "`#{self}` has already included ValueSemantics"
94
- end
95
-
96
- self::VALUE_SEMANTICS_RECIPE__
97
- end
98
-
99
- #
100
- # A coercer object for the value class
101
- #
102
- # This is mostly useful when nesting value objects inside each other.
103
- #
104
- # The coercer will coerce hashes into an instance of the value class, using
105
- # the hash for attribute values. It will return non-hash values unchanged.
106
- #
107
- # @return [#call] A callable object that can be used as a coercer
108
- #
109
- def coercer
110
- ->(obj) do
111
- if Hash === obj
112
- new(obj)
113
- else
114
- obj
115
- end
116
- end
117
- end
118
- end
119
-
120
- #
121
- # All the instance methods available on ValueSemantics objects
122
- #
123
- module InstanceMethods
124
- #
125
- # Creates a value object based on a hash of attributes
126
- #
127
- # @param attributes [#to_h] A hash of attribute values by name. Typically a
128
- # +Hash+, but can be any object that responds to +#to_h+.
129
- #
130
- # @raise [UnrecognizedAttributes] if given_attrs contains keys that are not
131
- # attributes
132
- # @raise [MissingAttributes] if given_attrs is missing any attributes that
133
- # do not have defaults
134
- # @raise [InvalidValue] if any attribute values do no pass their validators
135
- # @raise [TypeError] if the argument does not respond to +#to_h+
136
- #
137
- def initialize(attributes = nil)
138
- attributes_hash =
139
- if attributes.respond_to?(:to_h)
140
- attributes.to_h
141
- else
142
- raise TypeError, <<-END_MESSAGE.strip.gsub(/\s+/, ' ')
143
- Can not initialize a `#{self.class}` with a `#{attributes.class}`
144
- object. This argument is typically a `Hash` of attributes, but can
145
- be any object that responds to `#to_h`.
146
- END_MESSAGE
147
- end
148
-
149
- remaining_attrs = attributes_hash.dup
150
-
151
- self.class.value_semantics.attributes.each do |attr|
152
- key, value = attr.determine_from!(remaining_attrs, self.class)
153
- instance_variable_set(attr.instance_variable, value)
154
- remaining_attrs.delete(key)
155
- end
156
-
157
- unless remaining_attrs.empty?
158
- raise(
159
- UnrecognizedAttributes,
160
- "`#{self.class}` does not define attributes: " +
161
- remaining_attrs
162
- .keys
163
- .map { |k| '`' + k.inspect + '`' }
164
- .join(', ')
165
- )
166
- end
167
- end
168
-
169
- #
170
- # Returns the value for the given attribute name
171
- #
172
- # @param attr_name [Symbol] The name of the attribute. Can not be a +String+.
173
- # @return The value of the attribute
174
- #
175
- # @raise [UnrecognizedAttributes] if the attribute does not exist
176
- #
177
- def [](attr_name)
178
- attr = self.class.value_semantics.attributes.find do |attr|
179
- attr.name.equal?(attr_name)
180
- end
181
-
182
- if attr
183
- public_send(attr_name)
184
- else
185
- raise UnrecognizedAttributes, "`#{self.class}` has no attribute named `#{attr_name.inspect}`"
186
- end
187
- end
188
-
189
- #
190
- # Creates a copy of this object, with the given attributes changed (non-destructive update)
191
- #
192
- # @param new_attrs [Hash] the attributes to change
193
- # @return A new object, with the attribute changes applied
194
- #
195
- def with(new_attrs)
196
- self.class.new(to_h.merge(new_attrs))
197
- end
198
-
199
- #
200
- # @return [Hash] all of the attributes
201
- #
202
- def to_h
203
- self.class.value_semantics.attributes
204
- .map { |attr| [attr.name, public_send(attr.name)] }
205
- .to_h
206
- end
207
-
208
- #
209
- # Loose equality
210
- #
211
- # @return [Boolean] whether all attributes are equal, and the object
212
- # classes are ancestors of eachother in any way
213
- #
214
- def ==(other)
215
- (other.is_a?(self.class) || is_a?(other.class)) && other.to_h.eql?(to_h)
216
- end
217
-
218
- #
219
- # Strict equality
220
- #
221
- # @return [Boolean] whether all attribuets are equal, and both objects
222
- # has the exact same class
223
- #
224
- def eql?(other)
225
- other.class.equal?(self.class) && other.to_h.eql?(to_h)
226
- end
227
-
228
- #
229
- # Unique-ish integer, based on attributes and class of the object
230
- #
231
- def hash
232
- to_h.hash ^ self.class.hash
233
- end
234
-
235
- def inspect
236
- attrs = to_h
237
- .map { |key, value| "#{key}=#{value.inspect}" }
238
- .join(" ")
239
-
240
- "#<#{self.class} #{attrs}>"
241
- end
242
-
243
- def pretty_print(pp)
244
- pp.object_group(self) do
245
- to_h.each do |attr, value|
246
- pp.breakable
247
- pp.text("#{attr}=")
248
- pp.pp(value)
249
- end
250
- end
251
- end
252
-
253
- def deconstruct_keys(_)
254
- to_h
255
- end
256
- end
257
-
258
- #
259
- # Represents a single attribute of a value class
260
- #
261
- class Attribute
262
- NO_DEFAULT_GENERATOR = lambda do
263
- raise NoDefaultValue, "Attribute does not have a default value"
264
- end
265
-
266
- attr_reader :name, :validator, :coercer, :default_generator
267
-
268
- def initialize(name:,
269
- default_generator: NO_DEFAULT_GENERATOR,
270
- validator: Anything,
271
- coercer: nil)
272
- @name = name.to_sym
273
- @default_generator = default_generator
274
- @validator = validator
275
- @coercer = coercer
276
- freeze
277
- end
278
-
279
- def self.define(name,
280
- validator=Anything,
281
- default: NOT_SPECIFIED,
282
- default_generator: nil,
283
- coerce: nil)
284
- generator = begin
285
- if default_generator && !default.equal?(NOT_SPECIFIED)
286
- raise ArgumentError, "Attribute `#{name}` can not have both a `:default` and a `:default_generator`"
287
- elsif default_generator
288
- default_generator
289
- elsif !default.equal?(NOT_SPECIFIED)
290
- ->{ default }
291
- else
292
- NO_DEFAULT_GENERATOR
293
- end
294
- end
295
-
296
- new(
297
- name: name,
298
- validator: validator,
299
- default_generator: generator,
300
- coercer: coerce,
301
- )
302
- end
303
-
304
- def determine_from!(attr_hash, klass)
305
- raw_value = attr_hash.fetch(name) do
306
- if default_generator.equal?(NO_DEFAULT_GENERATOR)
307
- raise MissingAttributes, "Attribute `#{klass}\##{name}` has no value"
308
- else
309
- default_generator.call
310
- end
311
- end
312
-
313
- coerced_value = coerce(raw_value, klass)
314
-
315
- if validate?(coerced_value)
316
- [name, coerced_value]
317
- else
318
- raise InvalidValue, "Attribute `#{klass}\##{name}` is invalid: #{coerced_value.inspect}"
319
- end
320
- end
321
-
322
- def coerce(attr_value, klass)
323
- return attr_value unless coercer # coercion not enabled
324
-
325
- if coercer.equal?(true)
326
- klass.public_send(coercion_method, attr_value)
327
- else
328
- coercer.call(attr_value)
329
- end
330
- end
331
-
332
- def validate?(value)
333
- validator === value
334
- end
335
-
336
- def instance_variable
337
- '@' + name.to_s.chomp('!').chomp('?')
338
- end
339
-
340
- def coercion_method
341
- "coerce_#{name}"
342
- end
343
- end
344
-
345
- #
346
- # Contains all the configuration necessary to bake a ValueSemantics module
347
- #
348
- # @see ValueSemantics.bake_module
349
- # @see ClassMethods#value_semantics
350
- # @see DSL.run
351
- #
352
- class Recipe
353
- attr_reader :attributes
354
-
355
- def initialize(attributes:)
356
- @attributes = attributes
357
- freeze
358
- end
359
- end
360
-
361
- #
362
- # Builds a {Recipe} via DSL methods
363
- #
364
- # DSL blocks are <code>instance_eval</code>d against an object of this class.
365
- #
366
- # @see Recipe
367
- # @see ValueSemantics.for_attributes
368
- #
369
- class DSL
370
- #
371
- # Builds a {Recipe} from a DSL block
372
- #
373
- # @yield to the block containing the DSL
374
- # @return [Recipe]
375
- def self.run(&block)
376
- dsl = new
377
- dsl.instance_eval(&block)
378
- Recipe.new(attributes: dsl.__attributes.freeze)
379
- end
380
-
381
- attr_reader :__attributes
382
-
383
- def initialize
384
- @__attributes = []
385
- end
386
-
387
- def Bool
388
- Bool
389
- end
390
-
391
- def Either(*subvalidators)
392
- Either.new(subvalidators)
393
- end
394
-
395
- def Anything
396
- Anything
397
- end
398
-
399
- def ArrayOf(element_validator)
400
- ArrayOf.new(element_validator)
401
- end
402
-
403
- def HashOf(key_validator_to_value_validator)
404
- unless key_validator_to_value_validator.size.equal?(1)
405
- raise ArgumentError, "HashOf() takes a hash with one key and one value"
406
- end
407
-
408
- HashOf.new(
409
- key_validator_to_value_validator.keys.first,
410
- key_validator_to_value_validator.values.first,
411
- )
412
- end
413
-
414
- def ArrayCoercer(element_coercer)
415
- ArrayCoercer.new(element_coercer)
416
- end
417
-
418
- #
419
- # Defines one attribute.
420
- #
421
- # This is the method that gets called under the hood, when defining
422
- # attributes the typical +#method_missing+ way.
423
- #
424
- # You can use this method directly if your attribute name results in invalid
425
- # Ruby syntax. For example, if you want an attribute named +then+, you
426
- # can do:
427
- #
428
- # include ValueSemantics.for_attributes {
429
- # # Does not work:
430
- # then String, default: "whatever"
431
- # #=> SyntaxError: syntax error, unexpected `then'
432
- #
433
- # # Works:
434
- # def_attr :then, String, default: "whatever"
435
- # }
436
- #
437
- #
438
- def def_attr(*args, **kwargs)
439
- __attributes << Attribute.define(*args, **kwargs)
440
- end
441
-
442
- def method_missing(name, *args, **kwargs)
443
- if respond_to_missing?(name)
444
- def_attr(name, *args, **kwargs)
445
- else
446
- super
447
- end
448
- end
449
-
450
- def respond_to_missing?(method_name, _include_private=nil)
451
- first_letter = method_name.to_s.each_char.first
452
- first_letter.eql?(first_letter.downcase)
453
- end
454
- end
455
-
456
- #
457
- # Validator that only matches `true` and `false`
458
- #
459
- module Bool
460
- # @return [Boolean]
461
- def self.===(value)
462
- true.equal?(value) || false.equal?(value)
463
- end
464
- end
465
-
466
- #
467
- # Validator that matches any and all values
468
- #
469
- module Anything
470
- # @return [true]
471
- def self.===(_)
472
- true
473
- end
474
- end
475
-
476
- #
477
- # Validator that matches if any of the given subvalidators matches
478
- #
479
- class Either
480
- attr_reader :subvalidators
481
-
482
- def initialize(subvalidators)
483
- @subvalidators = subvalidators
484
- freeze
485
- end
486
-
487
- # @return [Boolean]
488
- def ===(value)
489
- subvalidators.any? { |sv| sv === value }
490
- end
491
- end
492
-
493
- #
494
- # Validator that matches arrays if each element matches a given subvalidator
495
- #
496
- class ArrayOf
497
- attr_reader :element_validator
498
-
499
- def initialize(element_validator)
500
- @element_validator = element_validator
501
- freeze
502
- end
503
-
504
- # @return [Boolean]
505
- def ===(value)
506
- Array === value && value.all? { |element| element_validator === element }
507
- end
508
- end
509
-
510
- #
511
- # Validator that matches +Hash+es with homogeneous keys and values
512
- #
513
- class HashOf
514
- attr_reader :key_validator, :value_validator
515
-
516
- def initialize(key_validator, value_validator)
517
- @key_validator, @value_validator = key_validator, value_validator
518
- freeze
519
- end
520
-
521
- # @return [Boolean]
522
- def ===(value)
523
- Hash === value && value.all? do |key, value|
524
- key_validator === key && value_validator === value
525
- end
526
- end
527
- end
528
-
529
- class ArrayCoercer
530
- attr_reader :element_coercer
531
-
532
- def initialize(element_coercer = nil)
533
- @element_coercer = element_coercer
534
- freeze
535
- end
536
-
537
- def call(obj)
538
- if obj.respond_to?(:to_a)
539
- array = obj.to_a
540
- if element_coercer
541
- array.map { |element| element_coercer.call(element) }
542
- else
543
- array
544
- end
545
- else
546
- obj
547
- end
548
- end
549
- end
550
-
551
- #
552
- # ValueSemantics equivalent of the Struct class from the Ruby standard
553
- # library
554
- #
555
- class Struct
556
- #
557
- # Creates a new Class with ValueSemantics mixed in
558
- #
559
- # @yield a block containing ValueSemantics DSL
560
- # @return [Class] the newly created class
561
- #
562
- def self.new(&block)
563
- klass = Class.new
564
- klass.include(ValueSemantics.for_attributes(&block))
565
- klass
566
- end
567
- end
568
-
569
102
  end