value_semantics 3.2.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.
@@ -1,10 +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
26
+ class InvalidValue < ArgumentError; end
27
+
28
+ # @deprecated Use {Attribute::NOT_SPECIFIED} instead
29
+ NOT_SPECIFIED = Attribute::NOT_SPECIFIED
6
30
 
7
- NOT_SPECIFIED = Object.new.freeze
31
+ # @deprecated Use {Attribute#optional?} to check if there is a default or not
32
+ class NoDefaultValue < Error; end
8
33
 
9
34
  #
10
35
  # Creates a module via the DSL
@@ -44,344 +69,34 @@ module ValueSemantics
44
69
  end
45
70
 
46
71
  #
47
- # All the class methods available on ValueSemantics classes
72
+ # Makes the +.value_semantics+ convenience method available to all classes
48
73
  #
49
- # When a ValueSemantics module is included into a class,
50
- # the class is extended by this module.
74
+ # +.value_semantics+ is a shortcut for {.for_attributes}. Instead of:
51
75
  #
52
- module ClassMethods
53
- #
54
- # @return [Recipe] the recipe used to build the ValueSemantics module that
55
- # was included into this class.
56
- #
57
- def value_semantics
58
- self::VALUE_SEMANTICS_RECIPE__
59
- end
60
- end
61
-
76
+ # class Person
77
+ # include ValueSemantics.for_attributes {
78
+ # name String
79
+ # }
80
+ # end
62
81
  #
63
- # All the instance methods available on ValueSemantics objects
82
+ # You can just write:
64
83
  #
65
- module InstanceMethods
66
- #
67
- # Creates a value object based on a Hash of attributes
68
- #
69
- # @param given_attrs [Hash] a hash of attributes, with symbols for keys
70
- # @raise [UnrecognizedAttributes] if given_attrs contains keys that are not attributes
71
- # @raise [MissingAttributes] if given_attrs is missing any attributes that do not have defaults
72
- # @raise [ArgumentError] if any attribute values do no pass their validators
73
- #
74
- def initialize(given_attrs = {})
75
- remaining_attrs = given_attrs.dup
76
-
77
- self.class.value_semantics.attributes.each do |attr|
78
- key, value = attr.determine_from!(remaining_attrs, self.class)
79
- instance_variable_set(attr.instance_variable, value)
80
- remaining_attrs.delete(key)
81
- end
82
-
83
- unless remaining_attrs.empty?
84
- unrecognised = remaining_attrs.keys.map(&:inspect).join(', ')
85
- raise UnrecognizedAttributes, "Unrecognized attributes: #{unrecognised}"
86
- end
87
- end
88
-
89
- #
90
- # Creates a copy of this object, with the given attributes changed (non-destructive update)
91
- #
92
- # @param new_attrs [Hash] the attributes to change
93
- # @return A new object, with the attribute changes applied
94
- #
95
- def with(new_attrs)
96
- self.class.new(to_h.merge(new_attrs))
97
- end
98
-
99
- #
100
- # @return [Hash] all of the attributes
101
- #
102
- def to_h
103
- self.class.value_semantics.attributes
104
- .map { |attr| [attr.name, public_send(attr.name)] }
105
- .to_h
106
- end
107
-
108
- #
109
- # Loose equality
110
- #
111
- # @return [Boolean] whether all attributes are equal, and the object
112
- # classes are ancestors of eachother in any way
113
- #
114
- def ==(other)
115
- (other.is_a?(self.class) || is_a?(other.class)) && other.to_h.eql?(to_h)
116
- end
117
-
118
- #
119
- # Strict equality
120
- #
121
- # @return [Boolean] whether all attribuets are equal, and both objects
122
- # has the exact same class
123
- #
124
- def eql?(other)
125
- other.class.equal?(self.class) && other.to_h.eql?(to_h)
126
- end
127
-
128
- #
129
- # Unique-ish integer, based on attributes and class of the object
130
- #
131
- def hash
132
- to_h.hash ^ self.class.hash
133
- end
134
-
135
- def inspect
136
- attrs = to_h
137
- .map { |key, value| "#{key}=#{value.inspect}" }
138
- .join(" ")
139
-
140
- "#<#{self.class} #{attrs}>"
141
- end
142
-
143
- def pretty_print(pp)
144
- pp.object_group(self) do
145
- to_h.each do |attr, value|
146
- pp.breakable
147
- pp.text("#{attr}=")
148
- pp.pp(value)
149
- end
150
- end
151
- end
152
- end
153
-
84
+ # class Person
85
+ # value_semantics do
86
+ # name String
87
+ # end
88
+ # end
154
89
  #
