Scrivener
Validation frontend for models.
Description
Scrivener removes the validation responsibility from models and acts as a filter for whitelisted attributes.
A model may expose different APIs to satisfy different purposes. For example, the set of validations for a User in a Sign up process may not be the same as the one exposed to an Admin when editing a user profile. While you want the User to provide an email, a password and a password confirmation, you probably don't want the admin to mess with those attributes at all.
In a wizard, different model states ask for different validations, and a single set of validations for the whole process is not the best solution.
Scrivener is Bureaucrat's little brother. It draws all the inspiration from it and its features are a subset of Bureaucrat's. For a more robust and tested solution, please check it.
This library exists to satify the need of extracting Ohm's validations for reuse in other scenarios.
Usage
Using Scrivener feels very natural no matter what underlying model you are using. As it provides its own validation and whitelisting features, you can choose to ignore the ones that come bundled with ORMs.
This short example illustrates how to move the validation and whitelisting responsibilities away from the model and into Scrivener:
# We use Sequel::Model in this example, but it applies to other ORMs such
# as Ohm or ActiveRecord.
class Article < Sequel::Model
# Whitelist for mass assigned attributes.
set_allowed_columns :title, :body, :state
# Validations for all contexts.
def validate
validates_presence :title
validates_presence :body
validates_presence :state
end
end
title = "Bartleby, the Scrivener"
body = "I am a rather elderly man..."
# When using the model...
article = Article.new(title: title, body: body)
article.valid? #=> false
article.errors[:state] #=> [:not_present]
Of course, what you would do instead is declare :title
and :body
as allowed
columns, then assign :state
using the attribute accessor. The reason for this
example is to show how you need to work around the fact that there's a single
declaration for allowed columns and validations, which in many cases is a great
feature and in others is a minor obstacle.
Now see what happens with Scrivener:
# Now the model has no validations or whitelists. It may still have schema
# constraints, which is a good practice to enforce data integrity.
class Article < Sequel::Model
end
# The attribute accessors are the only fields that will be set. If more
# fields are sent when using mass assignment, a NoMethodError exception is
# raised.
#
# Note how in this example we don't accept the status attribute.
class Edit < Scrivener
attr_accessor :title
attr_accessor :body
def validate
assert_present :title
assert_present :body
end
end
edit = Edit.new(title: title, body: body)
edit.valid? #=> true
article = Article.new(edit.attributes)
article.save
# And now we only ask for the status.
class Publish < Scrivener
attr_accessor :status
def validate
assert_format :status, /^(published|draft)$/
end
end
publish = Publish.new(status: "published")
publish.valid? #=> true
article.update_attributes(publish.attributes)
# Extra fields are discarded
publish = Publish.new(status: "published", title: "foo")
publish.attributes #=> { :status => "published" }
Slices
If you don't need all the attributes after the filtering is done, you can fetch just the ones you need. For example:
class SignUp < Scrivener
attr_accessor :email
attr_accessor :password
attr_accessor :password_confirmation
def validate
assert_email :email
if assert_present :password
assert_equal :password, password_confirmation
end
end
end
filter = SignUp.new(email: "[email protected]",
password: "monkey",
password_confirmation: "monkey")
# If the validation succeeds, we only need email and password to
# create a new user, and we can discard the password_confirmation.
if filter.valid?
User.create(filter.slice(:email, :password))
end
By calling slice
with a list of attributes, you get a hash with only
those key/value pairs.
Assertions
Scrivener ships with some basic assertions. The following is a brief description for each of them:
assert
The assert
method is used by all the other assertions. It pushes the
second parameter to the list of errors if the first parameter evaluates
to false.
def assert(value, error)
value or errors[error.first].push(error.last) && false
end
assert_present
Checks that the given field is not nil or empty. The error code for this
assertion is :not_present
.
assert_equal
Check that the attribute has the expected value. It uses === for
comparison, so type checks are possible too. Note that in order to
make the case equality work, the check inverts the order of the
arguments: assert_equal :foo, Bar
is translated to the expression
Bar === send(:foo)
.
assert_format
Checks that the given field matches the provided regular expression.
The error code for this assertion is :format
.
assert_numeric
Checks that the given field holds a number as a Fixnum or as a string
representation. The error code for this assertion is :not_numeric
.
assert_url
Provides a pretty general URL regular expression match. An important
point to make is that this assumes that the URL should start with
http://
or https://
. The error code for this assertion is
:not_url
.
assert_email
In this current day and age, almost all web applications need to
validate an email address. This pretty much matches 99% of the emails
out there. The error code for this assertion is :not_email
.
assert_member
Checks that a given field is contained within a set of values (i.e.
like an ENUM
).
def validate
assert_member :state, %w{pending paid delivered}
end
The error code for this assertion is :not_valid
assert_length
Checks that a given field's length falls under a specified range.
def validate
assert_length :username, 3..20
end
The error code for this assertion is :not_in_range
.
assert_decimal
Checks that a given field looks like a number in the human sense of the word. Valid numbers are: 0.1, .1, 1, 1.1, 3.14159, etc.
The error code for this assertion is :not_decimal
.
Installation
$ gem install scrivener