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 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.