scrivener 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d856e30f00eb2fbb898b1f436706d513921d7b6b
4
- data.tar.gz: 2cee2565f0507e0ebf8177d2cb57626f683528c1
2
+ SHA256:
3
+ metadata.gz: ca36d57e390df275cd0365e9b8d0b9a8f3d0dda7b00a39aaaf4a7da37a9f0202
4
+ data.tar.gz: b48a3d6ab1a6646fc649d12f501d97b36dfde4729d377848b0b4d97c8147b53b
5
5
  SHA512:
6
- metadata.gz: 068427019df2072dc56517932811d744dd8fcbc4ad9842880382c9e8a3dfc46f118e066d24b73a51364dbbf6dcce42e5ffeb3d1121b714acf61e8066acff9c56
7
- data.tar.gz: c2acd1812ffc52b268d1294c2c744cda6e92f17c5a0011e8777400b39215f276ce5ff43fbae5d1f1e17dcc440aaad160a83b851c224a1bd7b51e62f5bacfe29d
6
+ metadata.gz: cbe35ddb7f28d6cbee5d7f31731af970cef677060f6428f4aec67da1ec11ab11e6f78edb3aee2dfda56b4a950cf9e16a2a6326eb65e5119d3e4fb1bbd615396a
7
+ data.tar.gz: 45e428e8d5204f8493199d020bd5af06fd857cfa42b67aac4b03e078b23db7a47bce1ed7588621198ad5fb797f102aab1178cc7245444a25bdd2cc446ba51f51
@@ -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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Michel Martens
1
+ Copyright (c) 2011-2015 Michel Martens
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
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 acts as a
10
- filter for whitelisted attributes.
11
-
12
- A model may expose different APIs to satisfy different purposes. For example,
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
- Using Scrivener feels very natural no matter what underlying model you are
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
- # We use Sequel::Model in this example, but it applies to other ORMs such
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
- edit = Edit.new(title: title, body: body)
94
- edit.valid? #=> true
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
- def validate
104
- assert_format :status, /^(published|draft)$/
105
- end
106
- end
35
+ ```ruby
36
+ params = {
37
+ title: "Bartleby",
38
+ body: "I am a rather elderly man..."
39
+ }
107
40
 
108
- publish = Publish.new(status: "published")
109
- publish.valid? #=> true
41
+ filter = CreateBlogPost.new(params)
42
+ ```
110
43
 
111
- article.update_attributes(publish.attributes)
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
- # Extra fields are discarded
114
- publish = Publish.new(status: "published", title: "foo")
115
- publish.attributes #=> { :status => "published" }
49
+ ```ruby
50
+ if filter.valid?
51
+ puts filter.attributes
52
+ else
53
+ puts filter.errors
54
+ end
116
55
  ```
117
56
 
118
- Slices
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
- If you don't need all the attributes after the filtering is done,
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 SignUp < Scrivener
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 = SignUp.new(email: "info@example.com",
140
- password: "monkey",
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
- # If the validation succeeds, we only need email and password to
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
- By calling `slice` with a list of attributes, you get a hash with only
152
- those key/value pairs.
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. The following is a brief description
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 this
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
 
@@ -1,7 +1,7 @@
1
1
  require_relative "scrivener/validations"
2
2
 
3
3
  class Scrivener
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
 
6
6
  include Validations
7
7
 
@@ -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.
@@ -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
- assert_length :a, 3..10
213
+ assert_equal :a, "foo"
214
+ assert_equal :b, Integer
213
215
  end
214
216
  end
215
217
 
216
218
  scope do
217
- test "length validation" do
219
+ test "equality validation" do
218
220
  filter = H.new({})
219
221
 
220
222
  assert ! filter.valid?
221
- assert filter.errors[:a].include?(:not_in_range)
223
+ assert filter.errors[:a].include?(:not_equal)
224
+ assert filter.errors[:b].include?(:not_equal)
222
225
 
223
- filter = H.new(a: "fo")
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: "foofoofoofo")
229
+ filter = H.new(a: "foo")
228
230
  assert ! filter.valid?
229
- assert filter.errors[:a].include?(:not_in_range)
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 I < Scrivener
237
- attr_accessor :a
238
- attr_accessor :b
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 :a, "foo"
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 "equality validation" do
249
- filter = I.new({})
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
- assert ! filter.valid?
252
- assert filter.errors[:a].include?(:not_equal)
253
- assert filter.errors[:b].include?(:not_equal)
273
+ assert_equal true, j1.valid?
274
+ assert_equal false, j2.valid?
254
275
 
255
- filter = I.new(a: "foo", b: "bar")
256
- assert ! filter.valid?
276
+ errors = {
277
+ i: [{ name: [:not_equal] }]
278
+ }
257
279
 
258
- filter = I.new(a: "foo")
259
- assert ! filter.valid?
260
- assert filter.errors[:a].empty?
261
- assert filter.errors[:b].include?(:not_equal)
280
+ assert_equal errors, j2.errors
281
+ end
282
+ end
262
283
 
263
- filter = I.new(a: "foo", b: 42)
264
- filter.valid?
265
- assert filter.valid?
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.0.0
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: 2015-03-18 00:00:00.000000000 Z
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.0.14
67
+ rubygems_version: 2.7.6
67
68
  signing_key:
68
69
  specification_version: 4
69
70
  summary: Validation frontend for models.