value_semantics 2.1.0 → 3.2.1
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 +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: []
|