155
- # Represents a single attribute of a value class
90
+ # Alternatively, you can +require 'value_semantics/monkey_patched'+, which
91
+ # will call this method automatically.
156
92
  #
157
- class Attribute
158
- NO_DEFAULT_GENERATOR = lambda do
159
- raise NoDefaultValue, "Attribute does not have a default value"
160
- end
161
-
162
- attr_reader :name, :validator, :coercer, :default_generator
163
-
164
- def initialize(name:,
165
- default_generator: NO_DEFAULT_GENERATOR,
166
- validator: Anything,
167
- coercer: nil)
168
- @name = name.to_sym
169
- @default_generator = default_generator
170
- @validator = validator
171
- @coercer = coercer
172
- freeze
173
- end
174
-
175
- def self.define(name,
176
- validator=Anything,
177
- default: NOT_SPECIFIED,
178
- default_generator: nil,
179
- coerce: nil)
180
- generator = begin
181
- if default_generator && !default.equal?(NOT_SPECIFIED)
182
- raise ArgumentError, "Attribute '#{name}' can not have both a :default and a :default_generator"
183
- elsif default_generator
184
- default_generator
185
- elsif !default.equal?(NOT_SPECIFIED)
186
- ->{ default }
187
- else
188
- NO_DEFAULT_GENERATOR
189
- end
93
+ def self.monkey_patch!
94
+ Class.class_eval do
95
+ # @!visibility private
96
+ def value_semantics(&block)
97
+ include ValueSemantics.for_attributes(&block)
190
98
  end
191
-
192
- new(
193
- name: name,
194
- validator: validator,
195
- default_generator: generator,
196
- coercer: coerce,
197
- )
198
- end
199
-
200
- def determine_from!(attr_hash, klass)
201
- raw_value = attr_hash.fetch(name) do
202
- if default_generator.equal?(NO_DEFAULT_GENERATOR)
203
- raise MissingAttributes, "Value missing for attribute '#{name}'"
204
- else
205
- default_generator.call
206
- end
207
- end
208
-
209
- coerced_value = coerce(raw_value, klass)
210
-
211
- if validate?(coerced_value)
212
- [name, coerced_value]
213
- else
214
- raise ArgumentError, "Value for attribute '#{name}' is not valid: #{coerced_value.inspect}"
215
- end
216
- end
217
-
218
- def coerce(attr_value, klass)
219
- return attr_value unless coercer # coercion not enabled
220
-
221
- if coercer.equal?(true)
222
- klass.public_send(coercion_method, attr_value)
223
- else
224
- coercer.call(attr_value)
225
- end
226
- end
227
-
228
- def validate?(value)
229
- validator === value
230
- end
231
-
232
- def instance_variable
233
- '@' + name.to_s.chomp('!').chomp('?')
234
- end
235
-
236
- def coercion_method
237
- "coerce_#{name}"
99
+ private :value_semantics
238
100
  end
239
101
  end
