value_semantics 2.1.0 → 3.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +17 -5
- data/CHANGELOG.md +24 -0
- data/Gemfile +3 -1
- data/README.md +31 -10
- data/bin/test +15 -0
- data/lib/value_semantics.rb +201 -56
- data/lib/value_semantics/version.rb +1 -1
- data/value_semantics.gemspec +7 -8
- metadata +28 -13
- data/bin/mutation_test.sh +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f122fa566277c213c7eb2a31b38af94d4287b1688d91da91bd5eea8044a041ba
|
4
|
+
data.tar.gz: 371bc15f0cc7449042b19d5064e4a28f76ec1d6c0aae2c64622adf3252621045
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7ebe03be2de318e43027cc4d27ed737111654a5d74c0646f62691076e0f51938eb55966a3f04105f4a4c86d242a0134ca3f5fafa27089aedd4c51f2fb45abd9
|
7
|
+
data.tar.gz: ac4e7a9f938e0bd9193d4260a8c211db6a05554463b6312a6409499a8d22c7a34b281093cf2c487676fefd58915b21027a47c99a75619fb2b80c457672ced57e
|
data/.travis.yml
CHANGED
@@ -1,13 +1,25 @@
|
|
1
1
|
language: ruby
|
2
|
+
script: bin/test
|
3
|
+
|
4
|
+
# test old Ruby versions WITHOUT mutation testing
|
2
5
|
rvm:
|
3
|
-
- 2.3.
|
4
|
-
- 2.4.
|
5
|
-
- 2.5.
|
6
|
-
|
6
|
+
- 2.3.8
|
7
|
+
- 2.4.10
|
8
|
+
- 2.5.8
|
9
|
+
- 2.6.6
|
10
|
+
env: MUTATION_TEST=false
|
11
|
+
|
12
|
+
# test the latest Ruby version WITH mutation testing
|
13
|
+
matrix:
|
14
|
+
include:
|
15
|
+
- rvm: 2.7.1
|
16
|
+
env: MUTATION_TEST=true
|
17
|
+
|
18
|
+
# deploy gem on tagged commits, on the latest Ruby version only
|
7
19
|
deploy:
|
8
20
|
provider: rubygems
|
9
21
|
on:
|
10
22
|
tags: true
|
11
|
-
|
23
|
+
env: MUTATION_TEST=true
|
12
24
|
api_key:
|
13
25
|
secure: nL74QuUczEpA0qbhSBN2zjGdviWgKB3wR6vFvwervv1MZNWmwOQUYe99Oq9kPeyc8/x2MR/H6PQm5qbrk/WAfRede01WxlZ/EBUW+9CYGrxcBsGONx9IULO8A0I8/yN/YJHW2vjo3dfR66EwVsXTVWq8U63PRRcwJIyTqnIiUm2sxauMQoPRBbXG+pD9v/EJSn3ugpdtxp0lVYDn8LDKk5Ho4/wbpY4ML11XUJa9mz9CyR/GsAzdy5FTXaDMOwuWOVEx9cab7m4qPOBhmlJY4TrmooFpxTxRwChcvByjq1IboEd2M3RT5on7Q/xDTlHSOuT0OS8mnS2AocGT4a1gC+W/xOlghgEcN+xs2V5mfucR6+iUYlCy32uz1w3ey7T2X5xN4ubut09r1xLi7eu1NisAoAc+GOJ4TIxQNqkeRhY4X/fs8j7SMfOEMDr6pPxSLKZxgSvExt+IbdcZD/uQ7rTBQkadYCbc9MX5dHazBievmar3ZsFffbIf+n13FVDXsaPgRt7DlFM5dqGrEwVwt1jFRhdFuDCjkj4QWOLn7E1uY3XqgrqGvgUBlF8Znwc6qicW8zxV4SIWhqIzCOH6L9WIZGLHNq0remoCd9sq9Ter9av3jL+6UmZRRAr+JceeZfZmsYIXKomECzleM9FXMx7FXlpjJKOlf3JnrfeCTwI=
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
Notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
|
9
|
+
## [3.2.1] - 2020-07-11
|
10
|
+
### Fixed
|
11
|
+
- Fix warnings new to Ruby 2.7 about keyword arguments
|
12
|
+
|
13
|
+
## [3.2.0] - 2019-09-30
|
14
|
+
### Added
|
15
|
+
- `ValueSemantics::Struct`, a convenience for creating a new class and mixing
|
16
|
+
in ValueSemantics in a single step.
|
17
|
+
|
18
|
+
## [3.1.0] - 2019-06-30
|
19
|
+
### Added
|
20
|
+
- Built-in PP support for value classes
|
21
|
+
|
22
|
+
## [3.0.0] - 2019-01-27
|
23
|
+
|
24
|
+
First public release
|
data/Gemfile
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
|
3
|
-
|
3
|
+
source 'https://oss:fLUos7k6c7Ak7zjhwbYphPJbwBk1Uuew@gem.mutant.dev' do
|
4
|
+
gem 'mutant-license'
|
5
|
+
end
|
4
6
|
|
5
7
|
# Specify your gem's dependencies in the gemspec
|
6
8
|
gemspec
|
data/README.md
CHANGED
@@ -5,15 +5,20 @@
|
|
5
5
|
ValueSemantics
|
6
6
|
==============
|
7
7
|
|
8
|
-
|
8
|
+
A gem for making value classes.
|
9
9
|
|
10
|
-
Generates modules that provide value semantics for a given set of attributes.
|
11
|
-
|
12
|
-
|
10
|
+
Generates modules that provide [conventional value semantics](https://github.com/zverok/good-value-object) for a given set of attributes.
|
11
|
+
The behaviour is similar to an immutable `Struct` class,
|
12
|
+
plus extensible, lightweight validation and coercion.
|
13
13
|
|
14
14
|
These are intended for internal use, as opposed to validating user input like ActiveRecord.
|
15
|
-
Invalid or missing attributes cause an exception
|
16
|
-
not an error message intended for
|
15
|
+
Invalid or missing attributes cause an exception for developers,
|
16
|
+
not an error message intended for application users.
|
17
|
+
|
18
|
+
See the [announcement blog post][] for some of the rationale behind the gem, and some [discussion on Reddit].
|
19
|
+
|
20
|
+
[announcement blog post]: https://www.rubypigeon.com/posts/value-semantics-gem-for-making-value-classes/
|
21
|
+
[discussion on Reddit]: https://www.reddit.com/r/ruby/comments/akz4fs/valuesemanticsa_gem_for_making_value_classes/
|
17
22
|
|
18
23
|
|
19
24
|
Defining and Creating Value Objects
|
@@ -103,7 +108,7 @@ Defaults
|
|
103
108
|
--------
|
104
109
|
|
105
110
|
Defaults can be specified in one of two ways:
|
106
|
-
the `:default` option, or the
|
111
|
+
the `:default` option, or the `:default_generator` option.
|
107
112
|
|
108
113
|
```ruby
|
109
114
|
class Cat
|
@@ -164,8 +169,8 @@ for common situations:
|
|
164
169
|
class LightSwitch
|
165
170
|
include ValueSemantics.for_attributes {
|
166
171
|
|
167
|
-
#
|
168
|
-
on?
|
172
|
+
# Bool: only allows `true` or `false`
|
173
|
+
on? Bool()
|
169
174
|
|
170
175
|
# ArrayOf: validates elements in an array
|
171
176
|
light_ids ArrayOf(Integer)
|
@@ -174,7 +179,7 @@ class LightSwitch
|
|
174
179
|
color Either(Integer, String, nil)
|
175
180
|
|
176
181
|
# these validators are composable
|
177
|
-
wierd_attr Either(
|
182
|
+
wierd_attr Either(Bool(), ArrayOf(Bool()))
|
178
183
|
}
|
179
184
|
end
|
180
185
|
|
@@ -299,6 +304,21 @@ For example, the default value could be a string,
|
|
299
304
|
which would then be coerced into an `IPAddr` object.
|
300
305
|
|
301
306
|
|
307
|
+
## ValueSemantics::Struct
|
308
|
+
|
309
|
+
This is a convenience for making a new class and including ValueSemantics in
|
310
|
+
one step, similar to how `Struct` works from the Ruby standard library. For
|
311
|
+
example:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
Cat = ValueSemantics::Struct.new do
|
315
|
+
name String, default: "Mittens"
|
316
|
+
end
|
317
|
+
|
318
|
+
Cat.new.name #=> "Mittens"
|
319
|
+
```
|
320
|
+
|
321
|
+
|
302
322
|
## Installation
|
303
323
|
|
304
324
|
Add this line to your application's Gemfile:
|
@@ -321,6 +341,7 @@ Or install it yourself as:
|
|
321
341
|
Bug reports and pull requests are welcome on GitHub at:
|
322
342
|
https://github.com/tomdalling/value_semantics
|
323
343
|
|
344
|
+
Keep in mind that this gem aims to be as close to 100% backwards compatible as possible.
|
324
345
|
|
325
346
|
## License
|
326
347
|
|
data/bin/test
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -ue
|
3
|
+
|
4
|
+
MUTANT_PATTERN=${1:-ValueSemantics*}
|
5
|
+
|
6
|
+
# if $MUTATION_TEST is false, just run RSpec
|
7
|
+
if [[ "${MUTATION_TEST:-true}" == "false" ]] ; then
|
8
|
+
bundle exec rspec
|
9
|
+
else
|
10
|
+
bundle exec mutant \
|
11
|
+
--include lib \
|
12
|
+
--require value_semantics \
|
13
|
+
--use rspec "$MUTANT_PATTERN" \
|
14
|
+
--ignore-subject "ValueSemantics::Struct.new" # causes unfixable failure I don't want to deal with
|
15
|
+
fi
|
data/lib/value_semantics.rb
CHANGED
@@ -1,39 +1,80 @@
|
|
1
1
|
module ValueSemantics
|
2
|
+
class Error < StandardError; end
|
3
|
+
class UnrecognizedAttributes < Error; end
|
4
|
+
class NoDefaultValue < Error; end
|
5
|
+
class MissingAttributes < Error; end
|
6
|
+
|
2
7
|
NOT_SPECIFIED = Object.new.freeze
|
3
8
|
|
9
|
+
#
|
10
|
+
# Creates a module via the DSL
|
11
|
+
#
|
12
|
+
# @yield The block containing the DSL
|
13
|
+
# @return [Module]
|
14
|
+
#
|
15
|
+
# @see DSL
|
16
|
+
# @see InstanceMethods
|
17
|
+
#
|
4
18
|
def self.for_attributes(&block)
|
5
|
-
|
6
|
-
|
19
|
+
recipe = DSL.run(&block)
|
20
|
+
bake_module(recipe)
|
7
21
|
end
|
8
22
|
|
9
|
-
|
23
|
+
#
|
24
|
+
# Creates a module from a {Recipe}
|
25
|
+
#
|
26
|
+
# @param recipe [Recipe]
|
27
|
+
# @return [Module]
|
28
|
+
#
|
29
|
+
def self.bake_module(recipe)
|
10
30
|
Module.new do
|
11
|
-
|
12
|
-
include(
|
31
|
+
const_set(:VALUE_SEMANTICS_RECIPE__, recipe)
|
32
|
+
include(InstanceMethods)
|
13
33
|
|
14
34
|
# define the attr readers
|
15
|
-
attributes.each do |attr|
|
35
|
+
recipe.attributes.each do |attr|
|
16
36
|
module_eval("def #{attr.name}; #{attr.instance_variable}; end")
|
17
37
|
end
|
18
38
|
|
19
|
-
# define BaseClass.attributes class method
|
20
|
-
const_set(:ATTRIBUTES__, attributes)
|
21
39
|
def self.included(base)
|
22
|
-
base.const_set(:
|
23
|
-
|
24
|
-
def attributes
|
25
|
-
self::ATTRIBUTES__
|
26
|
-
end
|
27
|
-
end
|
40
|
+
base.const_set(:ValueSemantics_Attributes, self)
|
41
|
+
base.extend(ClassMethods)
|
28
42
|
end
|
29
43
|
end
|
30
44
|
end
|
31
45
|
|
32
|
-
|
46
|
+
#
|
47
|
+
# All the class methods available on ValueSemantics classes
|
48
|
+
#
|
49
|
+
# When a ValueSemantics module is included into a class,
|
50
|
+
# the class is extended by this module.
|
51
|
+
#
|
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
|
+
|
62
|
+
#
|
63
|
+
# All the instance methods available on ValueSemantics objects
|
64
|
+
#
|
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
|
+
#
|
33
74
|
def initialize(given_attrs = {})
|
34
75
|
remaining_attrs = given_attrs.dup
|
35
76
|
|
36
|
-
self.class.attributes.each do |attr|
|
77
|
+
self.class.value_semantics.attributes.each do |attr|
|
37
78
|
key, value = attr.determine_from!(remaining_attrs, self.class)
|
38
79
|
instance_variable_set(attr.instance_variable, value)
|
39
80
|
remaining_attrs.delete(key)
|
@@ -41,30 +82,54 @@ module ValueSemantics
|
|
41
82
|
|
42
83
|
unless remaining_attrs.empty?
|
43
84
|
unrecognised = remaining_attrs.keys.map(&:inspect).join(', ')
|
44
|
-
raise
|
85
|
+
raise UnrecognizedAttributes, "Unrecognized attributes: #{unrecognised}"
|
45
86
|
end
|
46
87
|
end
|
47
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
|
+
#
|
48
95
|
def with(new_attrs)
|
49
96
|
self.class.new(to_h.merge(new_attrs))
|
50
97
|
end
|
51
98
|
|
99
|
+
#
|
100
|
+
# @return [Hash] all of the attributes
|
101
|
+
#
|
52
102
|
def to_h
|
53
|
-
self.class.attributes
|
103
|
+
self.class.value_semantics.attributes
|
54
104
|
.map { |attr| [attr.name, public_send(attr.name)] }
|
55
105
|
.to_h
|
56
106
|
end
|
57
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
|
+
#
|
58
114
|
def ==(other)
|
59
115
|
(other.is_a?(self.class) || is_a?(other.class)) && other.to_h.eql?(to_h)
|
60
116
|
end
|
61
117
|
|
118
|
+
#
|
119
|
+
# Strict equality
|
120
|
+
#
|
121
|
+
# @return [Boolean] whether all attribuets are equal, and both objects
|
122
|
+
# has the exact same class
|
123
|
+
#
|
62
124
|
def eql?(other)
|
63
125
|
other.class.equal?(self.class) && other.to_h.eql?(to_h)
|
64
126
|
end
|
65
127
|
|
128
|
+
#
|
129
|
+
# Unique-ish integer, based on attributes and class of the object
|
130
|
+
#
|
66
131
|
def hash
|
67
|
-
|
132
|
+
to_h.hash ^ self.class.hash
|
68
133
|
end
|
69
134
|
|
70
135
|
def inspect
|
@@ -74,14 +139,32 @@ module ValueSemantics
|
|
74
139
|
|
75
140
|
"#<#{self.class} #{attrs}>"
|
76
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
|
77
152
|
end
|
78
153
|
|
154
|
+
#
|
155
|
+
# Represents a single attribute of a value class
|
156
|
+
#
|
79
157
|
class Attribute
|
80
|
-
NO_DEFAULT_GENERATOR =
|
158
|
+
NO_DEFAULT_GENERATOR = lambda do
|
159
|
+
raise NoDefaultValue, "Attribute does not have a default value"
|
160
|
+
end
|
81
161
|
|
82
162
|
attr_reader :name, :validator, :coercer, :default_generator
|
83
163
|
|
84
|
-
def initialize(name:,
|
164
|
+
def initialize(name:,
|
165
|
+
default_generator: NO_DEFAULT_GENERATOR,
|
166
|
+
validator: Anything,
|
167
|
+
coercer: nil)
|
85
168
|
@name = name.to_sym
|
86
169
|
@default_generator = default_generator
|
87
170
|
@validator = validator
|
@@ -89,10 +172,35 @@ module ValueSemantics
|
|
89
172
|
freeze
|
90
173
|
end
|
91
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
|
190
|
+
end
|
191
|
+
|
192
|
+
new(
|
193
|
+
name: name,
|
194
|
+
validator: validator,
|
195
|
+
default_generator: generator,
|
196
|
+
coercer: coerce,
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
92
200
|
def determine_from!(attr_hash, klass)
|
93
201
|
raw_value = attr_hash.fetch(name) do
|
94
202
|
if default_generator.equal?(NO_DEFAULT_GENERATOR)
|
95
|
-
raise
|
203
|
+
raise MissingAttributes, "Value missing for attribute '#{name}'"
|
96
204
|
else
|
97
205
|
default_generator.call
|
98
206
|
end
|
@@ -130,11 +238,38 @@ module ValueSemantics
|
|
130
238
|
end
|
131
239
|
end
|
132
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
|
+
#
|
133
263
|
class DSL
|
264
|
+
#
|
265
|
+
# Builds a {Recipe} from a DSL block
|
266
|
+
#
|
267
|
+
# @yield to the block containing the DSL
|
268
|
+
# @return [Recipe]
|
134
269
|
def self.run(&block)
|
135
270
|
dsl = new
|
136
271
|
dsl.instance_eval(&block)
|
137
|
-
dsl.__attributes
|
272
|
+
Recipe.new(attributes: dsl.__attributes.freeze)
|
138
273
|
end
|
139
274
|
|
140
275
|
attr_reader :__attributes
|
@@ -143,8 +278,8 @@ module ValueSemantics
|
|
143
278
|
@__attributes = []
|
144
279
|
end
|
145
280
|
|
146
|
-
def
|
147
|
-
|
281
|
+
def Bool
|
282
|
+
Bool
|
148
283
|
end
|
149
284
|
|
150
285
|
def Either(*subvalidators)
|
@@ -159,60 +294,47 @@ module ValueSemantics
|
|
159
294
|
ArrayOf.new(element_validator)
|
160
295
|
end
|
161
296
|
|
162
|
-
def def_attr(
|
163
|
-
|
164
|
-
default: NOT_SPECIFIED,
|
165
|
-
default_generator: nil,
|
166
|
-
coerce: nil
|
167
|
-
)
|
168
|
-
generator = begin
|
169
|
-
if default_generator && !default.equal?(NOT_SPECIFIED)
|
170
|
-
raise ArgumentError, "Attribute '#{attr_name}' can not have both a default, and a default_generator"
|
171
|
-
elsif default_generator
|
172
|
-
default_generator
|
173
|
-
elsif !default.equal?(NOT_SPECIFIED)
|
174
|
-
->{ default }
|
175
|
-
else
|
176
|
-
Attribute::NO_DEFAULT_GENERATOR
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
__attributes << Attribute.new(
|
181
|
-
name: attr_name,
|
182
|
-
validator: validator,
|
183
|
-
default_generator: generator,
|
184
|
-
coercer: coerce
|
185
|
-
)
|
297
|
+
def def_attr(*args, **kwargs)
|
298
|
+
__attributes << Attribute.define(*args, **kwargs)
|
186
299
|
end
|
187
300
|
|
188
|
-
def method_missing(name, *args)
|
301
|
+
def method_missing(name, *args, **kwargs)
|
189
302
|
if respond_to_missing?(name)
|
190
|
-
def_attr(name, *args)
|
303
|
+
def_attr(name, *args, **kwargs)
|
191
304
|
else
|
192
305
|
super
|
193
306
|
end
|
194
307
|
end
|
195
308
|
|
196
309
|
def respond_to_missing?(method_name, _include_private=nil)
|
197
|
-
first_letter = method_name
|
310
|
+
first_letter = method_name.to_s.each_char.first
|
198
311
|
first_letter.eql?(first_letter.downcase)
|
199
312
|
end
|
200
313
|
end
|
201
314
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
315
|
+
#
|
316
|
+
# Validator that only matches `true` and `false`
|
317
|
+
#
|
318
|
+
module Bool
|
319
|
+
# @return [Boolean]
|
320
|
+
def self.===(value)
|
206
321
|
true.equal?(value) || false.equal?(value)
|
207
322
|
end
|
208
323
|
end
|
209
324
|
|
325
|
+
#
|
326
|
+
# Validator that matches any and all values
|
327
|
+
#
|
210
328
|
module Anything
|
329
|
+
# @return [true]
|
211
330
|
def self.===(_)
|
212
331
|
true
|
213
332
|
end
|
214
333
|
end
|
215
334
|
|
335
|
+
#
|
336
|
+
# Validator that matches if any of the given subvalidators matches
|
337
|
+
#
|
216
338
|
class Either
|
217
339
|
attr_reader :subvalidators
|
218
340
|
|
@@ -221,11 +343,15 @@ module ValueSemantics
|
|
221
343
|
freeze
|
222
344
|
end
|
223
345
|
|
346
|
+
# @return [Boolean]
|
224
347
|
def ===(value)
|
225
348
|
subvalidators.any? { |sv| sv === value }
|
226
349
|
end
|
227
350
|
end
|
228
351
|
|
352
|
+
#
|
353
|
+
# Validator that matches arrays if each element matches a given subvalidator
|
354
|
+
#
|
229
355
|
class ArrayOf
|
230
356
|
attr_reader :element_validator
|
231
357
|
|
@@ -234,9 +360,28 @@ module ValueSemantics
|
|
234
360
|
freeze
|
235
361
|
end
|
236
362
|
|
363
|
+
# @return [Boolean]
|
237
364
|
def ===(value)
|
238
365
|
Array === value && value.all? { |element| element_validator === element }
|
239
366
|
end
|
240
367
|
end
|
241
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
|
+
|
242
387
|
end
|
data/value_semantics.gemspec
CHANGED
@@ -9,13 +9,11 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Tom Dalling"]
|
10
10
|
spec.email = [["tom", "@", "tomdalling.com"].join]
|
11
11
|
|
12
|
-
spec.summary = %q{
|
12
|
+
spec.summary = %q{Makes value classes, with lightweight validation and coercion.}
|
13
13
|
spec.description = %q{
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
Provides the behaviour of an immutable struct-like value class,
|
18
|
-
with light-weight validation and coercion.
|
14
|
+
Generates modules that provide conventional value semantics for a given set of attributes.
|
15
|
+
The behaviour is similar to an immutable `Struct` class,
|
16
|
+
plus extensible, lightweight validation and coercion.
|
19
17
|
}
|
20
18
|
spec.homepage = "https://github.com/tomdalling/value_semantics"
|
21
19
|
spec.license = "MIT"
|
@@ -27,9 +25,10 @@ Gem::Specification.new do |spec|
|
|
27
25
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
26
|
spec.require_paths = ["lib"]
|
29
27
|
|
30
|
-
spec.add_development_dependency "bundler", "
|
31
|
-
spec.add_development_dependency "rspec", "~> 3.7
|
28
|
+
spec.add_development_dependency "bundler", ">= 1.15"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.7"
|
32
30
|
spec.add_development_dependency "mutant-rspec"
|
31
|
+
spec.add_development_dependency "yard"
|
33
32
|
spec.add_development_dependency "byebug"
|
34
33
|
spec.add_development_dependency "gem-release"
|
35
34
|
end
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: value_semantics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1
|
4
|
+
version: 3.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Dalling
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.15'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.15'
|
27
27
|
- !ruby/object:Gem::Dependency
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 3.7
|
33
|
+
version: '3.7'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 3.7
|
40
|
+
version: '3.7'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: mutant-rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: byebug
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,10 +94,9 @@ dependencies:
|
|
80
94
|
- - ">="
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
83
|
-
description: "\n
|
84
|
-
|
85
|
-
|
86
|
-
validation and coercion.\n "
|
97
|
+
description: "\n Generates modules that provide conventional value semantics for
|
98
|
+
a given set of attributes.\n The behaviour is similar to an immutable `Struct`
|
99
|
+
class,\n plus extensible, lightweight validation and coercion.\n "
|
87
100
|
email:
|
88
101
|
- tom@tomdalling.com
|
89
102
|
executables: []
|
@@ -93,12 +106,13 @@ files:
|
|
93
106
|
- ".gitignore"
|
94
107
|
- ".rspec"
|
95
108
|
- ".travis.yml"
|
109
|
+
- CHANGELOG.md
|
96
110
|
- Gemfile
|
97
111
|
- LICENSE.txt
|
98
112
|
- README.md
|
99
113
|
- bin/console
|
100
|
-
- bin/mutation_test.sh
|
101
114
|
- bin/setup
|
115
|
+
- bin/test
|
102
116
|
- lib/value_semantics.rb
|
103
117
|
- lib/value_semantics/version.rb
|
104
118
|
- value_semantics.gemspec
|
@@ -121,8 +135,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
135
|
- !ruby/object:Gem::Version
|
122
136
|
version: '0'
|
123
137
|
requirements: []
|
124
|
-
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 2.7.7
|
125
140
|
signing_key:
|
126
141
|
specification_version: 4
|
127
|
-
summary:
|
142
|
+
summary: Makes value classes, with lightweight validation and coercion.
|
128
143
|
test_files: []
|