sullivan 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|