value_semantics 2.1.0 → 3.0.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 +4 -4
- data/.travis.yml +16 -5
- data/README.md +4 -4
- data/bin/test +19 -0
- data/lib/value_semantics.rb +171 -54
- data/lib/value_semantics/version.rb +1 -1
- data/value_semantics.gemspec +1 -0
- metadata +18 -4
- 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: 1b538b4f5ab14c310ffdbc26ccb92cb9b47806126d0bc77ef466e2d9e7d64fcb
|
4
|
+
data.tar.gz: 85a0542f08c4a832b21acf75a697f999b7eae42f6a8989ae8c1d17838b76fd8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f869be70a14418aa6247cca5222e53aed3bc7d4b506f1231e4f010b382cd962d08558a92506b47817600acca04d0f5cad1c4f9375ca6c249042ada9ea709fb3
|
7
|
+
data.tar.gz: 3c26016b77c88eb8ae851dadaf2853eb0792beb68a8dfb2961dca77d2b98414e6ba4f31ba0141a23544fb18dfcc73425cc9f1737df6539933e1b815a6037f564
|
data/.travis.yml
CHANGED
@@ -1,13 +1,24 @@
|
|
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.5
|
8
|
+
- 2.5.3
|
9
|
+
env: MUTATION_TEST=false
|
10
|
+
|
11
|
+
# test the latest Ruby version WITH mutation testing
|
12
|
+
matrix:
|
13
|
+
include:
|
14
|
+
- rvm: 2.6.0
|
15
|
+
env: MUTATION_TEST=true
|
16
|
+
|
17
|
+
# deploy gem on tagged commits, on the latest Ruby version only
|
7
18
|
deploy:
|
8
19
|
provider: rubygems
|
9
20
|
on:
|
10
21
|
tags: true
|
11
|
-
|
22
|
+
env: MUTATION_TEST=true
|
12
23
|
api_key:
|
13
24
|
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/README.md
CHANGED
@@ -103,7 +103,7 @@ Defaults
|
|
103
103
|
--------
|
104
104
|
|
105
105
|
Defaults can be specified in one of two ways:
|
106
|
-
the `:default` option, or the
|
106
|
+
the `:default` option, or the `:default_generator` option.
|
107
107
|
|
108
108
|
```ruby
|
109
109
|
class Cat
|
@@ -164,8 +164,8 @@ for common situations:
|
|
164
164
|
class LightSwitch
|
165
165
|
include ValueSemantics.for_attributes {
|
166
166
|
|
167
|
-
#
|
168
|
-
on?
|
167
|
+
# Bool: only allows `true` or `false`
|
168
|
+
on? Bool()
|
169
169
|
|
170
170
|
# ArrayOf: validates elements in an array
|
171
171
|
light_ids ArrayOf(Integer)
|
@@ -174,7 +174,7 @@ class LightSwitch
|
|
174
174
|
color Either(Integer, String, nil)
|
175
175
|
|
176
176
|
# these validators are composable
|
177
|
-
wierd_attr Either(
|
177
|
+
wierd_attr Either(Bool(), ArrayOf(Bool()))
|
178
178
|
}
|
179
179
|
end
|
180
180
|
|
data/bin/test
ADDED
@@ -0,0 +1,19 @@
|
|
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
|
+
# Mutant 0.8.24 introduces new mutations that cause infinite recursion inside
|
15
|
+
# #method_missing. These --ignore-subject lines prevent that from happening
|
16
|
+
#--ignore-subject "ValueSemantics::DSL#method_missing" \
|
17
|
+
#--ignore-subject "ValueSemantics::DSL#respond_to_missing?" \
|
18
|
+
#--ignore-subject "ValueSemantics::DSL#def_attr" \
|
19
|
+
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
|
@@ -76,12 +141,20 @@ module ValueSemantics
|
|
76
141
|
end
|
77
142
|
end
|
78
143
|
|
144
|
+
#
|
145
|
+
# Represents a single attribute of a value class
|
146
|
+
#
|
79
147
|
class Attribute
|
80
|
-
NO_DEFAULT_GENERATOR =
|
148
|
+
NO_DEFAULT_GENERATOR = lambda do
|
149
|
+
raise NoDefaultValue, "Attribute does not have a default value"
|
150
|
+
end
|
81
151
|
|
82
152
|
attr_reader :name, :validator, :coercer, :default_generator
|
83
153
|
|
84
|
-
def initialize(name:,
|
154
|
+
def initialize(name:,
|
155
|
+
default_generator: NO_DEFAULT_GENERATOR,
|
156
|
+
validator: Anything,
|
157
|
+
coercer: nil)
|
85
158
|
@name = name.to_sym
|
86
159
|
@default_generator = default_generator
|
87
160
|
@validator = validator
|
@@ -89,10 +162,35 @@ module ValueSemantics
|
|
89
162
|
freeze
|
90
163
|
end
|
91
164
|
|
165
|
+
def self.define(name,
|
166
|
+
validator=Anything,
|
167
|
+
default: NOT_SPECIFIED,
|
168
|
+
default_generator: nil,
|
169
|
+
coerce: nil)
|
170
|
+
generator = begin
|
171
|
+
if default_generator && !default.equal?(NOT_SPECIFIED)
|
172
|
+
raise ArgumentError, "Attribute '#{name}' can not have both a :default and a :default_generator"
|
173
|
+
elsif default_generator
|
174
|
+
default_generator
|
175
|
+
elsif !default.equal?(NOT_SPECIFIED)
|
176
|
+
->{ default }
|
177
|
+
else
|
178
|
+
NO_DEFAULT_GENERATOR
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
new(
|
183
|
+
name: name,
|
184
|
+
validator: validator,
|
185
|
+
default_generator: generator,
|
186
|
+
coercer: coerce,
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
92
190
|
def determine_from!(attr_hash, klass)
|
93
191
|
raw_value = attr_hash.fetch(name) do
|
94
192
|
if default_generator.equal?(NO_DEFAULT_GENERATOR)
|
95
|
-
raise
|
193
|
+
raise MissingAttributes, "Value missing for attribute '#{name}'"
|
96
194
|
else
|
97
195
|
default_generator.call
|
98
196
|
end
|
@@ -130,11 +228,38 @@ module ValueSemantics
|
|
130
228
|
end
|
131
229
|
end
|
132
230
|
|
231
|
+
#
|
232
|
+
# Contains all the configuration necessary to bake a ValueSemantics module
|
233
|
+
#
|
234
|
+
# @see ValueSemantics.bake_module
|
235
|
+
#
|
236
|
+
class Recipe
|
237
|
+
attr_reader :attributes
|
238
|
+
|
239
|
+
def initialize(attributes:)
|
240
|
+
@attributes = attributes
|
241
|
+
freeze
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
#
|
246
|
+
# Builds a {Recipe} via DSL methods
|
247
|
+
#
|
248
|
+
# DSL blocks are <code>instance_eval</code>d against an object of this class.
|
249
|
+
#
|
250
|
+
# @see Recipe
|
251
|
+
# @see ValueSemantics.for_attributes
|
252
|
+
#
|
133
253
|
class DSL
|
254
|
+
#
|
255
|
+
# Builds a {Recipe} from a DSL block
|
256
|
+
#
|
257
|
+
# @yield to the block containing the DSL
|
258
|
+
# @return [Recipe]
|
134
259
|
def self.run(&block)
|
135
260
|
dsl = new
|
136
261
|
dsl.instance_eval(&block)
|
137
|
-
dsl.__attributes
|
262
|
+
Recipe.new(attributes: dsl.__attributes.freeze)
|
138
263
|
end
|
139
264
|
|
140
265
|
attr_reader :__attributes
|
@@ -143,8 +268,8 @@ module ValueSemantics
|
|
143
268
|
@__attributes = []
|
144
269
|
end
|
145
270
|
|
146
|
-
def
|
147
|
-
|
271
|
+
def Bool
|
272
|
+
Bool
|
148
273
|
end
|
149
274
|
|
150
275
|
def Either(*subvalidators)
|
@@ -159,30 +284,8 @@ module ValueSemantics
|
|
159
284
|
ArrayOf.new(element_validator)
|
160
285
|
end
|
161
286
|
|
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
|
-
)
|
287
|
+
def def_attr(*args)
|
288
|
+
__attributes << Attribute.define(*args)
|
186
289
|
end
|
187
290
|
|
188
291
|
def method_missing(name, *args)
|
@@ -194,25 +297,34 @@ module ValueSemantics
|
|
194
297
|
end
|
195
298
|
|
196
299
|
def respond_to_missing?(method_name, _include_private=nil)
|
197
|
-
first_letter = method_name
|
300
|
+
first_letter = method_name.to_s.each_char.first
|
198
301
|
first_letter.eql?(first_letter.downcase)
|
199
302
|
end
|
200
303
|
end
|
201
304
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
305
|
+
#
|
306
|
+
# Validator that only matches `true` and `false`
|
307
|
+
#
|
308
|
+
module Bool
|
309
|
+
# @return [Boolean]
|
310
|
+
def self.===(value)
|
206
311
|
true.equal?(value) || false.equal?(value)
|
207
312
|
end
|
208
313
|
end
|
209
314
|
|
315
|
+
#
|
316
|
+
# Validator that matches any and all values
|
317
|
+
#
|
210
318
|
module Anything
|
319
|
+
# @return [true]
|
211
320
|
def self.===(_)
|
212
321
|
true
|
213
322
|
end
|
214
323
|
end
|
215
324
|
|
325
|
+
#
|
326
|
+
# Validator that matches if any of the given subvalidators matches
|
327
|
+
#
|
216
328
|
class Either
|
217
329
|
attr_reader :subvalidators
|
218
330
|
|
@@ -221,11 +333,15 @@ module ValueSemantics
|
|
221
333
|
freeze
|
222
334
|
end
|
223
335
|
|
336
|
+
# @return [Boolean]
|
224
337
|
def ===(value)
|
225
338
|
subvalidators.any? { |sv| sv === value }
|
226
339
|
end
|
227
340
|
end
|
228
341
|
|
342
|
+
#
|
343
|
+
# Validator that matches arrays if each element matches a given subvalidator
|
344
|
+
#
|
229
345
|
class ArrayOf
|
230
346
|
attr_reader :element_validator
|
231
347
|
|
@@ -234,6 +350,7 @@ module ValueSemantics
|
|
234
350
|
freeze
|
235
351
|
end
|
236
352
|
|
353
|
+
# @return [Boolean]
|
237
354
|
def ===(value)
|
238
355
|
Array === value && value.all? { |element| element_validator === element }
|
239
356
|
end
|
data/value_semantics.gemspec
CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_development_dependency "bundler", "~> 1.15"
|
31
31
|
spec.add_development_dependency "rspec", "~> 3.7.0"
|
32
32
|
spec.add_development_dependency "mutant-rspec"
|
33
|
+
spec.add_development_dependency "yard"
|
33
34
|
spec.add_development_dependency "byebug"
|
34
35
|
spec.add_development_dependency "gem-release"
|
35
36
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: value_semantics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
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: 2019-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -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
|
@@ -97,8 +111,8 @@ files:
|
|
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,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
135
|
- !ruby/object:Gem::Version
|
122
136
|
version: '0'
|
123
137
|
requirements: []
|
124
|
-
rubygems_version: 3.0.
|
138
|
+
rubygems_version: 3.0.2
|
125
139
|
signing_key:
|
126
140
|
specification_version: 4
|
127
141
|
summary: Create value classes quickly, with all the proper conventions.
|