项目作者: sborrazas

项目描述 :
Forms with integrated validations and attribute coercing.
高级语言: Ruby
项目地址: git://github.com/sborrazas/organ.git
创建时间: 2014-05-14T20:02:38Z
项目社区:https://github.com/sborrazas/organ

开源协议:MIT License

下载


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:

  1. class CreateCustomer < Organ::Form
  2. attribute(:name, :type => :string, :trim => true)
  3. attribute(:address, :type => :string, :trim => true)
  4. def validate
  5. validate_presence(:name)
  6. validate_length(:name, :min => 4, :max => 255)
  7. validate_length(:address, :max => 255)
  8. validate_uniqueness(:address) do |addr|
  9. Customer.where(:address => addr).empty?
  10. end
  11. end
  12. def perform
  13. Customer.create(attributes)
  14. end
  15. end
  16. # Sinatra example
  17. post "/customer" do
  18. form = CreateCustomer.new(params[:customer])
  19. content_type(:json)
  20. if form.valid?
  21. form.perform
  22. status(204)
  23. else
  24. status(422)
  25. JSON.generate("errors" => form.errors)
  26. end
  27. 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:

  1. validate_uniqueness(:username) do |username|
  2. User.where(:username => username).empty?
  3. 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:

  1. validate_length(:username, :min => 3, :max => 255)
  2. 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:

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

  1. validate_length(:username, :min => 7)
  2. validation_block do # Will only get called if previous validation passed
  3. validate_uniqueness(:username) do |username|
  4. User.where(:username => username).empty?
  5. end
  6. 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.

  1. module Extensions
  2. module Presenter
  3. DEFAULT_PER_PAGE = 30
  4. MAX_PER_PAGE = 100
  5. def self.included(base)
  6. base.attribute(:page, :type => :integer, :skip_reader => true)
  7. base.attribute(:per_page, :type => :integer, :skip_reader => true)
  8. end
  9. def each(&block)
  10. results.each(&block)
  11. end
  12. def total_pages
  13. @total_pages ||= (1.0 * dataset.count / per_page).ceil
  14. end
  15. def any?
  16. total_pages > 0
  17. end
  18. def results
  19. @results ||= begin
  20. start = (page - 1) * per_page
  21. _dataset = dataset.limit(per_page, start)
  22. _dataset.all
  23. end
  24. end
  25. def per_page
  26. if @per_page && @per_page >= 1 && per_page <= MAX_PER_PAGE
  27. @per_page
  28. else
  29. DEFAULT_PER_PAGE
  30. end
  31. end
  32. def page
  33. @page && @page > 1 ? @page : 1
  34. end
  35. end
  36. end
  37. module Presenters
  38. class UserPets < Organ::Form
  39. include Extensions::Paginate
  40. attribute(:user_id, :type => :integer)
  41. def dataset
  42. Pet.where(:user_id => user_id)
  43. end
  44. end
  45. end
  46. # Sinatra app
  47. get "/pets" do
  48. presenter = Presenter::UserPets.new({
  49. :user_id => session[:user_id],
  50. :page => params[:page],
  51. :per_page => params[:per_page]
  52. })
  53. erb(:"pets/index", :locals => { :pets => presenter })
  54. end

Extensions::Worker

An extension to create worker job handlers using
Ost.

  1. module Extensions
  2. module Worker
  3. def self.queue_job(attributes)
  4. queue << JSON.generate(attributes)
  5. end
  6. def self.stop
  7. queue.stop
  8. end
  9. def self.watch_queue
  10. queue.each do |json_str|
  11. attributes = JSON.parse(json_str)
  12. new(attributes).perform
  13. end
  14. end
  15. private
  16. def self.queue
  17. Ost[self.name]
  18. end
  19. end
  20. end
  21. module Workers
  22. class EmailNotifier < Organ::Form
  23. include Extensions::Worker
  24. attribute(:email)
  25. attribute(:message)
  26. def perform
  27. # send message to email...
  28. end
  29. end
  30. end
  31. # Sinatra app
  32. get "/queue_email" do
  33. Workers::EmailNotifier.queue_job(params[:notification])
  34. status(204)
  35. end

Aknowledgements

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