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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b993257c0f51eb41768a9c272798cd6ec6caa160
4
- data.tar.gz: 5e15408676c3ad43fb4645581120072914ae60aa
3
+ metadata.gz: facd94a17e179bef5d7a3caa27a183b1bc855a9c
4
+ data.tar.gz: 1abdb5a1efee46327163f3bf68eb5d43c825ceaa
5
5
  SHA512:
6
- metadata.gz: da18cbc61e3cff2bb8e1715126a9e46dd8ead724d719a5c8534eb3717074fac16568ac9127bc461aeb32515d30981400e925e3c7c4e4511abef429f51ce8e675
7
- data.tar.gz: 13dece6a3657c7e653b87db7847ef42ad6faee4db2c6ee70cc52d62295ab05f08c4781bb3f5913843dee6a4499a6805797a168bd849cb9475773c71ef5ad34d8
6
+ metadata.gz: 79388d55c6f019930dcfb8607927021190678d4967c11daf7ca20a734b18708a93c0f3f161e646430e3791b5b3a92e2bb5cabe96822c588790d64a5600e6648c
7
+ data.tar.gz: c9e6f2c9d92a24844d9e764cc37f30be64000f96947abd5edf3830db7c7fd05d9bf2d5858f609b9616593a3a4f9a46acd82c5c071a1921ce4e214e8c6b22fc57
data/README.md CHANGED
@@ -1,6 +1,78 @@
1
1
  # Sullivan
2
2
 
3
- TODO: Write a gem description
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
- TODO: Write usage instructions here
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/<my-github-username>/sullivan/fork )
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`)
@@ -1,6 +1,15 @@
1
1
  module Sullivan
2
2
  def self.validation(&block)
3
- DSL.new.instance_eval(&block)
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
@@ -1,3 +1,3 @@
1
1
  module Sullivan
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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.1
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-07 00:00:00.000000000 Z
11
+ date: 2014-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler