value_semantics 0.1.1 → 1.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 +4 -1
- data/README.md +91 -38
- data/lib/value_semantics/version.rb +1 -1
- data/lib/value_semantics.rb +77 -12
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36400a694002cc8c879dfbad099d365e2674edf63ad258794094e2bb41f894da
|
4
|
+
data.tar.gz: f8f67ee234971124bcb65e487d59ed3126c8f51406b1bc4cc9d93eb1d70a5262
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d8becb2999466ceb24b0c9e6139ab089a64f019304c8803e6412e6a4818965dac59c601fc65f950c7e29cd5f4e27895a37e677baac6054e554e4fe749f2b5a6
|
7
|
+
data.tar.gz: 67d37d2f3fe4f07a6c54fdec2546e3a2b2e12ee92b1aa69a914382fee08ad9b0a2bfc00948e38f1b17b1608a6247763e507e255640add73c0ea0a3c5d6bac9b0
|
data/.travis.yml
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
- 2.
|
3
|
+
- 2.3.7
|
4
|
+
- 2.4.4
|
5
|
+
- 2.5.1
|
4
6
|
script: bundle exec rspec
|
5
7
|
deploy:
|
6
8
|
provider: rubygems
|
7
9
|
on:
|
8
10
|
tags: true
|
11
|
+
rvm: 2.5.1
|
9
12
|
api_key:
|
10
13
|
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
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
[](https://travis-ci.org/tomdalling/value_semantics)
|
2
|
+
|
3
|
+
ValueSemantics
|
4
|
+
==============
|
2
5
|
|
3
6
|
Create value classes quickly, with all the [conventions of a good value object](https://github.com/zverok/good-value-object).
|
4
7
|
|
@@ -10,7 +13,9 @@ These are intended for internal use, as opposed to validating user input like Ac
|
|
10
13
|
Invalid or missing attributes cause an exception intended for developers,
|
11
14
|
not an error message intended for the user.
|
12
15
|
|
13
|
-
|
16
|
+
|
17
|
+
Basic Usage
|
18
|
+
-----------
|
14
19
|
|
15
20
|
```ruby
|
16
21
|
require 'value_semantics'
|
@@ -62,10 +67,15 @@ mandatory due to Ruby's precedence rules.
|
|
62
67
|
The `do`/`end` syntax will not work unless you surround the whole thing with parenthesis.
|
63
68
|
|
64
69
|
|
65
|
-
|
70
|
+
Validation (Types)
|
71
|
+
------------------
|
72
|
+
|
73
|
+
Each attribute may optionally have a validator, to check that values are correct.
|
66
74
|
|
67
75
|
Validators are objects that implement the `===` method,
|
68
|
-
which means you can use `Class` objects (like `String`)
|
76
|
+
which means you can use `Class` objects (like `String`),
|
77
|
+
and also things like regular expressions.
|
78
|
+
Anything that you can use in a `case`/`when` expression will work.
|
69
79
|
|
70
80
|
```ruby
|
71
81
|
class Person
|
@@ -86,6 +96,41 @@ Person.new(birthday: "hello", ...)
|
|
86
96
|
#=> Value for attribute 'birthday' is not valid: "hello"
|
87
97
|
```
|
88
98
|
|
99
|
+
|
100
|
+
### Built-in Validators
|
101
|
+
|
102
|
+
The ValueSemantics DSL comes with a small number of built-in validators,
|
103
|
+
for common situations:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class LightSwitch
|
107
|
+
include ValueSemantics.for_attributes {
|
108
|
+
|
109
|
+
# Boolean: only allows `true` or `false`
|
110
|
+
on? Boolean()
|
111
|
+
|
112
|
+
# ArrayOf: validates elements in an array
|
113
|
+
light_ids ArrayOf(Integer)
|
114
|
+
|
115
|
+
# Either: value must match at least one of a list of validators
|
116
|
+
color Either(Integer, String, nil)
|
117
|
+
|
118
|
+
# these validators are composable
|
119
|
+
wierd_attr Either(Boolean(), ArrayOf(Boolean()))
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
LightSwitch.new(
|
124
|
+
on?: true,
|
125
|
+
light_ids: [11, 12, 13],
|
126
|
+
color: "#FFAABB",
|
127
|
+
wierd_attr: [true, false, true, true],
|
128
|
+
)
|
129
|
+
```
|
130
|
+
|
131
|
+
|
132
|
+
### Custom Validators
|
133
|
+
|
89
134
|
A custom validator might look something like this:
|
90
135
|
|
91
136
|
```ruby
|
@@ -110,70 +155,78 @@ Person.new(age: 8)
|
|
110
155
|
Default attribute values also pass through validation.
|
111
156
|
|
112
157
|
|
113
|
-
|
158
|
+
Coercion
|
159
|
+
--------
|
160
|
+
|
161
|
+
Coercion allows non-standard or "convenience" values to be converted into
|
162
|
+
proper, valid values, where possible.
|
163
|
+
|
164
|
+
For example, an object with an `IPAddr` attribute may allow string values,
|
165
|
+
which are then coerced into `IPAddr` objects.
|
114
166
|
|
115
|
-
|
167
|
+
To implement coercion, define a class method called `coerce_#{attr}` which
|
168
|
+
accepts a raw value, and returns the coerced value.
|
116
169
|
|
117
170
|
```ruby
|
118
171
|
class Server
|
119
172
|
include ValueSemantics.for_attributes {
|
120
|
-
address IPAddr
|
121
|
-
if value.is_a?(String)
|
122
|
-
IPAddr.new(value)
|
123
|
-
else
|
124
|
-
value
|
125
|
-
end
|
126
|
-
end
|
173
|
+
address IPAddr
|
127
174
|
}
|
175
|
+
|
176
|
+
def self.coerce_address(value)
|
177
|
+
if value.is_a?(String)
|
178
|
+
IPAddr.new(value)
|
179
|
+
else
|
180
|
+
value
|
181
|
+
end
|
182
|
+
end
|
128
183
|
end
|
129
184
|
|
130
|
-
Server.new(address: '127.0.0.1')
|
131
|
-
Server
|
185
|
+
Server.new(address: '127.0.0.1')
|
186
|
+
#=> #<Server address=#<IPAddr: IPv4:127.0.0.1/255.255.255.255>>
|
187
|
+
|
188
|
+
Server.new(address: IPAddr.new('127.0.0.1'))
|
189
|
+
#=> #<Server address=#<IPAddr: IPv4:127.0.0.1/255.255.255.255>>
|
190
|
+
|
132
191
|
Server.new(address: 42)
|
133
192
|
#=> ArgumentError:
|
134
193
|
#=> Value for attribute 'address' is not valid: 42
|
135
194
|
```
|
136
195
|
|
137
|
-
If coercion is not possible,
|
138
|
-
|
196
|
+
If coercion is not possible, you can return the value unchanged,
|
197
|
+
allowing the validator to fail.
|
198
|
+
Another option is to raise an error within the coercion method.
|
139
199
|
|
140
200
|
Coercion happens before validation.
|
141
201
|
Default attribute values also pass through coercion.
|
142
202
|
|
143
|
-
The coercion block runs in the context of the value object,
|
144
|
-
so you can call methods from the value object.
|
145
|
-
For example:
|
146
203
|
|
147
|
-
|
148
|
-
|
204
|
+
## All Together
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
class Person
|
149
208
|
include ValueSemantics.for_attributes {
|
150
|
-
|
151
|
-
|
152
|
-
end
|
209
|
+
name String, default: "Anon Emous"
|
210
|
+
birthday Either(Date, nil)
|
153
211
|
}
|
154
212
|
|
155
|
-
def
|
213
|
+
def self.coerce_birthday(value)
|
156
214
|
if value.is_a?(String)
|
157
|
-
|
215
|
+
Date.parse(value)
|
158
216
|
else
|
159
217
|
value
|
160
218
|
end
|
161
219
|
end
|
162
220
|
end
|
163
|
-
```
|
164
221
|
|
165
|
-
|
222
|
+
Person.new(name: "Tom", birthday: "2020-12-25")
|
223
|
+
#=> #<Person name="Tom" birthday=#<Date: 2020-12-25 ((2459209j,0s,0n),+0s,2299161j)>>
|
166
224
|
|
167
|
-
|
168
|
-
|
169
|
-
include ValueSemantics.for_attributes {
|
170
|
-
latitude Float, default: 0 { |value| value.to_f }
|
171
|
-
longitude Float, default: 0 { |value| value.to_f }
|
172
|
-
}
|
173
|
-
end
|
225
|
+
Person.new(birthday: Date.today)
|
226
|
+
#=> #<Person name="Anon Emous" birthday=#<Date: 2018-09-04 ((2458366j,0s,0n),+0s,2299161j)>>
|
174
227
|
|
175
|
-
|
176
|
-
#=> #<
|
228
|
+
Person.new(birthday: nil)
|
229
|
+
#=> #<Person name="Anon Emous" birthday=nil>
|
177
230
|
```
|
178
231
|
|
179
232
|
|
data/lib/value_semantics.rb
CHANGED
@@ -32,7 +32,7 @@ module ValueSemantics
|
|
32
32
|
remaining_attrs = given_attrs.dup
|
33
33
|
|
34
34
|
self.class.attributes.each do |attr|
|
35
|
-
key, value = attr.determine_from!(remaining_attrs, self)
|
35
|
+
key, value = attr.determine_from!(remaining_attrs, self.class)
|
36
36
|
instance_variable_set(attr.instance_variable, value)
|
37
37
|
remaining_attrs.delete(key)
|
38
38
|
end
|
@@ -75,18 +75,17 @@ module ValueSemantics
|
|
75
75
|
end
|
76
76
|
|
77
77
|
class Attribute
|
78
|
-
attr_reader :name, :has_default, :default_value
|
78
|
+
attr_reader :name, :has_default, :default_value
|
79
79
|
|
80
|
-
def initialize(name:, has_default:, default_value:, validator
|
80
|
+
def initialize(name:, has_default:, default_value:, validator:)
|
81
81
|
@name = name.to_sym
|
82
82
|
@has_default = has_default
|
83
83
|
@default_value = default_value
|
84
84
|
@validator = validator
|
85
|
-
@coercer = coercer
|
86
85
|
freeze
|
87
86
|
end
|
88
87
|
|
89
|
-
def determine_from!(attr_hash,
|
88
|
+
def determine_from!(attr_hash, klass)
|
90
89
|
raw_value = attr_hash.fetch(name) do
|
91
90
|
if has_default
|
92
91
|
default_value
|
@@ -95,7 +94,7 @@ module ValueSemantics
|
|
95
94
|
end
|
96
95
|
end
|
97
96
|
|
98
|
-
coerced_value =
|
97
|
+
coerced_value = coerce(raw_value, klass)
|
99
98
|
|
100
99
|
if validate?(coerced_value)
|
101
100
|
[name, coerced_value]
|
@@ -104,6 +103,14 @@ module ValueSemantics
|
|
104
103
|
end
|
105
104
|
end
|
106
105
|
|
106
|
+
def coerce(attr_value, klass)
|
107
|
+
if klass.respond_to?(coercion_method)
|
108
|
+
klass.public_send(coercion_method, attr_value)
|
109
|
+
else
|
110
|
+
attr_value
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
107
114
|
def default_value
|
108
115
|
if has_default
|
109
116
|
@default_value
|
@@ -119,6 +126,10 @@ module ValueSemantics
|
|
119
126
|
def instance_variable
|
120
127
|
'@' + name.to_s.chomp('!').chomp('?')
|
121
128
|
end
|
129
|
+
|
130
|
+
def coercion_method
|
131
|
+
"coerce_#{name}"
|
132
|
+
end
|
122
133
|
end
|
123
134
|
|
124
135
|
class DSL
|
@@ -136,29 +147,83 @@ module ValueSemantics
|
|
136
147
|
@__attributes = []
|
137
148
|
end
|
138
149
|
|
139
|
-
def
|
140
|
-
|
150
|
+
def Boolean
|
151
|
+
Boolean
|
152
|
+
end
|
153
|
+
|
154
|
+
def Either(*subvalidators)
|
155
|
+
Either.new(subvalidators)
|
156
|
+
end
|
157
|
+
|
158
|
+
def Anything
|
159
|
+
Anything
|
160
|
+
end
|
141
161
|
|
162
|
+
def ArrayOf(element_validator)
|
163
|
+
ArrayOf.new(element_validator)
|
164
|
+
end
|
165
|
+
|
166
|
+
def declare_attribute(attr_name, validator=Anything, default: NOT_SPECIFIED)
|
142
167
|
__attributes << Attribute.new(
|
143
168
|
name: attr_name,
|
144
169
|
has_default: default != NOT_SPECIFIED,
|
145
170
|
default_value: default,
|
146
171
|
validator: validator,
|
147
|
-
coercer: coercion_block || IdentityCoercer,
|
148
172
|
)
|
149
173
|
end
|
150
174
|
|
175
|
+
def method_missing(name, *args, &block)
|
176
|
+
if respond_to_missing?(name)
|
177
|
+
declare_attribute(name, *args, &block)
|
178
|
+
else
|
179
|
+
super
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
151
183
|
def respond_to_missing?(method_name, include_private = false)
|
152
|
-
|
184
|
+
first_letter = method_name.to_s[0]
|
185
|
+
(first_letter == first_letter.downcase) || super
|
153
186
|
end
|
154
187
|
end
|
155
188
|
|
156
|
-
module
|
189
|
+
module Boolean
|
190
|
+
extend self
|
191
|
+
|
192
|
+
def ===(value)
|
193
|
+
true.eql?(value) || false.eql?(value)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
module Anything
|
157
198
|
def self.===(value)
|
158
199
|
true
|
159
200
|
end
|
160
201
|
end
|
161
202
|
|
162
|
-
|
203
|
+
class Either
|
204
|
+
attr_reader :subvalidators
|
205
|
+
|
206
|
+
def initialize(subvalidators)
|
207
|
+
@subvalidators = subvalidators
|
208
|
+
freeze
|
209
|
+
end
|
210
|
+
|
211
|
+
def ===(value)
|
212
|
+
subvalidators.any? { |sv| sv === value }
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
class ArrayOf
|
217
|
+
attr_reader :element_validator
|
218
|
+
|
219
|
+
def initialize(element_validator)
|
220
|
+
@element_validator = element_validator
|
221
|
+
freeze
|
222
|
+
end
|
223
|
+
|
224
|
+
def ===(value)
|
225
|
+
Array === value && value.all? { |element| element_validator === element }
|
226
|
+
end
|
227
|
+
end
|
163
228
|
|
164
229
|
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: 0.
|
4
|
+
version: 1.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: 2018-
|
11
|
+
date: 2018-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|