Giter VIP home page Giter VIP logo

scrivener's Introduction

Scrivener

Validation frontend for models.

Description

Scrivener removes the validation responsibility from models and acts as a filter for whitelisted attributes. Read about the motivation to understand why this separation of concerns is important.

Usage

A very basic example would be creating a blog post:

class CreateBlogPost < Scrivener
  attr_accessor :title
  attr_accessor :body

  def validate
    assert_present :title
    assert_present :body
  end
end

In order to use it, you have to create an instance of CreateBlogPost by passing a hash with the attributes title and body and their corresponding values:

params = {
  title: "Bartleby",
  body: "I am a rather elderly man..."
}

filter = CreateBlogPost.new(params)

Now you can run the validations by calling filter.valid?, and you can retrieve the attributes by calling filter.attributes. If the validation fails, a hash of attributes and error codes will be available by calling filter.errors. For example:

if filter.valid?
  puts filter.attributes
else
  puts filter.errors
end

For now, we are just printing the attributes and the list of errors, but often you will use the attributes to create an instance of a model, and you will display the error messages in a view.

Let's consider the case of creating a new user:

class CreateUser < 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

The filter looks very similar, but as you can see the validations return booleans, thus they can be nested. In this example, we don't want to bother asserting if the password and the password confirmation are equal if the password was not provided.

Let's instantiate the filter:

params = {
  email: "[email protected]",
  password: "monkey",
  password_confirmation: "monkey"
}

filter = CreateUser.new(params)

If the validation succeeds, we only need email and password to create a new user, and we can discard the password_confirmation. The filter.slice method receives a list of attributes and returns the attributes hash with any other attributes removed. In this example, the hash returned by filter.slice will contain only the email and password fields:

if filter.valid?
  User.create(filter.slice(:email, :password))
end

Sometimes we might want to use parameters from the outside for validation, but don't want the validator to treat them as attributes. In that case we can pass arguments to #valid?, and they will be forwarded to #validate.

class CreateComment < Scrivener
  attr_accessor :content
  attr_accessor :article_id

  def validate(available_articles:)
    assert_present :content
    assert_member :article_id, available_articles.map(&:id)
  end
end
params = {
  content:    "this is a comment",
  article_id: 57,
}

filter = CreateComment.new(params)

filter.valid?(available_articles: user.articles)

Assertions

Scrivener ships with some basic assertions.

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 or nil.

def assert(value, error)
   value or errors[error.first].push(error.last) && false
end

New assertions can be built upon existing ones. For example, let's define an assertion for positive numbers:

def assert_positive(att, error = [att, :not_positive])
  assert(send(att) > 0, error)
end

This assertion calls assert and passes both the result of evaluating send(att) > 0 and the array with the attribute and the error code. All assertions respect this API.

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.

Motivation

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.

This library exists to satisfy the need for 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 those that come bundled with ORMs.

See also

Scrivener is Bureaucrat's little brother. It draws all the inspiration from it and its features are a subset of Bureaucrat's.

Installation

$ gem install scrivener

scrivener's People

Contributors

bemurphy avatar f-3r avatar foca avatar frodsan avatar janko avatar julio-pintos avatar lguardiola avatar pootsbook avatar slowernet avatar soveran avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

scrivener's Issues

i18n validation error messages

How can I integrate scrivener with my locale.yml file?

Looking through the source I cannot see where the actual error strings are built

Passing context information in errors

Something I've run into often is that validation errors can't be fully compressed into a single code. A common enough case is validating length:

assert_length :username, 3..16

If the user's provided username was out of bounds, we'd get a { foo: [:not_in_range] } error. Now, the error message we want to render will most likely need to include the range in which it is valid. Something like "The username must be between 3 and 16 characters long".

However, to generate this error, we need to know the range of validity in the view, which means duplicating knowledge. This is worsened when you have multiple front-ends, and the API serialises error codes instead of rendered messages due to i18n).

When the form changes, we need to remember to change the error message in the serialiser and/or probably in the web views if the app also has those.

It would be useful, then, to include context information about the failure in the error itself, so that clients that render this error have all the information from the single source of truth regarding validations: the Scrivener object.

The solution I used (which is probably not be the best, but it worked for me) was to introduce a Scrivener::Error object that emulated the error symbol, but added a context attribute. Essentially:

class Error < SimpleDelegator
  attr_reader :context
  def initialize(code, context = nil)
    super(code)
    @context = context
  end
end

And then went and copied scrivener/validations into the project and replaced all the assertions with:

def assert_length(att, range, error = [att, Error.new(:not_in_range, range)])
  
end

This works, and particularly, kept compatibility with all our existing forms as the error "is still" a Symbol. However, seems a bit heavy handed to wrap an (extra) object around every error code.

While writing this issue, I thought of an alternative, by changing errors into 2-element pairs:

def assert_length(att, range, error = [att, [:not_in_range, range]])
  
end


def assert_url(att, error = [att, [:not_url]]) # or [:not_url, nil] to be explicit ¯\_(ツ)_/¯
  
end

This is pretty minimalistic, and a bit more memory efficient than the above solution, but would break everyone's code if implemented into Scrivener 😄

Is this a "problem" only for me?

We introduced this when a requirement changed and we had to wait for a new release of the mobile application to support these contexts in the API before we were able to implement the change in the business rule itself, which was a bit annoying, as mobile apps aren't particularly fast at upgrading 😛

Is this worth changing in Scrivener itself? Or is everyone fine with duplicating error contexts in wherever you generate error messages / serialise responses to your APIs / whatever.

Warning due to splat operator with ruby 2.7

When using validate with keywords arguments, ruby 2.7 shows a warning:

  def validate(available_articles:)
    # ..
  end
scrivener/lib/scrivener/validations.rb:72: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call

This is due to valid? method definition using the splat operator when forwarding arguments to validate:

def valid?(*args)
  # ...
  validate(*args)
  # ...
end

This could be fixed using the double splat, as suggested by the warning message, but that wouldn't allow the use of positional parameters in validate definition.

I think that adding an extra parameter to the valid? definition should work for all cases:

def valid?(*args, **kargs)
  # ..
  validate(*args, **kargs)
  # ..
end

Of course, this would break compatibility with ruby 1.9 (not sure about ruby 2.0), but I don't think that version is intended to be supported.

It that makes sense, I could crate a PR with the proposal.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.