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 +7 -0
- data/.bundle/config +2 -0
- data/.gems +1 -0
- data/.gitignore +1 -0
- data/AUTHORS +3 -0
- data/CHANGELOG.md +33 -0
- data/CONTRIBUTING +19 -0
- data/LICENSE +19 -0
- data/README.md +243 -0
- data/lib/opal-scrivener.rb +5 -0
- data/lib/scrivener/types.rb +88 -0
- data/lib/scrivener/validations.rb +225 -0
- data/lib/scrivener.rb +74 -0
- data/makefile +4 -0
- data/opal-scrivener.gemspec +16 -0
- data/test/scrivener_test.rb +317 -0
- metadata +76 -0
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
data/.gems
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
cutest -v 1.2.2
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/pkg
|
data/AUTHORS
ADDED
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,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,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: []
|