240
-
241
- #
242
- # Contains all the configuration necessary to bake a ValueSemantics module
243
- #
244
- # @see ValueSemantics.bake_module
245
- #
246
- class Recipe
247
- attr_reader :attributes
248
-
249
- def initialize(attributes:)
250
- @attributes = attributes
251
- freeze
252
- end
253
- end
254
-
255
- #
256
- # Builds a {Recipe} via DSL methods
257
- #
258
- # DSL blocks are <code>instance_eval</code>d against an object of this class.
259
- #
260
- # @see Recipe
261
- # @see ValueSemantics.for_attributes
262
- #
263
- class DSL
264
- #
265
- # Builds a {Recipe} from a DSL block
266
- #
267
- # @yield to the block containing the DSL
268
- # @return [Recipe]
269
- def self.run(&block)
270
- dsl = new
271
- dsl.instance_eval(&block)
272
- Recipe.new(attributes: dsl.__attributes.freeze)
273
- end
274
-
275
- attr_reader :__attributes
276
-
277
- def initialize
278
- @__attributes = []
279
- end
280
-
281
- def Bool
282
- Bool
283
- end
284
-
285
- def Either(*subvalidators)
286
- Either.new(subvalidators)
287
- end
288
-
289
- def Anything
290
- Anything
291
- end
292
-
293
- def ArrayOf(element_validator)
294
- ArrayOf.new(element_validator)
295
- end
296
-
297
- def def_attr(*args)
298
- __attributes << Attribute.define(*args)
299
- end
300
-
301
- def method_missing(name, *args)
302
- if respond_to_missing?(name)
303
- def_attr(name, *args)
304
- else
305
- super
306
- end
307
- end
308
-
309
- def respond_to_missing?(method_name, _include_private=nil)
310
- first_letter = method_name.to_s.each_char.first
311
- first_letter.eql?(first_letter.downcase)
312
- end
313
- end
314
-
315
- #
316
- # Validator that only matches `true` and `false`
317
- #
318
- module Bool
319
- # @return [Boolean]
320
- def self.===(value)
321
- true.equal?(value) || false.equal?(value)
322
- end
323
- end
324
-
325
- #
326
- # Validator that matches any and all values
327
- #
328
- module Anything
329
- # @return [true]
330
- def self.===(_)
331
- true
332
- end
333
- end
334
-
335
- #
336
- # Validator that matches if any of the given subvalidators matches
337
- #
338
- class Either
339
- attr_reader :subvalidators
340
-
341
- def initialize(subvalidators)
342
- @subvalidators = subvalidators
343
- freeze
344
- end
345
-
346
- # @return [Boolean]
347
- def ===(value)
348
- subvalidators.any? { |sv| sv === value }
349
- end
350
- end
351
-
352
- #
353
- # Validator that matches arrays if each element matches a given subvalidator
354
- #
355
- class ArrayOf
356
- attr_reader :element_validator
357
-
358
- def initialize(element_validator)
359
- @element_validator = element_validator
360
- freeze
361
- end
362
-
363
- # @return [Boolean]
364
- def ===(value)
365
- Array === value && value.all? { |element| element_validator === element }
366
- end
367
- end
368
-
369
- #
370
- # ValueSemantics equivalent of the Struct class from the Ruby standard
371
- # library
372
- #
373
- class Struct
374
- #
375
- # Creates a new Class with ValueSemantics mixed in
376
- #
377
- # @yield a block containing ValueSemantics DSL
378
- # @return [Class] the newly created class
379
- #
380
- def self.new(&block)
381
- klass = Class.new
382
- klass.include(ValueSemantics.for_attributes(&block))
383
- klass
384
- end
385
- end
386
-
387
102
  end
@@ -0,0 +1,11 @@
1
+ module ValueSemantics
2
+ #
3
+ # Validator that matches any and all values
4
+ #
5
+ module Anything
6
+ # @return [true]
7
+ def self.===(_)
8
+ true
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ module ValueSemantics
2
+ class ArrayCoercer
3
+ attr_reader :element_coercer
4
+
5
+ def initialize(element_coercer = nil)
6
+ @element_coercer = element_coercer
7
+ freeze
8
+ end
9
+
10
+ def call(obj)
11
+ if obj.respond_to?(:to_a)
12
+ array = obj.to_a
13
+ if element_coercer
14
+ array.map { |element| element_coercer.call(element) }
15
+ else
16
+ array
17
+ end
18
+ else
19
+ obj
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ module ValueSemantics
2
+ #
3
+ # Validator that matches arrays if each element matches a given subvalidator
4
+ #
5
+ class ArrayOf
6
+ attr_reader :element_validator
7
+
8
+ def initialize(element_validator)
9
+ @element_validator = element_validator
10
+ freeze
11
+ end
12
+
13
+ # @return [Boolean]
14
+ def ===(value)
15
+ Array === value && value.all? { |element| element_validator === element }
16
+ end
17
+ end
18
+ end