scrivener 0.0.1
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.
- data/AUTHORS +3 -0
- data/LICENSE +19 -0
- data/README.md +123 -0
- data/Rakefile +6 -0
- data/lib/scrivener.rb +56 -0
- data/lib/scrivener/validations.rb +152 -0
- data/scrivener.gemspec +21 -0
- data/test/scrivener_test.rb +82 -0
- metadata +67 -0
data/AUTHORS
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Michel Martens
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
Scrivener
|
2
|
+
=========
|
3
|
+
|
4
|
+
Validation frontend for models.
|
5
|
+
|
6
|
+
Description
|
7
|
+
-----------
|
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. By doing this, all projects using Ohm::Validations
|
27
|
+
will be able to profit from extra assertions such as those provided by
|
28
|
+
[ohm-contrib](https://github.com/cyx/ohm-contrib).
|
29
|
+
|
30
|
+
Usage
|
31
|
+
-----
|
32
|
+
|
33
|
+
Using Scrivener feels very natural no matter what underlying model you are
|
34
|
+
using. As it provides its own validation and whitelisting features, you can
|
35
|
+
chose to ignore the ones that come bundled with ORMs.
|
36
|
+
|
37
|
+
This short example illustrates how to move the validation and whitelisting
|
38
|
+
responsibilities away from the model and into Scrivener:
|
39
|
+
|
40
|
+
# We use Sequel::Model in this example, but it applies to other ORMs such
|
41
|
+
# as Ohm or ActiveRecord.
|
42
|
+
class Article < Sequel::Model
|
43
|
+
|
44
|
+
# Whitelist for mass assigned attributes.
|
45
|
+
set_allowed_columns :title, :body, :state
|
46
|
+
|
47
|
+
# Validations for all contexts.
|
48
|
+
def validate
|
49
|
+
validates_presence :title
|
50
|
+
validates_presence :body
|
51
|
+
validates_presence :state
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
title = "Bartleby, the Scrivener"
|
56
|
+
body = "I am a rather elderly man..."
|
57
|
+
|
58
|
+
# When using the model...
|
59
|
+
article = Article.new(title: title, body: body)
|
60
|
+
|
61
|
+
article.valid? #=> false
|
62
|
+
article.errors.on(:state) #=> ["cannot be empty"]
|
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 other is a minor obstacle.
|
69
|
+
|
70
|
+
Now see what happens with Scrivener:
|
71
|
+
|
72
|
+
# Now the model has no validations or whitelists. It may still have schema
|
73
|
+
# constraints, which is a good practice to enforce data integrity.
|
74
|
+
class Article < Sequel::Model
|
75
|
+
end
|
76
|
+
|
77
|
+
# The attribute accessors are the only fields that will be set. If more
|
78
|
+
# fields are sent when using mass assignment, a NoMethodError exception is
|
79
|
+
# raised.
|
80
|
+
#
|
81
|
+
# Note how in this example we don't ask the name on signup.
|
82
|
+
class Edit < Scrivener
|
83
|
+
attr_accessor :title
|
84
|
+
attr_accessor :body
|
85
|
+
|
86
|
+
def validate
|
87
|
+
assert_present :title
|
88
|
+
assert_present :body
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
edit = Edit.new(title: title, body: body)
|
93
|
+
edit.valid? #=> true
|
94
|
+
|
95
|
+
article = Article.new(edit.attributes)
|
96
|
+
article.save
|
97
|
+
|
98
|
+
# And now we only ask for the status.
|
99
|
+
class Publish < Scrivener
|
100
|
+
attr_accessor :status
|
101
|
+
|
102
|
+
def validate
|
103
|
+
assert_format /^(published|draft)$/
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
publish = Publish.new(status: "published")
|
108
|
+
publish.valid? #=> true
|
109
|
+
|
110
|
+
article.update_attributes(publish.attributes)
|
111
|
+
|
112
|
+
# If we try to change other fields...
|
113
|
+
publish = Publish.new(status: "published", title: title)
|
114
|
+
#=> NoMethodError: undefined method `title=' for #<Publish...>
|
115
|
+
|
116
|
+
It's important to note that using Scrivener implies a greater risk than using
|
117
|
+
the model validations. Having a central repository of mass assignable
|
118
|
+
attributes and validations is more secure in most scenarios.
|
119
|
+
|
120
|
+
Installation
|
121
|
+
------------
|
122
|
+
|
123
|
+
$ gem install scrivener
|
data/Rakefile
ADDED
data/lib/scrivener.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.expand_path("scrivener/validations", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class Scrivener
|
4
|
+
VERSION = "0.0.1"
|
5
|
+
|
6
|
+
include Validations
|
7
|
+
|
8
|
+
# Initialize with a hash of attributes and values.
|
9
|
+
# If extra attributes are sent, a NoMethodError exception will be raised.
|
10
|
+
#
|
11
|
+
# The grand daddy of all assertions. If you want to build custom
|
12
|
+
# assertions, or even quick and dirty ones, you can simply use this method.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
#
|
16
|
+
# class EditPost < Scrivener
|
17
|
+
# attr_accessor :title
|
18
|
+
# attr_accessor :body
|
19
|
+
#
|
20
|
+
# def validate
|
21
|
+
# assert_present :title
|
22
|
+
# assert_present :body
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# edit = EditPost.new(title: "Software Tools")
|
27
|
+
#
|
28
|
+
# edit.valid? #=> false
|
29
|
+
#
|
30
|
+
# edit.errors[:title] #=> []
|
31
|
+
# edit.errors[:body] #=> [:not_present]
|
32
|
+
#
|
33
|
+
# edit.body = "Recommended reading..."
|
34
|
+
#
|
35
|
+
# edit.valid? #=> true
|
36
|
+
#
|
37
|
+
# # Now it's safe to initialize the model.
|
38
|
+
# post = Post.new(edit.attributes)
|
39
|
+
# post.save
|
40
|
+
def initialize(attrs)
|
41
|
+
attrs.each do |key, val|
|
42
|
+
send(:"#{key}=", val)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return hash of attributes and values.
|
47
|
+
def attributes
|
48
|
+
Hash.new.tap do |atts|
|
49
|
+
instance_variables.each do |ivar|
|
50
|
+
att = ivar.to_s.sub(/@/, "").to_sym
|
51
|
+
atts[att] = send(att)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,152 @@
|
|
1
|
+
class Scrivener
|
2
|
+
|
3
|
+
# Provides a base implementation for extensible validation routines.
|
4
|
+
# {Scrivener::Validations} currently only provides the following assertions:
|
5
|
+
#
|
6
|
+
# * assert
|
7
|
+
# * assert_present
|
8
|
+
# * assert_format
|
9
|
+
# * assert_numeric
|
10
|
+
#
|
11
|
+
# The core tenets that Scrivener::Validations advocates can be summed up in a
|
12
|
+
# few bullet points:
|
13
|
+
#
|
14
|
+
# 1. Validations are much simpler and better done using composition rather
|
15
|
+
# than macros.
|
16
|
+
# 2. Error messages should be kept separate and possibly in the view or
|
17
|
+
# presenter layer.
|
18
|
+
# 3. It should be easy to write your own validation routine.
|
19
|
+
#
|
20
|
+
# Other validations are simply added on a per-model or per-project basis.
|
21
|
+
#
|
22
|
+
# If you want other validations you may want to take a peek at Ohm::Contrib
|
23
|
+
# and all of the validation modules it provides.
|
24
|
+
#
|
25
|
+
# @see http://cyx.github.com/ohm-contrib/doc/Ohm/WebValidations.html
|
26
|
+
# @see http://cyx.github.com/ohm-contrib/doc/Ohm/NumberValidations.html
|
27
|
+
# @see http://cyx.github.com/ohm-contrib/doc/Ohm/ExtraValidations.html
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
#
|
31
|
+
# class Quote
|
32
|
+
# attr_accessor :title
|
33
|
+
# attr_accessor :price
|
34
|
+
# attr_accessor :date
|
35
|
+
#
|
36
|
+
# def validate
|
37
|
+
# assert_present :title
|
38
|
+
# assert_numeric :price
|
39
|
+
# assert_format :date, /\A[\d]{4}-[\d]{1,2}-[\d]{1,2}\z
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# s = Quote.new
|
44
|
+
# s.valid?
|
45
|
+
# # => false
|
46
|
+
#
|
47
|
+
# s.errors
|
48
|
+
# # => { :title => [:not_present],
|
49
|
+
# :price => [:not_numeric],
|
50
|
+
# :date => [:format] }
|
51
|
+
#
|
52
|
+
module Validations
|
53
|
+
|
54
|
+
# Check if the current model state is valid. Each call to {#valid?} will
|
55
|
+
# reset the {#errors} array.
|
56
|
+
#
|
57
|
+
# All validations should be declared in a `validate` method.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
#
|
61
|
+
# class Login
|
62
|
+
# attr_accessor :username
|
63
|
+
# attr_accessor :password
|
64
|
+
#
|
65
|
+
# def validate
|
66
|
+
# assert_present :user
|
67
|
+
# assert_present :password
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
def valid?
|
72
|
+
errors.clear
|
73
|
+
validate
|
74
|
+
errors.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Base validate implementation. Override this method in subclasses.
|
78
|
+
def validate
|
79
|
+
end
|
80
|
+
|
81
|
+
# Hash of errors for each attribute in this model.
|
82
|
+
def errors
|
83
|
+
@errors ||= Hash.new { |hash, key| hash[key] = [] }
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
# Allows you to do a validation check against a regular expression.
|
89
|
+
# It's important to note that this internally calls {#assert_present},
|
90
|
+
# therefore you need not structure your regular expression to check
|
91
|
+
# for a non-empty value.
|
92
|
+
#
|
93
|
+
# @param [Symbol] att The attribute you want to verify the format of.
|
94
|
+
# @param [Regexp] format The regular expression with which to compare
|
95
|
+
# the value of att with.
|
96
|
+
# @param [Array<Symbol, Symbol>] error The error that should be returned
|
97
|
+
# when the validation fails.
|
98
|
+
def assert_format(att, format, error = [att, :format])
|
99
|
+
if assert_present(att, error)
|
100
|
+
assert(send(att).to_s.match(format), error)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# The most basic and highly useful assertion. Simply checks if the
|
105
|
+
# value of the attribute is empty.
|
106
|
+
#
|
107
|
+
# @param [Symbol] att The attribute you wish to verify the presence of.
|
108
|
+
# @param [Array<Symbol, Symbol>] error The error that should be returned
|
109
|
+
# when the validation fails.
|
110
|
+
def assert_present(att, error = [att, :not_present])
|
111
|
+
assert(!send(att).to_s.empty?, error)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Checks if all the characters of an attribute is a digit. If you want to
|
115
|
+
# verify that a value is a decimal, try looking at Ohm::Contrib's
|
116
|
+
# assert_decimal assertion.
|
117
|
+
#
|
118
|
+
# @param [Symbol] att The attribute you wish to verify the numeric format.
|
119
|
+
# @param [Array<Symbol, Symbol>] error The error that should be returned
|
120
|
+
# when the validation fails.
|
121
|
+
# @see http://cyx.github.com/ohm-contrib/doc/Ohm/NumberValidations.html
|
122
|
+
def assert_numeric(att, error = [att, :not_numeric])
|
123
|
+
if assert_present(att, error)
|
124
|
+
assert_format(att, /^\d+$/, error)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# The grand daddy of all assertions. If you want to build custom
|
129
|
+
# assertions, or even quick and dirty ones, you can simply use this method.
|
130
|
+
#
|
131
|
+
# @example
|
132
|
+
#
|
133
|
+
# class CreatePost
|
134
|
+
# attr_accessor :slug
|
135
|
+
# attr_accessor :votes
|
136
|
+
#
|
137
|
+
# def validate
|
138
|
+
# assert_slug :slug
|
139
|
+
# assert votes.to_i > 0, [:votes, :not_valid]
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# protected
|
143
|
+
# def assert_slug(att, error = [att, :not_slug])
|
144
|
+
# assert send(att).to_s =~ /\A[a-z\-0-9]+\z/, error
|
145
|
+
# end
|
146
|
+
# end
|
147
|
+
def assert(value, error)
|
148
|
+
value or errors[error.first].push(error.last) && false
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
data/scrivener.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "./lib/scrivener"
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "scrivener"
|
5
|
+
s.version = Scrivener::VERSION
|
6
|
+
s.summary = "Validation frontend for models."
|
7
|
+
s.description = "Scrivener removes the validation responsibility from models and acts as a filter for whitelisted attributes."
|
8
|
+
s.authors = ["Michel Martens"]
|
9
|
+
s.email = ["michel@soveran.com"]
|
10
|
+
s.homepage = "http://github.com/soveran/scrivener"
|
11
|
+
s.files = Dir[
|
12
|
+
"LICENSE",
|
13
|
+
"AUTHORS",
|
14
|
+
"README.md",
|
15
|
+
"Rakefile",
|
16
|
+
"lib/**/*.rb",
|
17
|
+
"*.gemspec",
|
18
|
+
"test/**/*.rb"
|
19
|
+
]
|
20
|
+
s.add_development_dependency "cutest"
|
21
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.expand_path("../lib/scrivener", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class S < Scrivener
|
4
|
+
attr_accessor :a
|
5
|
+
attr_accessor :b
|
6
|
+
end
|
7
|
+
|
8
|
+
scope do
|
9
|
+
test "raise when there are extra fields" do
|
10
|
+
atts = { :a => 1, :b => 2, :c => 3 }
|
11
|
+
|
12
|
+
assert_raise NoMethodError do
|
13
|
+
s = S.new(atts)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
test "not raise when there are less fields" do
|
18
|
+
atts = { :a => 1 }
|
19
|
+
|
20
|
+
assert s = S.new(atts)
|
21
|
+
end
|
22
|
+
|
23
|
+
test "return attributes" do
|
24
|
+
atts = { :a => 1, :b => 2 }
|
25
|
+
|
26
|
+
s = S.new(atts)
|
27
|
+
|
28
|
+
assert_equal atts, s.attributes
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class T < Scrivener
|
33
|
+
attr_accessor :a
|
34
|
+
attr_accessor :b
|
35
|
+
|
36
|
+
def validate
|
37
|
+
assert_present :a
|
38
|
+
assert_present :b
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
scope do
|
43
|
+
test "validations" do
|
44
|
+
atts = { :a => 1, :b => 2 }
|
45
|
+
|
46
|
+
t = T.new(atts)
|
47
|
+
|
48
|
+
assert t.valid?
|
49
|
+
end
|
50
|
+
|
51
|
+
test "validation errors" do
|
52
|
+
atts = { :a => 1 }
|
53
|
+
|
54
|
+
t = T.new(atts)
|
55
|
+
|
56
|
+
assert_equal false, t.valid?
|
57
|
+
assert_equal [], t.errors[:a]
|
58
|
+
assert_equal [:not_present], t.errors[:b]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Quote
|
63
|
+
include Scrivener::Validations
|
64
|
+
|
65
|
+
attr_accessor :foo
|
66
|
+
|
67
|
+
def validate
|
68
|
+
assert_present :foo
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
scope do
|
73
|
+
test "validations without Scrivener" do
|
74
|
+
q = Quote.new
|
75
|
+
q.foo = 1
|
76
|
+
assert q.valid?
|
77
|
+
|
78
|
+
q = Quote.new
|
79
|
+
assert_equal false, q.valid?
|
80
|
+
assert_equal [:not_present], q.errors[:foo]
|
81
|
+
end
|
82
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scrivener
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michel Martens
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-18 00:00:00.000000000 -03:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: cutest
|
17
|
+
requirement: &2154104200 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2154104200
|
26
|
+
description: Scrivener removes the validation responsibility from models and acts
|
27
|
+
as a filter for whitelisted attributes.
|
28
|
+
email:
|
29
|
+
- michel@soveran.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- LICENSE
|
35
|
+
- AUTHORS
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- lib/scrivener/validations.rb
|
39
|
+
- lib/scrivener.rb
|
40
|
+
- scrivener.gemspec
|
41
|
+
- test/scrivener_test.rb
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://github.com/soveran/scrivener
|
44
|
+
licenses: []
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.6.2
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Validation frontend for models.
|
67
|
+
test_files: []
|