opal-scrivener 1.0.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 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: []