Validation frontend for models.
Ruby Makefile
Latest commit dacefda May 4, 2016 @soveran Remove redundant test
Failed to load latest commit information.
lib Release Mar 18, 2015
test Remove redundant test May 4, 2016
LICENSE Update year Mar 19, 2015 Fix typo. Mar 18, 2015
scrivener.gemspec Update gemspec. Mar 17, 2015


Validation frontend for models.


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.


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

title = "Bartleby, the Scrivener"
body  = "I am a rather elderly man..."

# When using the model...
article = 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

# 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

edit = title, body: body)
edit.valid?               #=> true

article =

# And now we only ask for the status.
class Publish < Scrivener
  attr_accessor :status

  def validate
    assert_format :status, /^(published|draft)$/

publish = "published")
publish.valid?            #=> true


# Extra fields are discarded
publish = "published", title: "foo")
publish.attributes #=> { :status => "published" }


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

filter = "",
                    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))

By calling slice with a list of attributes, you get a hash with only those key/value pairs.


Scrivener ships with some basic assertions. The following is a brief description for each of them:


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


Checks that the given field is not nil or empty. The error code for this assertion is :not_present.


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).


Checks that the given field matches the provided regular expression. The error code for this assertion is :format.


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.


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.


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.


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}

The error code for this assertion is :not_valid


Checks that a given field's length falls under a specified range.

def validate
  assert_length :username, 3..20

The error code for this assertion is :not_in_range.


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.


$ gem install scrivener