value_semantics 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/tomdalling/value_semantics.svg?branch=master)](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
|