scrivener 1.0.0 → 1.1.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 +5 -5
- data/CONTRIBUTING +19 -0
- data/LICENSE +1 -1
- data/README.md +129 -105
- data/lib/scrivener.rb +1 -1
- data/lib/scrivener/validations.rb +3 -3
- data/test/scrivener_test.rb +63 -27
- metadata +10 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ca36d57e390df275cd0365e9b8d0b9a8f3d0dda7b00a39aaaf4a7da37a9f0202
|
4
|
+
data.tar.gz: b48a3d6ab1a6646fc649d12f501d97b36dfde4729d377848b0b4d97c8147b53b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbe35ddb7f28d6cbee5d7f31731af970cef677060f6428f4aec67da1ec11ab11e6f78edb3aee2dfda56b4a950cf9e16a2a6326eb65e5119d3e4fb1bbd615396a
|
7
|
+
data.tar.gz: 45e428e8d5204f8493199d020bd5af06fd857cfa42b67aac4b03e078b23db7a47bce1ed7588621198ad5fb797f102aab1178cc7245444a25bdd2cc446ba51f51
|
data/CONTRIBUTING
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
This code tries to solve a particular problem with a very simple
|
2
|
+
implementation. We try to keep the code to a minimum while making
|
3
|
+
it as clear as possible. The design is very likely finished, and
|
4
|
+
if some feature is missing it is possible that it was left out on
|
5
|
+
purpose. That said, new usage patterns may arise, and when that
|
6
|
+
happens we are ready to adapt if necessary.
|
7
|
+
|
8
|
+
A good first step for contributing is to meet us on IRC and discuss
|
9
|
+
ideas. We spend a lot of time on #lesscode at freenode, always ready
|
10
|
+
to talk about code and simplicity. If connecting to IRC is not an
|
11
|
+
option, you can create an issue explaining the proposed change and
|
12
|
+
a use case. We pay a lot of attention to use cases, because our
|
13
|
+
goal is to keep the code base simple. Usually the result of a
|
14
|
+
conversation is the creation of a different tool.
|
15
|
+
|
16
|
+
Please don't start the conversation with a pull request. The code
|
17
|
+
should come at last, and even though it may help to convey an idea,
|
18
|
+
more often than not it draws the attention to a particular
|
19
|
+
implementation.
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -6,81 +6,18 @@ Validation frontend for models.
|
|
6
6
|
Description
|
7
7
|
-----------
|
8
8
|
|
9
|
-
Scrivener removes the validation responsibility from models and
|
10
|
-
filter for whitelisted attributes.
|
11
|
-
|
12
|
-
|
13
|
-
the set of validations for a User in a Sign up process may not be the same
|
14
|
-
as the one exposed to an Admin when editing a user profile. While you want
|
15
|
-
the User to provide an email, a password and a password confirmation, you
|
16
|
-
probably don't want the admin to mess with those attributes at all.
|
17
|
-
|
18
|
-
In a wizard, different model states ask for different validations, and a single
|
19
|
-
set of validations for the whole process is not the best solution.
|
20
|
-
|
21
|
-
Scrivener is Bureaucrat's little brother. It draws all the inspiration from it
|
22
|
-
and its features are a subset of Bureaucrat's. For a more robust and tested
|
23
|
-
solution, please [check it](https://github.com/tizoc/bureaucrat).
|
24
|
-
|
25
|
-
This library exists to satify the need of extracting Ohm's validations for
|
26
|
-
reuse in other scenarios.
|
9
|
+
Scrivener removes the validation responsibility from models and
|
10
|
+
acts as a filter for whitelisted attributes. Read about the
|
11
|
+
[motivation](#motivation) to understand why this separation of
|
12
|
+
concerns is important.
|
27
13
|
|
28
14
|
Usage
|
29
15
|
-----
|
30
16
|
|
31
|
-
|
32
|
-
using. As it provides its own validation and whitelisting features, you can
|
33
|
-
choose to ignore the ones that come bundled with ORMs.
|
34
|
-
|
35
|
-
This short example illustrates how to move the validation and whitelisting
|
36
|
-
responsibilities away from the model and into Scrivener:
|
17
|
+
A very basic example would be creating a blog post:
|
37
18
|
|
38
19
|
```ruby
|
39
|
-
|
40
|
-
# as Ohm or ActiveRecord.
|
41
|
-
class Article < Sequel::Model
|
42
|
-
|
43
|
-
# Whitelist for mass assigned attributes.
|
44
|
-
set_allowed_columns :title, :body, :state
|
45
|
-
|
46
|
-
# Validations for all contexts.
|
47
|
-
def validate
|
48
|
-
validates_presence :title
|
49
|
-
validates_presence :body
|
50
|
-
validates_presence :state
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
title = "Bartleby, the Scrivener"
|
55
|
-
body = "I am a rather elderly man..."
|
56
|
-
|
57
|
-
# When using the model...
|
58
|
-
article = Article.new(title: title, body: body)
|
59
|
-
|
60
|
-
article.valid? #=> false
|
61
|
-
article.errors[:state] #=> [:not_present]
|
62
|
-
```
|
63
|
-
|
64
|
-
Of course, what you would do instead is declare `:title` and `:body` as allowed
|
65
|
-
columns, then assign `:state` using the attribute accessor. The reason for this
|
66
|
-
example is to show how you need to work around the fact that there's a single
|
67
|
-
declaration for allowed columns and validations, which in many cases is a great
|
68
|
-
feature and in others is a minor obstacle.
|
69
|
-
|
70
|
-
Now see what happens with Scrivener:
|
71
|
-
|
72
|
-
```ruby
|
73
|
-
# Now the model has no validations or whitelists. It may still have schema
|
74
|
-
# constraints, which is a good practice to enforce data integrity.
|
75
|
-
class Article < Sequel::Model
|
76
|
-
end
|
77
|
-
|
78
|
-
# The attribute accessors are the only fields that will be set. If more
|
79
|
-
# fields are sent when using mass assignment, a NoMethodError exception is
|
80
|
-
# raised.
|
81
|
-
#
|
82
|
-
# Note how in this example we don't accept the status attribute.
|
83
|
-
class Edit < Scrivener
|
20
|
+
class CreateBlogPost < Scrivener
|
84
21
|
attr_accessor :title
|
85
22
|
attr_accessor :body
|
86
23
|
|
@@ -89,40 +26,42 @@ class Edit < Scrivener
|
|
89
26
|
assert_present :body
|
90
27
|
end
|
91
28
|
end
|
29
|
+
```
|
92
30
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
article = Article.new(edit.attributes)
|
97
|
-
article.save
|
98
|
-
|
99
|
-
# And now we only ask for the status.
|
100
|
-
class Publish < Scrivener
|
101
|
-
attr_accessor :status
|
31
|
+
In order to use it, you have to create an instance of `CreateBlogPost`
|
32
|
+
by passing a hash with the attributes `title` and `body` and their
|
33
|
+
corresponding values:
|
102
34
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
35
|
+
```ruby
|
36
|
+
params = {
|
37
|
+
title: "Bartleby",
|
38
|
+
body: "I am a rather elderly man..."
|
39
|
+
}
|
107
40
|
|
108
|
-
|
109
|
-
|
41
|
+
filter = CreateBlogPost.new(params)
|
42
|
+
```
|
110
43
|
|
111
|
-
|
44
|
+
Now you can run the validations by calling `filter.valid?`, and you
|
45
|
+
can retrieve the attributes by calling `filter.attributes`. If the
|
46
|
+
validation fails, a hash of attributes and error codes will be
|
47
|
+
available by calling `filter.errors`. For example:
|
112
48
|
|
113
|
-
|
114
|
-
|
115
|
-
|
49
|
+
```ruby
|
50
|
+
if filter.valid?
|
51
|
+
puts filter.attributes
|
52
|
+
else
|
53
|
+
puts filter.errors
|
54
|
+
end
|
116
55
|
```
|
117
56
|
|
118
|
-
|
119
|
-
|
57
|
+
For now, we are just printing the attributes and the list of errors,
|
58
|
+
but often you will use the attributes to create an instance of a
|
59
|
+
model, and you will display the error messages in a view.
|
120
60
|
|
121
|
-
|
122
|
-
you can fetch just the ones you need. For example:
|
61
|
+
Let's consider the case of creating a new user:
|
123
62
|
|
124
63
|
```ruby
|
125
|
-
class
|
64
|
+
class CreateUser < Scrivener
|
126
65
|
attr_accessor :email
|
127
66
|
attr_accessor :password
|
128
67
|
attr_accessor :password_confirmation
|
@@ -135,33 +74,75 @@ class SignUp < Scrivener
|
|
135
74
|
end
|
136
75
|
end
|
137
76
|
end
|
77
|
+
```
|
78
|
+
|
79
|
+
The filter looks very similar, but as you can see the validations
|
80
|
+
return booleans, thus they can be nested. In this example, we don't
|
81
|
+
want to bother asserting if the password and the password confirmation
|
82
|
+
are equal if the password was not provided.
|
83
|
+
|
84
|
+
Let's instantiate the filter:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
params = {
|
88
|
+
email: "info@example.com",
|
89
|
+
password: "monkey",
|
90
|
+
password_confirmation: "monkey"
|
91
|
+
}
|
138
92
|
|
139
|
-
filter =
|
140
|
-
|
141
|
-
password_confirmation: "monkey")
|
93
|
+
filter = CreateUser.new(params)
|
94
|
+
```
|
142
95
|
|
96
|
+
If the validation succeeds, we only need email and password to
|
97
|
+
create a new user, and we can discard the password_confirmation.
|
98
|
+
The `filter.slice` method receives a list of attributes and returns
|
99
|
+
the attributes hash with any other attributes removed. In this
|
100
|
+
example, the hash returned by `filter.slice` will contain only the
|
101
|
+
`email` and `password` fields:
|
143
102
|
|
144
|
-
|
145
|
-
# create a new user, and we can discard the password_confirmation.
|
103
|
+
```ruby
|
146
104
|
if filter.valid?
|
147
105
|
User.create(filter.slice(:email, :password))
|
148
106
|
end
|
149
107
|
```
|
150
108
|
|
151
|
-
|
152
|
-
|
109
|
+
Sometimes we might want to use parameters from the outside for validation,
|
110
|
+
but don't want the validator to treat them as attributes. In that case we
|
111
|
+
can pass arguments to `#valid?`, and they will be forwarded to `#validate`.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
class CreateComment < Scrivener
|
115
|
+
attr_accessor :content
|
116
|
+
attr_accessor :article_id
|
117
|
+
|
118
|
+
def validate(available_articles:)
|
119
|
+
assert_present :content
|
120
|
+
assert_member :article_id, available_articles.map(&:id)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
params = {
|
127
|
+
content: "this is a comment",
|
128
|
+
article_id: 57,
|
129
|
+
}
|
130
|
+
|
131
|
+
filter = CreateComment.new(params)
|
132
|
+
|
133
|
+
filter.valid?(available_articles: user.articles)
|
134
|
+
```
|
153
135
|
|
154
136
|
Assertions
|
155
137
|
-----------
|
156
138
|
|
157
|
-
Scrivener ships with some basic assertions.
|
158
|
-
for each of them:
|
139
|
+
Scrivener ships with some basic assertions.
|
159
140
|
|
160
141
|
### assert
|
161
142
|
|
162
143
|
The `assert` method is used by all the other assertions. It pushes the
|
163
144
|
second parameter to the list of errors if the first parameter evaluates
|
164
|
-
to false
|
145
|
+
to `false` or `nil`.
|
165
146
|
|
166
147
|
``` ruby
|
167
148
|
def assert(value, error)
|
@@ -169,10 +150,23 @@ def assert(value, error)
|
|
169
150
|
end
|
170
151
|
```
|
171
152
|
|
153
|
+
New assertions can be built upon existing ones. For example, let's
|
154
|
+
define an assertion for positive numbers:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
def assert_positive(att, error = [att, :not_positive])
|
158
|
+
assert(send(att) > 0, error)
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
This assertion calls `assert` and passes both the result of evaluating
|
163
|
+
`send(att) > 0` and the array with the attribute and the error code.
|
164
|
+
All assertions respect this API.
|
165
|
+
|
172
166
|
### assert_present
|
173
167
|
|
174
|
-
Checks that the given field is not nil or empty. The error code for
|
175
|
-
assertion is `:not_present`.
|
168
|
+
Checks that the given field is not nil or empty. The error code for
|
169
|
+
this assertion is `:not_present`.
|
176
170
|
|
177
171
|
### assert_equal
|
178
172
|
|
@@ -237,6 +231,36 @@ of the word. Valid numbers are: 0.1, .1, 1, 1.1, 3.14159, etc.
|
|
237
231
|
|
238
232
|
The error code for this assertion is `:not_decimal`.
|
239
233
|
|
234
|
+
Motivation
|
235
|
+
----------
|
236
|
+
|
237
|
+
A model may expose different APIs to satisfy different purposes.
|
238
|
+
For example, the set of validations for a User in a sign up process
|
239
|
+
may not be the same as the one exposed to an Admin when editing a
|
240
|
+
user profile. While you want the User to provide an email, a password
|
241
|
+
and a password confirmation, you probably don't want the admin to
|
242
|
+
mess with those attributes at all.
|
243
|
+
|
244
|
+
In a wizard, different model states ask for different validations,
|
245
|
+
and a single set of validations for the whole process is not the
|
246
|
+
best solution.
|
247
|
+
|
248
|
+
This library exists to satisfy the need for extracting
|
249
|
+
[Ohm](http://ohm.keyvalue.org)'s validations for reuse in other
|
250
|
+
scenarios.
|
251
|
+
|
252
|
+
Using Scrivener feels very natural no matter what underlying model
|
253
|
+
you are using. As it provides its own validation and whitelisting
|
254
|
+
features, you can choose to ignore those that come bundled with
|
255
|
+
ORMs.
|
256
|
+
|
257
|
+
See also
|
258
|
+
--------
|
259
|
+
|
260
|
+
Scrivener is [Bureaucrat](https://github.com/tizoc/bureaucrat)'s
|
261
|
+
little brother. It draws all the inspiration from it and its features
|
262
|
+
are a subset of Bureaucrat's.
|
263
|
+
|
240
264
|
Installation
|
241
265
|
------------
|
242
266
|
|
data/lib/scrivener.rb
CHANGED
@@ -67,14 +67,14 @@ class Scrivener
|
|
67
67
|
# end
|
68
68
|
# end
|
69
69
|
#
|
70
|
-
def valid?
|
70
|
+
def valid?(*args)
|
71
71
|
errors.clear
|
72
|
-
validate
|
72
|
+
validate(*args)
|
73
73
|
errors.empty?
|
74
74
|
end
|
75
75
|
|
76
76
|
# Base validate implementation. Override this method in subclasses.
|
77
|
-
def validate
|
77
|
+
def validate(*args)
|
78
78
|
end
|
79
79
|
|
80
80
|
# Hash of errors for each attribute in this model.
|
data/test/scrivener_test.rb
CHANGED
@@ -207,61 +207,97 @@ end
|
|
207
207
|
|
208
208
|
class H < Scrivener
|
209
209
|
attr_accessor :a
|
210
|
+
attr_accessor :b
|
210
211
|
|
211
212
|
def validate
|
212
|
-
|
213
|
+
assert_equal :a, "foo"
|
214
|
+
assert_equal :b, Integer
|
213
215
|
end
|
214
216
|
end
|
215
217
|
|
216
218
|
scope do
|
217
|
-
test "
|
219
|
+
test "equality validation" do
|
218
220
|
filter = H.new({})
|
219
221
|
|
220
222
|
assert ! filter.valid?
|
221
|
-
assert filter.errors[:a].include?(:
|
223
|
+
assert filter.errors[:a].include?(:not_equal)
|
224
|
+
assert filter.errors[:b].include?(:not_equal)
|
222
225
|
|
223
|
-
filter = H.new(a: "
|
226
|
+
filter = H.new(a: "foo", b: "bar")
|
224
227
|
assert ! filter.valid?
|
225
|
-
assert filter.errors[:a].include?(:not_in_range)
|
226
228
|
|
227
|
-
filter = H.new(a: "
|
229
|
+
filter = H.new(a: "foo")
|
228
230
|
assert ! filter.valid?
|
229
|
-
assert filter.errors[:a].
|
231
|
+
assert filter.errors[:a].empty?
|
232
|
+
assert filter.errors[:b].include?(:not_equal)
|
230
233
|
|
231
|
-
filter = H.new(a: "foo")
|
234
|
+
filter = H.new(a: "foo", b: 42)
|
235
|
+
filter.valid?
|
232
236
|
assert filter.valid?
|
233
237
|
end
|
234
238
|
end
|
235
239
|
|
236
|
-
class
|
237
|
-
|
238
|
-
|
240
|
+
class Scrivener
|
241
|
+
def assert_filter(att, filter, error = nil)
|
242
|
+
filter = filter.new(send(att))
|
243
|
+
|
244
|
+
unless filter.valid?
|
245
|
+
assert(false, error || [att, filter.errors])
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
239
249
|
|
250
|
+
class I < Scrivener
|
251
|
+
attr_accessor :name
|
252
|
+
|
240
253
|
def validate
|
241
|
-
assert_equal :
|
242
|
-
assert_equal :b, Fixnum
|
254
|
+
assert_equal :name, "I"
|
243
255
|
end
|
244
256
|
end
|
245
257
|
|
258
|
+
class J < Scrivener
|
259
|
+
attr_accessor :name
|
260
|
+
attr_accessor :i
|
261
|
+
|
262
|
+
def validate
|
263
|
+
assert_equal :name, "J"
|
264
|
+
assert_filter :i, I
|
265
|
+
end
|
266
|
+
end
|
246
267
|
|
247
268
|
scope do
|
248
|
-
test "
|
249
|
-
|
269
|
+
test "nested filters" do
|
270
|
+
j1 = J.new(name: "J", i: { name: "I" })
|
271
|
+
j2 = J.new(name: "J", i: { name: "H" })
|
250
272
|
|
251
|
-
|
252
|
-
|
253
|
-
assert filter.errors[:b].include?(:not_equal)
|
273
|
+
assert_equal true, j1.valid?
|
274
|
+
assert_equal false, j2.valid?
|
254
275
|
|
255
|
-
|
256
|
-
|
276
|
+
errors = {
|
277
|
+
i: [{ name: [:not_equal] }]
|
278
|
+
}
|
257
279
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
assert filter.errors[:b].include?(:not_equal)
|
280
|
+
assert_equal errors, j2.errors
|
281
|
+
end
|
282
|
+
end
|
262
283
|
|
263
|
-
|
264
|
-
|
265
|
-
assert
|
284
|
+
class K < Scrivener
|
285
|
+
def validate(argument)
|
286
|
+
assert argument == "K", [:k, :not_valid]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
scope do
|
291
|
+
test "passing arguments" do
|
292
|
+
k = K.new({})
|
293
|
+
|
294
|
+
assert_equal true, k.valid?("K")
|
295
|
+
assert_equal false, k.valid?("L")
|
296
|
+
|
297
|
+
errors = {
|
298
|
+
k: [:not_valid]
|
299
|
+
}
|
300
|
+
|
301
|
+
assert_equal errors, k.errors
|
266
302
|
end
|
267
303
|
end
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scrivener
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michel Martens
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-11-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cutest
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
description: Scrivener removes the validation responsibility from models and acts
|
@@ -32,10 +32,11 @@ executables: []
|
|
32
32
|
extensions: []
|
33
33
|
extra_rdoc_files: []
|
34
34
|
files:
|
35
|
-
- .gems
|
36
|
-
- .gitignore
|
35
|
+
- ".gems"
|
36
|
+
- ".gitignore"
|
37
37
|
- AUTHORS
|
38
38
|
- CHANGELOG.md
|
39
|
+
- CONTRIBUTING
|
39
40
|
- LICENSE
|
40
41
|
- README.md
|
41
42
|
- lib/scrivener.rb
|
@@ -53,17 +54,17 @@ require_paths:
|
|
53
54
|
- lib
|
54
55
|
required_ruby_version: !ruby/object:Gem::Requirement
|
55
56
|
requirements:
|
56
|
-
- -
|
57
|
+
- - ">="
|
57
58
|
- !ruby/object:Gem::Version
|
58
59
|
version: '0'
|
59
60
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
61
|
requirements:
|
61
|
-
- -
|
62
|
+
- - ">="
|
62
63
|
- !ruby/object:Gem::Version
|
63
64
|
version: '0'
|
64
65
|
requirements: []
|
65
66
|
rubyforge_project:
|
66
|
-
rubygems_version: 2.
|
67
|
+
rubygems_version: 2.7.6
|
67
68
|
signing_key:
|
68
69
|
specification_version: 4
|
69
70
|
summary: Validation frontend for models.
|