scrivener 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/AUTHORS ADDED
@@ -0,0 +1,3 @@
1
+ * Cyril David (cyx)
2
+ * Lucas Nasif (ehqhvm)
3
+ * Michel Martens (soveran)
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.
@@ -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
@@ -0,0 +1,6 @@
1
+ task :test do
2
+ require "cutest"
3
+ Cutest.run(Dir["test/*.rb"])
4
+ end
5
+
6
+ task :default => :test
@@ -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
+
@@ -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: []