sullivan 0.0.1 → 0.0.2
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/README.md +225 -3
- data/lib/sullivan.rb +10 -1
- data/lib/sullivan/version.rb +1 -1
- data/spec/sullivan_spec.rb +21 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: facd94a17e179bef5d7a3caa27a183b1bc855a9c
|
4
|
+
data.tar.gz: 1abdb5a1efee46327163f3bf68eb5d43c825ceaa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79388d55c6f019930dcfb8607927021190678d4967c11daf7ca20a734b18708a93c0f3f161e646430e3791b5b3a92e2bb5cabe96822c588790d64a5600e6648c
|
7
|
+
data.tar.gz: c9e6f2c9d92a24844d9e764cc37f30be64000f96947abd5edf3830db7c7fd05d9bf2d5858f609b9616593a3a4f9a46acd82c5c071a1921ce4e214e8c6b22fc57
|
data/README.md
CHANGED
@@ -1,6 +1,78 @@
|
|
1
1
|
# Sullivan
|
2
2
|
|
3
|
-
|
3
|
+
<img src="doc/img/LouisSullivan.jpg" alt="Louis Sullivan" align="right" />
|
4
|
+
|
5
|
+
> It is the pervading law of all things organic and inorganic,
|
6
|
+
> Of all things physical and metaphysical,
|
7
|
+
> Of all things human, and all things super-human,
|
8
|
+
> Of all true manifestations of the head,
|
9
|
+
> Of the heart, of the soul,
|
10
|
+
> That the life is recognizable in its expression,
|
11
|
+
> That **form ever follows function**. This is the law.
|
12
|
+
> <cite>— Louis Sullivan, 1896</cite>
|
13
|
+
|
14
|
+
**Sullivan** is a functional, composable, simple way to validate nested data
|
15
|
+
structures. It generates validation errors which are especially suitable for
|
16
|
+
API responses.
|
17
|
+
|
18
|
+
Sullivan doesn't do much, because it doesn't need to. It's three things:
|
19
|
+
|
20
|
+
1. A simple pattern for defining validators,
|
21
|
+
2. A handful of useful, composable validators provided for free, and
|
22
|
+
3. Some syntactic sugar for using the built-in validators.
|
23
|
+
|
24
|
+
## Example
|
25
|
+
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'sullivan'
|
29
|
+
|
30
|
+
laugh_session_validation = Sullivan.validation do
|
31
|
+
laugh = hash(
|
32
|
+
sound: string_matching(/\Al(ol)+\z/, error: "must be be a laughing sound of some length"),
|
33
|
+
intensity: optional(kind_of(Numeric))
|
34
|
+
)
|
35
|
+
|
36
|
+
hash(
|
37
|
+
primary_laugh: laugh,
|
38
|
+
rebound_giggles: many(laugh)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
laugh_session = {
|
43
|
+
primary_laugh: {
|
44
|
+
sound: "lolololol",
|
45
|
+
intensity: "High"
|
46
|
+
},
|
47
|
+
rebound_giggles: [
|
48
|
+
{
|
49
|
+
sound: "lololol",
|
50
|
+
intensity: 2
|
51
|
+
},
|
52
|
+
{
|
53
|
+
sound: "sigh",
|
54
|
+
mood: "pleasant"
|
55
|
+
}
|
56
|
+
]
|
57
|
+
}
|
58
|
+
|
59
|
+
laugh_session_validation.validate(laugh_session)
|
60
|
+
|
61
|
+
# =>
|
62
|
+
# {
|
63
|
+
# :primary_laugh => {
|
64
|
+
# :intensity => "must be a kind of Numeric, if present"
|
65
|
+
# },
|
66
|
+
# :rebound_giggles => [
|
67
|
+
# nil,
|
68
|
+
# {
|
69
|
+
# :sound => "must be be a laughing sound of some length",
|
70
|
+
# :mood => "is unexpected"
|
71
|
+
# }
|
72
|
+
# ]
|
73
|
+
# }
|
74
|
+
```
|
75
|
+
|
4
76
|
|
5
77
|
## Installation
|
6
78
|
|
@@ -18,11 +90,161 @@ Or install it yourself as:
|
|
18
90
|
|
19
91
|
## Usage
|
20
92
|
|
21
|
-
|
93
|
+
Like it says above, Sullivan is three things.
|
94
|
+
|
95
|
+
### `validate`: A simple pattern for defining validators.
|
96
|
+
|
97
|
+
Sullivan specifies a simple API for defining validators. It's so simple, you
|
98
|
+
don't need Sullivan to use it. All you need is an object which responds to
|
99
|
+
`#validate`. If it fails to validate, it should return an error message (a
|
100
|
+
string). If it passes validation, it should return `nil`. That's all there is
|
101
|
+
to it.
|
102
|
+
|
103
|
+
If your validator takes parameters, it might make sense to write it as a class
|
104
|
+
and instantiate it:
|
105
|
+
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class LegalVotingAge
|
109
|
+
def initialize(country:)
|
110
|
+
@minimum_age =
|
111
|
+
case country
|
112
|
+
when :united_states
|
113
|
+
18
|
114
|
+
when :austria
|
115
|
+
16
|
116
|
+
else
|
117
|
+
raise "Don't know the voting age in #{country}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def validate(age)
|
122
|
+
"is too young to vote" if age < @minimum_age
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
LegalVotingAge.new(:united_states).validate(17) #=> "is too young to vote"
|
127
|
+
LegalVotingAge.new(:austria).validate(17) #=> nil
|
128
|
+
```
|
129
|
+
|
130
|
+
If your validator doesn't take parameters, you might want to just make it an
|
131
|
+
object:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
ApiBoolean = Object.tap do |v|
|
135
|
+
def v.validate(value)
|
136
|
+
"must be a boolean value" unless [true, false, 'true', 'false'].include?(value)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
ApiBoolean.validate(true) #=> nil
|
141
|
+
ApiBoolean.validate('false') #=> nil
|
142
|
+
ApiBoolean.validate('not sure') #=> "must be a boolean value"
|
143
|
+
```
|
144
|
+
|
145
|
+
Sullivan doesn't care. In fact, Sullivan-the-libary isn't even involved yet.
|
146
|
+
|
147
|
+
### `Sullivan::Validators`: A handful of useful, composable validators provided for free
|
148
|
+
|
149
|
+
Sometimes you need a really custom validator, but there are a few staples we
|
150
|
+
need in all sorts of projects. Sullivan provides those, including some
|
151
|
+
**higher-order validators** (validators which take other validators as
|
152
|
+
parameters, like `Hash` and `Optional`), which is where Sullivan's
|
153
|
+
composability really shines.
|
154
|
+
|
155
|
+
Sullivan's built-in validators live in `Sullivan::Validators`. Look there to
|
156
|
+
read more about each one.
|
157
|
+
|
158
|
+
|
159
|
+
### `Sullivan.validation`: Some syntactic sugar for using the built-in validators
|
160
|
+
|
161
|
+
The built-in validators are a bit cumbersome to instantiate, considering you'll
|
162
|
+
be using them quite a bit. To help, there's `Sullivan.validation`. Within its
|
163
|
+
block, you can instantiate the validators in `Sullivan::Validators` as
|
164
|
+
`snake_cased` methods. So:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
v = Sullivan.validation do
|
168
|
+
hash({})
|
169
|
+
end
|
170
|
+
|
171
|
+
v.class #=> Sullivan::Validators::Hash
|
172
|
+
```
|
173
|
+
|
174
|
+
There's one catch: because this uses `instance_eval`, inside the block `self`
|
175
|
+
will not be the same as `self` outside the block, so you can't use method calls
|
176
|
+
the way you might like to. That is, this won't work:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class User
|
180
|
+
def valid_username_regex
|
181
|
+
%r{\w+}
|
182
|
+
end
|
183
|
+
|
184
|
+
def validation
|
185
|
+
Sullivan.validation do
|
186
|
+
string_matching(valid_username_regex)
|
187
|
+
#=> NameError: undefined local variable or method `valid_username_regex' for #<Sullivan::DSL:0x007fa5c44e1508>
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
If you need to do something like that, you can use the 1-arity form of the block, for a slightly more verbose syntax:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class User
|
197
|
+
def valid_username_regex
|
198
|
+
%r{\w+}
|
199
|
+
end
|
200
|
+
|
201
|
+
def validation
|
202
|
+
Sullivan.validation do |vals|
|
203
|
+
vals.string_matching(valid_username_regex)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
Of course, `Sullivan.validation` is completely optional. Feel free to instantiate the validation classes directly.
|
210
|
+
|
211
|
+
|
212
|
+
### Composition: Bringing it all together
|
213
|
+
|
214
|
+
This is where it gets fun. Let's say you're validating API input. Suppose you
|
215
|
+
have an API that can create a Person record and one that can create multiple
|
216
|
+
Person records at once. You might have validations like:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
module Validations
|
220
|
+
Person = Sullivan.validation do
|
221
|
+
hash(
|
222
|
+
name: kind_of(String),
|
223
|
+
favorite_ice_cream_flavor: optional(kind_of(String))
|
224
|
+
)
|
225
|
+
end
|
226
|
+
|
227
|
+
PersonCreation = Sullivan.validation do
|
228
|
+
hash(person: Person)
|
229
|
+
end
|
230
|
+
|
231
|
+
PeopleCreation = Sullivan.validation do
|
232
|
+
hash(people: many(Person))
|
233
|
+
end
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
Now your API could use `Validations::PersonCreation.validate` and `Validations::PeopleCreation.validate`
|
238
|
+
to validate the two kinds of requests.
|
239
|
+
|
240
|
+
Notice that I've assigned these validators to constants in a module. That's a
|
241
|
+
useful pattern in some cases, but it's completely optional. Store them wherever
|
242
|
+
they're most useful in your application. They're just objects.
|
243
|
+
|
22
244
|
|
23
245
|
## Contributing
|
24
246
|
|
25
|
-
1. Fork it ( http://github.com
|
247
|
+
1. Fork it ( http://github.com/Peeja/sullivan/fork )
|
26
248
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
249
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
250
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/lib/sullivan.rb
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
module Sullivan
|
2
2
|
def self.validation(&block)
|
3
|
-
DSL.new
|
3
|
+
dsl = DSL.new
|
4
|
+
|
5
|
+
case block.arity
|
6
|
+
when 0
|
7
|
+
dsl.instance_eval(&block)
|
8
|
+
when 1
|
9
|
+
block.call(dsl)
|
10
|
+
else
|
11
|
+
raise ArgumentError.new("Sullivan.validation's block must have an arity of 0 or 1.")
|
12
|
+
end
|
4
13
|
end
|
5
14
|
|
6
15
|
class DSL < BasicObject
|
data/lib/sullivan/version.rb
CHANGED
data/spec/sullivan_spec.rb
CHANGED
@@ -6,7 +6,7 @@ describe Sullivan do
|
|
6
6
|
v = Sullivan.validation do
|
7
7
|
hash(
|
8
8
|
string_matching: string_matching(/\Al(ol)+\z/, error: "must be be a laugh"),
|
9
|
-
kind_of: kind_of(Numeric)
|
9
|
+
kind_of: kind_of(Numeric)
|
10
10
|
)
|
11
11
|
end
|
12
12
|
|
@@ -15,5 +15,25 @@ describe Sullivan do
|
|
15
15
|
expect(error[:string_matching]).to eq("must be be a laugh")
|
16
16
|
expect(error[:kind_of]).to eq("must be a kind of Numeric")
|
17
17
|
end
|
18
|
+
|
19
|
+
it "uses a non-instance-eval version when the block has an arity of 1" do
|
20
|
+
v = Sullivan.validation do |vals|
|
21
|
+
expect(a_method_on_self).to eq("can be called without an error")
|
22
|
+
|
23
|
+
vals.hash(
|
24
|
+
string_matching: vals.string_matching(/\Al(ol)+\z/, error: "must be be a laugh"),
|
25
|
+
kind_of: vals.kind_of(Numeric)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
error = v.validate({})
|
30
|
+
|
31
|
+
expect(error[:string_matching]).to eq("must be be a laugh")
|
32
|
+
expect(error[:kind_of]).to eq("must be a kind of Numeric")
|
33
|
+
end
|
34
|
+
|
35
|
+
def a_method_on_self
|
36
|
+
"can be called without an error"
|
37
|
+
end
|
18
38
|
end
|
19
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sullivan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Jaros
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|