Skip to content

Forms with integrated validations and attribute coercing.

License

Notifications You must be signed in to change notification settings

sborrazas/organ

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Organ

Forms with integrated validations and attribute coercing.

Introduction

Organ is a small library for manipulating form-based data with validations attributes coercion.

These forms are very useful for handling HTTP requests where we receive certain parameters and need to coerce them, validate them and then do something with them. They are made so that the system has 1 form per service, so the form names should usually be very explicit of the service they are providing (CreateUser, DeleteUser, SendTweet, NotifyAdmin).

You should reuse forms behaviour (including validations) through inheritance or modules (like having CreateUser inherit from UpdateUser). They can be extended to handle worker jobs, to act as presenters, etc.

They do not handle HTML rendering of forms or do any HTTP manipulation.

The name Organ was inspired by the fact that organs beheave in a module manner so that they do one thing only.

Usage

A form is simply a class which inherits from Organ::Form. You can specify the attributes the form will have with the attributes class method.

The attributes class method takes the attribute name and any options that attribute has.

The options can be:

  • :type - The type for which that attribute will be coerced.
  • :skip - If true it won't include the attribute when calling the #attributes method on the instance.
  • :skip_reader - If true, it won't create the attribute reader for that attribute.

Example:

class CreateCustomer < Organ::Form

  attribute(:name, :type => :string, :trim => true)
  attribute(:address, :type => :string, :trim => true)

  def validate
    validate_presence(:name)
    validate_length(:name, :min => 4, :max => 255)
    validate_length(:address, :max => 255)

    validate_uniqueness(:address) do |addr|
      Customer.where(:address => addr).empty?
    end
  end

  def perform
    Customer.create(attributes)
  end

end

# Sinatra example
post "/customer" do
  form = CreateCustomer.new(params[:customer])
  content_type(:json)
  if form.valid?
    form.perform
    status(204)
  else
    status(422)
    JSON.generate("errors" => form.errors)
  end
end

Default types

The default types you can use are:

:string

Coerces the value into a string or nil if no value given. If the :trim option is given it also strips the preceding/trailing whitespaces and newlines.

:boolean

Coerces the value into false (if no value given) or true otherwise.

:array

Coerces the value into an Array. If it can't be coerced into an Array, it returns an empty Array. An additional :element_type option with another type can be specifed to coerce all the elements of the array into it.

If a Hash is passed instead of an array, it takes the Hash values.

:float

Coerce the value into a Float, or nil of the value can't be coerced into a float.

:hash

Coerces the value into a Hash. If it can't be coerced into a Hash, it returns an empty Hash. An additional :key_type and/or :value_type can be specified to coerce the keys/values of the hash respectively.

:integer

Coerces the value into a Fixnum. If it can't be coerced it returns nil.

:date

Coerces the value into a date. If the value doesn't have the %Y-%m-%d format it returns nil.

Default validations

validate_presence

If the value is falsy or an empty string it appends a :blank error to the attribute.

validate_uniqueness

If the value is present and the block passed returns false, it appends a :taken error to the attribute. Example:

validate_uniqueness(:username) do |username|
  User.where(:username => username).empty?
end

validate_email_format

If the value is present and doesn't match an emails format, it appends an :invalid error to the attribute.

validate_format

If the value is present and doesn't match the specified format, it appends an :invalid error to the attribute.

validate_length

If the value is present and shorter than the :min option, it appends a :too_short error to the attribute. If it's longer than the :max option, it appends a :too_long error to the attribute. Example:

validate_length(:username, :min => 3, :max => 255)
validate_length(:first_name, :max => 255)

validate_inclusion

If the value is present and not included on the given list it appends a :not_included error to the attribute.

validate_range

If the value is present and less than the :min option, it appends a :less_than error to the attribute. If it's greater than the :max option, it appends a :greater_than error to the attribute. Example:

validate_range(:age, :min => 18)

validation_block

This is a helper method that only calls the given block if the form doesn't have any errors. This is particularly useful when some of the validations are costly to make and unnecessary if the form already has errors.

validate_length(:username, :min => 7)

validation_block do # Will only get called if previous validation passed
  validate_uniqueness(:username) do |username|
    User.where(:username => username).empty?
  end
end

Extensions

These forms were meant to be extended when necessary. These are a few examples of how they can be extended.

Extensions::Paginate

An extension to paginate results with Sequel Datasets.

module Extensions
  module Presenter

    DEFAULT_PER_PAGE = 30
    MAX_PER_PAGE = 100

    def self.included(base)
      base.attribute(:page, :type => :integer, :skip_reader => true)
      base.attribute(:per_page, :type => :integer, :skip_reader => true)
    end

    def each(&block)
      results.each(&block)
    end

    def total_pages
      @total_pages ||= (1.0 * dataset.count / per_page).ceil
    end

    def any?
      total_pages > 0
    end

    def results
      @results ||= begin
        start = (page - 1) * per_page
        _dataset = dataset.limit(per_page, start)
        _dataset.all
      end
    end

    def per_page
      if @per_page && @per_page >= 1 && per_page <= MAX_PER_PAGE
        @per_page
      else
        DEFAULT_PER_PAGE
      end
    end

    def page
      @page && @page > 1 ? @page : 1
    end

  end
end

module Presenters
  class UserPets < Organ::Form

    include Extensions::Paginate

    attribute(:user_id, :type => :integer)

    def dataset
      Pet.where(:user_id => user_id)
    end

  end
end

# Sinatra app
get "/pets" do
  presenter = Presenter::UserPets.new({
    :user_id => session[:user_id],
    :page => params[:page],
    :per_page => params[:per_page]
  })
  erb(:"pets/index", :locals => { :pets => presenter })
end

Extensions::Worker

An extension to create worker job handlers using Ost.

module Extensions
  module Worker

    def self.queue_job(attributes)
      queue << JSON.generate(attributes)
    end

    def self.stop
      queue.stop
    end

    def self.watch_queue
      queue.each do |json_str|
        attributes = JSON.parse(json_str)
        new(attributes).perform
      end
    end

    private

    def self.queue
      Ost[self.name]
    end

  end
end

module Workers
  class EmailNotifier < Organ::Form

    include Extensions::Worker

    attribute(:email)
    attribute(:message)

    def perform
      # send message to email...
    end

  end
end

# Sinatra app
get "/queue_email" do
  Workers::EmailNotifier.queue_job(params[:notification])
  status(204)
end

Aknowledgements

This library was inspired mainly by @soveran Scrivener and was made with the help ofn @grilix.