opal-scrivener 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 69be06e09445d99a285f4672cb17347b7cdfeea4
4
+ data.tar.gz: b2c8e116c64a665ece1a5e67177f1fb6e6b9193d
5
+ SHA512:
6
+ metadata.gz: f2762dafc01d96c4651566a2a73fa2e25aea7a6497bd96a67692671741ceab402de649d8bea97fdf0faa58207c57fb28b8f47f84f007c084a3b43f9b2cf057e5
7
+ data.tar.gz: 53bde447f9322660cee1b50ea1c18b9fd9411eeaf17e73f41f819985de1c9a2fa19674d83742ff676bfbaf920eb0c58f33324aa8daaebd19167335714758ebaf
data/.bundle/config ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_DISABLE_SHARED_GEMS: '1'
data/.gems ADDED
@@ -0,0 +1 @@
1
+ cutest -v 1.2.2
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ /pkg
data/AUTHORS ADDED
@@ -0,0 +1,3 @@
1
+ * Cyril David (cyx)
2
+ * Lucas Nasif (ehqhvm)
3
+ * Michel Martens (soveran)
data/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ ## 1.0.0
2
+
3
+ * Extra attributes are ignored.
4
+
5
+ ```ruby
6
+ # Before:
7
+
8
+ publish = Publish.new(status: "published", title: "foo")
9
+ publis.attributes # => { :status => "published" }
10
+ # => NoMethodError: undefined method `title=' for #<Publish...>
11
+
12
+ # Now:
13
+
14
+ # Extra fields are discarded
15
+ publish = Publish.new(status: "published", title: "foo")
16
+ publish.attributes # => { :status => "published" }
17
+ ```
18
+
19
+ ## 0.4.1
20
+
21
+ * Fix creation of symbols for extra attributes.
22
+
23
+ ## 0.4.0
24
+
25
+ * Fix `assert_email` and `assert_url` to support longer tld's.
26
+
27
+ ## 0.3.0
28
+
29
+ * Add support for negative numbers.
30
+
31
+ ## 0.2.0
32
+
33
+ * Add `assert_equal` validation.
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 ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011-2015 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,243 @@
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.
27
+
28
+ Usage
29
+ -----
30
+
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:
37
+
38
+ ```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
84
+ attr_accessor :title
85
+ attr_accessor :body
86
+
87
+ def validate
88
+ assert_present :title
89
+ assert_present :body
90
+ end
91
+ end
92
+
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
102
+
103
+ def validate
104
+ assert_format :status, /^(published|draft)$/
105
+ end
106
+ end
107
+
108
+ publish = Publish.new(status: "published")
109
+ publish.valid? #=> true
110
+
111
+ article.update_attributes(publish.attributes)
112
+
113
+ # Extra fields are discarded
114
+ publish = Publish.new(status: "published", title: "foo")
115
+ publish.attributes #=> { :status => "published" }
116
+ ```
117
+
118
+ Slices
119
+ ------
120
+
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:
123
+
124
+ ```ruby
125
+ class SignUp < Scrivener
126
+ attr_accessor :email
127
+ attr_accessor :password
128
+ attr_accessor :password_confirmation
129
+
130
+ def validate
131
+ assert_email :email
132
+
133
+ if assert_present :password
134
+ assert_equal :password, password_confirmation
135
+ end
136
+ end
137
+ end
138
+
139
+ filter = SignUp.new(email: "info@example.com",
140
+ password: "monkey",
141
+ password_confirmation: "monkey")
142
+
143
+
144
+ # If the validation succeeds, we only need email and password to
145
+ # create a new user, and we can discard the password_confirmation.
146
+ if filter.valid?
147
+ User.create(filter.slice(:email, :password))
148
+ end
149
+ ```
150
+
151
+ By calling `slice` with a list of attributes, you get a hash with only
152
+ those key/value pairs.
153
+
154
+ Assertions
155
+ -----------
156
+
157
+ Scrivener ships with some basic assertions. The following is a brief description
158
+ for each of them:
159
+
160
+ ### assert
161
+
162
+ The `assert` method is used by all the other assertions. It pushes the
163
+ second parameter to the list of errors if the first parameter evaluates
164
+ to false.
165
+
166
+ ``` ruby
167
+ def assert(value, error)
168
+ value or errors[error.first].push(error.last) && false
169
+ end
170
+ ```
171
+
172
+ ### assert_present
173
+
174
+ Checks that the given field is not nil or empty. The error code for this
175
+ assertion is `:not_present`.
176
+
177
+ ### assert_equal
178
+
179
+ Check that the attribute has the expected value. It uses === for
180
+ comparison, so type checks are possible too. Note that in order to
181
+ make the case equality work, the check inverts the order of the
182
+ arguments: `assert_equal :foo, Bar` is translated to the expression
183
+ `Bar === send(:foo)`.
184
+
185
+ ### assert_format
186
+
187
+ Checks that the given field matches the provided regular expression.
188
+ The error code for this assertion is `:format`.
189
+
190
+ ### assert_numeric
191
+
192
+ Checks that the given field holds a number as a Fixnum or as a string
193
+ representation. The error code for this assertion is `:not_numeric`.
194
+
195
+ ### assert_url
196
+
197
+ Provides a pretty general URL regular expression match. An important
198
+ point to make is that this assumes that the URL should start with
199
+ `http://` or `https://`. The error code for this assertion is
200
+ `:not_url`.
201
+
202
+ ### assert_email
203
+
204
+ In this current day and age, almost all web applications need to
205
+ validate an email address. This pretty much matches 99% of the emails
206
+ out there. The error code for this assertion is `:not_email`.
207
+
208
+ ### assert_member
209
+
210
+ Checks that a given field is contained within a set of values (i.e.
211
+ like an `ENUM`).
212
+
213
+ ``` ruby
214
+ def validate
215
+ assert_member :state, %w{pending paid delivered}
216
+ end
217
+ ```
218
+
219
+ The error code for this assertion is `:not_valid`
220
+
221
+ ### assert_length
222
+
223
+ Checks that a given field's length falls under a specified range.
224
+
225
+ ``` ruby
226
+ def validate
227
+ assert_length :username, 3..20
228
+ end
229
+ ```
230
+
231
+ The error code for this assertion is `:not_in_range`.
232
+
233
+ ### assert_decimal
234
+
235
+ Checks that a given field looks like a number in the human sense
236
+ of the word. Valid numbers are: 0.1, .1, 1, 1.1, 3.14159, etc.
237
+
238
+ The error code for this assertion is `:not_decimal`.
239
+
240
+ Installation
241
+ ------------
242
+
243
+ $ gem install scrivener
@@ -0,0 +1,5 @@
1
+ require 'scrivener'
2
+
3
+ unless RUBY_ENGINE == 'opal'
4
+ Opal.append_path File.expand_path('..', __FILE__).untaint
5
+ end
@@ -0,0 +1,88 @@
1
+ require "time"
2
+ require "date"
3
+ require "bigdecimal"
4
+
5
+ class Scrivener
6
+ # Provides a way to define attributes so they are cast into a corresponding
7
+ # type (defaults to +String+) when getting the attributes.
8
+ #
9
+ # Any object that supports a .call method can be used as a "type". The
10
+ # following are implemented out of the box:
11
+ #
12
+ # - Types::Symbol
13
+ # - Types::String
14
+ # - Types::Integer
15
+ # - Types::Float
16
+ # - Types::Decimal
17
+ # - Types::Date
18
+ # - Types::Time
19
+ # - Types::DateTime
20
+ # - Types::Boolean
21
+ #
22
+ # @example
23
+ #
24
+ # class CreateProduct < Scrivener
25
+ # attribute :name
26
+ # attribute :description
27
+ # attribute :price, Types::Decimal
28
+ # attribute :avaliable_after, Types::Date
29
+ # attribute :stock, Types::Integer
30
+ # end
31
+ #
32
+ # p = CreateProduct.new(name: "Foo", price: "10.0", available_after: "2012-07-10")
33
+ # p.cleaned_price #=> BigDecimal.new("10.0")
34
+ # p.cleaned_available_after #=> Date.new(2012, 7, 10)
35
+ # p.cleaned_name #=> "Foo"
36
+ #
37
+ module Types
38
+ Symbol = ->(value) { value.to_sym }
39
+ String = ->(value) { value.to_s }
40
+ Integer = ->(value) { Integer(value) }
41
+ Float = ->(value) { Float(value) }
42
+ Decimal = ->(value) { BigDecimal(value) }
43
+ Date = ->(value) { ::Date.parse(value.to_s) }
44
+ DateTime = ->(value) { ::DateTime.parse(value.to_s) }
45
+ Time = ->(value) { ::Time.parse(value.to_s) }
46
+ Boolean = ->(value) {
47
+ case value
48
+ when "f", "false", "0"; false
49
+ when "t", "true", "1"; true
50
+ else !!value
51
+ end
52
+ }
53
+
54
+ # Define an attribute with its corresponding type. This is similar to
55
+ # attr_accessor, except the reader method will cast the object into the
56
+ # proper type.
57
+ #
58
+ # If the casting results in a TypeError or ArgumentError, then an error on
59
+ # :typecast will be added to this attribute and the raw attribute will be
60
+ # returned instead.
61
+ #
62
+ # @param [Symbol] name The name of the attribute.
63
+ # @param [#call] type The type of this attribute (defaults to String).
64
+ #
65
+ # @example
66
+ #
67
+ # attribute :foo
68
+ # attribute :foo, Types::String
69
+ # attribute :foo, Types::Date
70
+ #
71
+ # @!macro [attach] attribute
72
+ # @return [$2] the $1 attribute.
73
+ #
74
+ def attribute(name, type=Types::String)
75
+ attr_writer name
76
+
77
+ define_method name do
78
+ begin
79
+ val = instance_variable_get(:"@#{name}")
80
+ val && type.call(val)
81
+ rescue TypeError, ArgumentError
82
+ errors[name].push(:typecast)
83
+ val
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,225 @@
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
+ # * assert_url
11
+ # * assert_email
12
+ # * assert_member
13
+ # * assert_length
14
+ # * assert_decimal
15
+ # * assert_equal
16
+ #
17
+ # The core tenets that Scrivener::Validations advocates can be summed up in a
18
+ # few bullet points:
19
+ #
20
+ # 1. Validations are much simpler and better done using composition rather
21
+ # than macros.
22
+ # 2. Error messages should be kept separate and possibly in the view or
23
+ # presenter layer.
24
+ # 3. It should be easy to write your own validation routine.
25
+ #
26
+ # Other validations are simply added on a per-model or per-project basis.
27
+ #
28
+ # @example
29
+ #
30
+ # class Quote
31
+ # attr_accessor :title
32
+ # attr_accessor :price
33
+ # attr_accessor :date
34
+ #
35
+ # def validate
36
+ # assert_present :title
37
+ # assert_numeric :price
38
+ # assert_format :date, /\A[\d]{4}-[\d]{1,2}-[\d]{1,2}\z
39
+ # end
40
+ # end
41
+ #
42
+ # s = Quote.new
43
+ # s.valid?
44
+ # # => false
45
+ #
46
+ # s.errors
47
+ # # => { :title => [:not_present],
48
+ # :price => [:not_numeric],
49
+ # :date => [:format] }
50
+ #
51
+ module Validations
52
+
53
+ # Check if the current model state is valid. Each call to {#valid?} will
54
+ # reset the {#errors} array.
55
+ #
56
+ # All validations should be declared in a `validate` method.
57
+ #
58
+ # @example
59
+ #
60
+ # class Login
61
+ # attr_accessor :username
62
+ # attr_accessor :password
63
+ #
64
+ # def validate
65
+ # assert_present :user
66
+ # assert_present :password
67
+ # end
68
+ # end
69
+ #
70
+ def valid?
71
+ errors.clear
72
+ validate
73
+ errors.empty?
74
+ end
75
+
76
+ # Base validate implementation. Override this method in subclasses.
77
+ def validate
78
+ end
79
+
80
+ # Hash of errors for each attribute in this model.
81
+ def errors
82
+ @errors ||= Hash.new { |hash, key| hash[key] = [] }
83
+ end
84
+
85
+ protected
86
+
87
+ # Allows you to do a validation check against a regular expression.
88
+ # It's important to note that this internally calls {#assert_present},
89
+ # therefore you need not structure your regular expression to check
90
+ # for a non-empty value.
91
+ #
92
+ # @param [Symbol] att The attribute you want to verify the format of.
93
+ # @param [Regexp] format The regular expression with which to compare
94
+ # the value of att with.
95
+ # @param [Array<Symbol, Symbol>] error The error that should be returned
96
+ # when the validation fails.
97
+ def assert_format(att, format, error = [att, :format])
98
+ if assert_present(att, error)
99
+ assert(send(att).to_s.match(format), error)
100
+ end
101
+ end
102
+
103
+ # The most basic and highly useful assertion. Simply checks if the
104
+ # value of the attribute is empty.
105
+ #
106
+ # @param [Symbol] att The attribute you wish to verify the presence of.
107
+ # @param [Array<Symbol, Symbol>] error The error that should be returned
108
+ # when the validation fails.
109
+ def assert_present(att, error = [att, :not_present])
110
+ assert(!send(att).to_s.empty?, error)
111
+ end
112
+
113
+ # Checks if all the characters of an attribute is a digit.
114
+ #
115
+ # @param [Symbol] att The attribute you wish to verify the numeric format.
116
+ # @param [Array<Symbol, Symbol>] error The error that should be returned
117
+ # when the validation fails.
118
+ def assert_numeric(att, error = [att, :not_numeric])
119
+ if assert_present(att, error)
120
+ value = send(att)
121
+
122
+ if RUBY_ENGINE == 'opal'
123
+ assert(value.respond_to?(:to_int) || value.to_s.match(/^\-?\d+$/), error)
124
+ else
125
+ assert(value.respond_to?(:to_int) || value.to_s.match(/\A\-?\d+\z/), error)
126
+ end
127
+ end
128
+ end
129
+
130
+ if RUBY_ENGINE == 'opal'
131
+ URL = /^(http|https):\/\/([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,12}|(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|localhost)(:[0-9]{1,5})?(\/.*)?$/i
132
+ else
133
+ URL = /\A(http|https):\/\/([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,12}|(2
134
+ 5[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}
135
+ |localhost)(:[0-9]{1,5})?(\/.*)?\z/ix
136
+ end
137
+
138
+ def assert_url(att, error = [att, :not_url])
139
+ if assert_present(att, error)
140
+ assert_format(att, URL, error)
141
+ end
142
+ end
143
+
144
+
145
+ if RUBY_ENGINE == 'opal'
146
+ EMAIL = /^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,12})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i
147
+ else
148
+ EMAIL = /\A([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*
149
+ [\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@
150
+ ((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+
151
+ [a-z]{2,12})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)\z/ix
152
+ end
153
+
154
+ def assert_email(att, error = [att, :not_email])
155
+ if assert_present(att, error)
156
+ assert_format(att, EMAIL, error)
157
+ end
158
+ end
159
+
160
+ def assert_member(att, set, err = [att, :not_valid])
161
+ assert(set.include?(send(att)), err)
162
+ end
163
+
164
+ def assert_length(att, range, error = [att, :not_in_range])
165
+ if assert_present(att, error)
166
+ val = send(att).to_s
167
+ assert range.include?(val.length), error
168
+ end
169
+ end
170
+
171
+ if RUBY_ENGINE == 'opal'
172
+ DECIMAL = /^\-?(\d+)?(\.\d+(E\d+)?)$/
173
+ else
174
+ DECIMAL = /\A\-?(\d+)?(\.\d+(E\d+)?)?\z/
175
+ end
176
+
177
+ def assert_decimal(att, error = [att, :not_decimal])
178
+ assert_format att, DECIMAL, error
179
+ end
180
+
181
+ # Check that the attribute has the expected value. It uses === for
182
+ # comparison, so type checks are possible too. Note that in order
183
+ # to make the case equality work, the check inverts the order of
184
+ # the arguments: `assert_equal :foo, Bar` is translated to the
185
+ # expression `Bar === send(:foo)`.
186
+ #
187
+ # @example
188
+ #
189
+ # def validate
190
+ # assert_equal :status, "pending"
191
+ # assert_equal :quantity, Fixnum
192
+ # end
193
+ #
194
+ # @param [Symbol] att The attribute you wish to verify for equality.
195
+ # @param [Object] value The value you want to test against.
196
+ # @param [Array<Symbol, Symbol>] error The error that should be returned
197
+ # when the validation fails.
198
+ def assert_equal(att, value, error = [att, :not_equal])
199
+ assert value === send(att), error
200
+ end
201
+
202
+ # The grand daddy of all assertions. If you want to build custom
203
+ # assertions, or even quick and dirty ones, you can simply use this method.
204
+ #
205
+ # @example
206
+ #
207
+ # class CreatePost
208
+ # attr_accessor :slug
209
+ # attr_accessor :votes
210
+ #
211
+ # def validate
212
+ # assert_slug :slug
213
+ # assert votes.to_i > 0, [:votes, :not_valid]
214
+ # end
215
+ #
216
+ # protected
217
+ # def assert_slug(att, error = [att, :not_slug])
218
+ # assert send(att).to_s =~ /\A[a-z\-0-9]+\z/, error
219
+ # end
220
+ # end
221
+ def assert(value, error)
222
+ value or errors[error.first].push(error.last) && false
223
+ end
224
+ end
225
+ end
data/lib/scrivener.rb ADDED
@@ -0,0 +1,74 @@
1
+ require_relative "scrivener/validations"
2
+ require_relative "scrivener/types"
3
+
4
+ class Scrivener
5
+ VERSION = "1.0.0"
6
+
7
+ include Validations
8
+ extend Types
9
+
10
+ # Initialize with a hash of attributes and values.
11
+ # Extra attributes are discarded.
12
+ #
13
+ # @example
14
+ #
15
+ # class EditPost < Scrivener
16
+ # attr_accessor :title
17
+ # attr_accessor :body
18
+ #
19
+ # def validate
20
+ # assert_present :title
21
+ # assert_present :body
22
+ # end
23
+ # end
24
+ #
25
+ # edit = EditPost.new(title: "Software Tools")
26
+ #
27
+ # edit.valid? #=> false
28
+ #
29
+ # edit.errors[:title] #=> []
30
+ # edit.errors[:body] #=> [:not_present]
31
+ #
32
+ # edit.body = "Recommended reading..."
33
+ #
34
+ # edit.valid? #=> true
35
+ #
36
+ # # Now it's safe to initialize the model.
37
+ # post = Post.new(edit.attributes)
38
+ # post.save
39
+ def initialize(atts)
40
+ @_accessors = atts.keys.map { |key| "#{key}=".to_sym }
41
+
42
+ atts.each do |key, val|
43
+ accessor = "#{key}="
44
+
45
+ if respond_to?(accessor)
46
+ send(accessor, val)
47
+ end
48
+ end
49
+ end
50
+
51
+ def _accessors
52
+ @_accessors & public_methods(false).select do |name|
53
+ name[-1] == "="
54
+ end
55
+ end
56
+
57
+ # Return hash of attributes and values.
58
+ def attributes
59
+ Hash.new.tap do |atts|
60
+ _accessors.each do |accessor|
61
+ att = accessor[0..-2].to_sym
62
+ atts[att] = send(att)
63
+ end
64
+ end
65
+ end
66
+
67
+ def slice(*keys)
68
+ Hash.new.tap do |atts|
69
+ keys.each do |att|
70
+ atts[att] = send(att)
71
+ end
72
+ end
73
+ end
74
+ end
data/makefile ADDED
@@ -0,0 +1,4 @@
1
+ .PHONY: test
2
+
3
+ test:
4
+ cutest test/**/*.rb
@@ -0,0 +1,16 @@
1
+ require "./lib/scrivener"
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "opal-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. Works with Opal."
8
+ s.authors = ["Michel Martens", "CJ Lazell"]
9
+ s.email = ["michel@soveran.com", "cjlazell@gmail.com"]
10
+ s.homepage = "http://github.com/cj/scrivener"
11
+ s.license = "MIT"
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+
15
+ s.add_development_dependency "cutest"
16
+ end
@@ -0,0 +1,317 @@
1
+ require File.expand_path("../lib/scrivener", File.dirname(__FILE__))
2
+
3
+ class A < Scrivener
4
+ attr_accessor :a
5
+ attr_accessor :b
6
+ end
7
+
8
+ scope do
9
+ test "ignore extra fields" do
10
+ atts = { :a => 1, :b => 2, :c => 3 }
11
+
12
+ filter = A.new(atts)
13
+
14
+ atts.delete(:c)
15
+
16
+ assert_equal atts, filter.attributes
17
+ end
18
+
19
+ test "not raise when there are less fields" do
20
+ atts = { :a => 1 }
21
+
22
+ assert filter = A.new(atts)
23
+ assert_equal filter.attributes, { :a => 1 }
24
+ end
25
+
26
+ test "return attributes" do
27
+ atts = { :a => 1, :b => 2 }
28
+
29
+ filter = A.new(atts)
30
+
31
+ assert_equal atts, filter.attributes
32
+ end
33
+
34
+ test "return only the required attributes" do
35
+ atts = { :a => 1, :b => 2 }
36
+
37
+ filter = A.new(atts)
38
+
39
+ assert_equal filter.slice(:a), { :a => 1 }
40
+ end
41
+ end
42
+
43
+ class B < Scrivener
44
+ attr_accessor :a
45
+ attr_accessor :b
46
+
47
+ def validate
48
+ assert_present :a
49
+ assert_present :b
50
+ end
51
+ end
52
+
53
+ scope do
54
+ test "validations" do
55
+ atts = { :a => 1, :b => 2 }
56
+
57
+ filter = B.new(atts)
58
+
59
+ assert filter.valid?
60
+ end
61
+
62
+ test "validation errors" do
63
+ atts = { :a => 1 }
64
+
65
+ filter = B.new(atts)
66
+
67
+ assert_equal false, filter.valid?
68
+ assert_equal [], filter.errors[:a]
69
+ assert_equal [:not_present], filter.errors[:b]
70
+ end
71
+
72
+ test "attributes without @errors" do
73
+ atts = { :a => 1, :b => 2 }
74
+
75
+ filter = B.new(atts)
76
+
77
+ filter.valid?
78
+ assert_equal atts, filter.attributes
79
+ end
80
+ end
81
+
82
+ class C
83
+ include Scrivener::Validations
84
+
85
+ attr_accessor :a
86
+
87
+ def validate
88
+ assert_present :a
89
+ end
90
+ end
91
+
92
+ scope do
93
+ test "validations without Scrivener" do
94
+ filter = C.new
95
+ filter.a = 1
96
+ assert filter.valid?
97
+
98
+ filter = C.new
99
+ assert_equal false, filter.valid?
100
+ assert_equal [:not_present], filter.errors[:a]
101
+ end
102
+ end
103
+
104
+ class D < Scrivener
105
+ attr_accessor :url, :email
106
+
107
+ def validate
108
+ assert_url :url
109
+ assert_email :email
110
+ end
111
+ end
112
+
113
+ scope do
114
+ test "email & url" do
115
+ filter = D.new({})
116
+
117
+ assert ! filter.valid?
118
+ assert_equal [:not_url], filter.errors[:url]
119
+ assert_equal [:not_email], filter.errors[:email]
120
+
121
+ filter = D.new(url: "google.com", email: "egoogle.com")
122
+
123
+ assert ! filter.valid?
124
+ assert_equal [:not_url], filter.errors[:url]
125
+ assert_equal [:not_email], filter.errors[:email]
126
+
127
+ filter = D.new(url: "http://google.com", email: "me@google.com")
128
+ assert filter.valid?
129
+
130
+ filter = D.new(url: "http://example.versicherung", email: "me@example.versicherung")
131
+ assert filter.valid?
132
+ end
133
+ end
134
+
135
+ class E < Scrivener
136
+ attr_accessor :a
137
+
138
+ def validate
139
+ assert_length :a, 3..10
140
+ end
141
+ end
142
+
143
+ scope do
144
+ test "length validation" do
145
+ filter = E.new({})
146
+
147
+ assert ! filter.valid?
148
+ assert filter.errors[:a].include?(:not_in_range)
149
+
150
+ filter = E.new(a: "fo")
151
+ assert ! filter.valid?
152
+ assert filter.errors[:a].include?(:not_in_range)
153
+
154
+ filter = E.new(a: "foofoofoofo")
155
+ assert ! filter.valid?
156
+ assert filter.errors[:a].include?(:not_in_range)
157
+
158
+ filter = E.new(a: "foo")
159
+ assert filter.valid?
160
+ end
161
+ end
162
+
163
+ class F < Scrivener
164
+ attr_accessor :status
165
+
166
+ def validate
167
+ assert_member :status, %w{pending paid delivered}
168
+ end
169
+ end
170
+
171
+ scope do
172
+ test "member validation" do
173
+ filter = F.new({})
174
+ assert ! filter.valid?
175
+ assert_equal [:not_valid], filter.errors[:status]
176
+
177
+ filter = F.new(status: "foo")
178
+ assert ! filter.valid?
179
+ assert_equal [:not_valid], filter.errors[:status]
180
+
181
+ %w{pending paid delivered}.each do |status|
182
+ filter = F.new(status: status)
183
+ assert filter.valid?
184
+ end
185
+ end
186
+ end
187
+
188
+ class G < Scrivener
189
+ attr_accessor :a
190
+
191
+ def validate
192
+ assert_decimal :a
193
+ end
194
+ end
195
+
196
+ scope do
197
+ test "decimal validation" do
198
+ filter = G.new({})
199
+ assert ! filter.valid?
200
+ assert_equal [:not_decimal], filter.errors[:a]
201
+
202
+ %w{10 10.1 10.100000 0.100000 .1000 -10}.each do |a|
203
+ filter = G.new(a: a)
204
+ assert filter.valid?
205
+ end
206
+ end
207
+ end
208
+
209
+ class H < Scrivener
210
+ attr_accessor :a
211
+
212
+ def validate
213
+ assert_length :a, 3..10
214
+ end
215
+ end
216
+
217
+ scope do
218
+ test "length validation" do
219
+ filter = H.new({})
220
+
221
+ assert ! filter.valid?
222
+ assert filter.errors[:a].include?(:not_in_range)
223
+
224
+ filter = H.new(a: "fo")
225
+ assert ! filter.valid?
226
+ assert filter.errors[:a].include?(:not_in_range)
227
+
228
+ filter = H.new(a: "foofoofoofo")
229
+ assert ! filter.valid?
230
+ assert filter.errors[:a].include?(:not_in_range)
231
+
232
+ filter = H.new(a: "foo")
233
+ assert filter.valid?
234
+ end
235
+ end
236
+
237
+ class I < Scrivener
238
+ attr_accessor :a
239
+ attr_accessor :b
240
+
241
+ def validate
242
+ assert_equal :a, "foo"
243
+ assert_equal :b, Fixnum
244
+ end
245
+ end
246
+
247
+ scope do
248
+ test "equality validation" do
249
+ filter = I.new({})
250
+
251
+ assert ! filter.valid?
252
+ assert filter.errors[:a].include?(:not_equal)
253
+ assert filter.errors[:b].include?(:not_equal)
254
+
255
+ filter = I.new(a: "foo", b: "bar")
256
+ assert ! filter.valid?
257
+
258
+ filter = I.new(a: "foo")
259
+ assert ! filter.valid?
260
+ assert filter.errors[:a].empty?
261
+ assert filter.errors[:b].include?(:not_equal)
262
+
263
+ filter = I.new(a: "foo", b: 42)
264
+ filter.valid?
265
+ assert filter.valid?
266
+ end
267
+ end
268
+
269
+ class Thing < Scrivener
270
+ attribute :name
271
+ attribute :ends_on, Types::Date
272
+ attribute :price, Types::Decimal
273
+
274
+ def validate
275
+ assert_present :ends_on
276
+ end
277
+ end
278
+
279
+ class OtherThing < Scrivener
280
+ attribute :foo, ->(val) { val.to_s.reverse }
281
+ end
282
+
283
+ scope do
284
+ test "casting to types" do
285
+ t = Thing.new(name: "Foo", ends_on: "2012-07-31", price: "20.45")
286
+
287
+ assert_equal "Foo", t.name
288
+ assert_equal Date.new(2012, 7, 31), t.ends_on
289
+ assert_equal BigDecimal("20.45"), t.price
290
+ end
291
+
292
+ test "#attributes includes the parsed objects" do
293
+ t = Thing.new(name: "Foo", ends_on: "2012-07-31", price: "20.45")
294
+
295
+ assert_equal "Foo", t.attributes[:name]
296
+ assert_equal Date.new(2012, 7, 31), t.attributes[:ends_on]
297
+ assert_equal BigDecimal("20.45"), t.attributes[:price]
298
+ end
299
+
300
+ test "casting with a proc" do
301
+ t = OtherThing.new(foo: "simple")
302
+ assert_equal "elpmis", t.foo
303
+ end
304
+
305
+ test "casting invalid values adds an error" do
306
+ t = Thing.new(ends_on: "this is not a date")
307
+
308
+ assert !t.valid?
309
+ assert_equal [:typecast], t.errors[:ends_on]
310
+ assert_equal "this is not a date", t.ends_on
311
+ end
312
+
313
+ test "accepts input with the correct type" do
314
+ t = Thing.new(name: "Foo", ends_on: Date.new(2012, 7, 31), price: BigDecimal("10.00"))
315
+ assert t.valid?
316
+ end
317
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opal-scrivener
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Michel Martens
8
+ - CJ Lazell
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-04-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: cutest
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ description: Scrivener removes the validation responsibility from models and acts
29
+ as a filter for whitelisted attributes. Works with Opal.
30
+ email:
31
+ - michel@soveran.com
32
+ - cjlazell@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - ".bundle/config"
38
+ - ".gems"
39
+ - ".gitignore"
40
+ - AUTHORS
41
+ - CHANGELOG.md
42
+ - CONTRIBUTING
43
+ - LICENSE
44
+ - README.md
45
+ - lib/opal-scrivener.rb
46
+ - lib/scrivener.rb
47
+ - lib/scrivener/types.rb
48
+ - lib/scrivener/validations.rb
49
+ - makefile
50
+ - opal-scrivener.gemspec
51
+ - test/scrivener_test.rb
52
+ homepage: http://github.com/cj/scrivener
53
+ licenses:
54
+ - MIT
55
+ metadata: {}
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 2.4.5.1
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Validation frontend for models.
76
+ test_files: []