{"id":30797242,"url":"https://github.com/sborrazas/organ","last_synced_at":"2025-10-27T19:04:15.664Z","repository":{"id":17030126,"uuid":"19794255","full_name":"sborrazas/organ","owner":"sborrazas","description":"Forms with integrated validations and attribute coercing.","archived":false,"fork":false,"pushed_at":"2017-04-13T15:51:54.000Z","size":17,"stargazers_count":9,"open_issues_count":2,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-08-09T20:53:51.392Z","etag":null,"topics":["coercion","forms","ruby","validation"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sborrazas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-05-14T20:02:38.000Z","updated_at":"2019-07-29T20:51:01.000Z","dependencies_parsed_at":"2022-09-24T12:46:09.032Z","dependency_job_id":null,"html_url":"https://github.com/sborrazas/organ","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/sborrazas/organ","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sborrazas%2Forgan","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sborrazas%2Forgan/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sborrazas%2Forgan/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sborrazas%2Forgan/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sborrazas","download_url":"https://codeload.github.com/sborrazas/organ/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sborrazas%2Forgan/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273796422,"owners_count":25170073,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-05T02:00:09.113Z","response_time":402,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coercion","forms","ruby","validation"],"created_at":"2025-09-05T17:51:53.622Z","updated_at":"2025-10-27T19:04:10.618Z","avatar_url":"https://github.com/sborrazas.png","language":"Ruby","readme":"Organ\n=====\n\nForms with integrated validations and attribute coercing.\n\nIntroduction\n-----------\n\nOrgan is a small library for manipulating form-based data with validations\nattributes coercion.\n\nThese forms are very useful for handling HTTP requests where we receive certain\nparameters and need to coerce them, validate them and then do something with\nthem. They are made so that the system has 1 form per service, so the form\nnames should usually be very explicit of the service they are providing\n(`CreateUser`, `DeleteUser`, `SendTweet`, `NotifyAdmin`).\n\nYou should reuse forms behaviour (including validations) through inheritance\nor modules (like having `CreateUser` inherit from `UpdateUser`). They can be\nextended to handle worker jobs, to act as presenters, etc.\n\nThey do not handle HTML rendering of forms or do any HTTP manipulation.\n\nThe name `Organ` was inspired by the fact that organs beheave in a module manner\nso that they do one thing only.\n\nUsage\n-----\n\nA form is simply a class which inherits from `Organ::Form`. You can specify\nthe attributes the form will have with the `attributes` class method.\n\nThe `attributes` class method takes the attribute name and any options that\nattribute has.\n\nThe options can be:\n\n* `:type` - The type for which that attribute will be coerced.\n* `:skip` - If `true` it won't include the attribute when calling the\n  `#attributes` method on the instance.\n* `:skip_reader` - If `true`, it won't create the attribute reader for that\n  attribute.\n\nExample:\n\n```ruby\nclass CreateCustomer \u003c Organ::Form\n\n  attribute(:name, :type =\u003e :string, :trim =\u003e true)\n  attribute(:address, :type =\u003e :string, :trim =\u003e true)\n\n  def validate\n    validate_presence(:name)\n    validate_length(:name, :min =\u003e 4, :max =\u003e 255)\n    validate_length(:address, :max =\u003e 255)\n\n    validate_uniqueness(:address) do |addr|\n      Customer.where(:address =\u003e addr).empty?\n    end\n  end\n\n  def perform\n    Customer.create(attributes)\n  end\n\nend\n\n# Sinatra example\npost \"/customer\" do\n  form = CreateCustomer.new(params[:customer])\n  content_type(:json)\n  if form.valid?\n    form.perform\n    status(204)\n  else\n    status(422)\n    JSON.generate(\"errors\" =\u003e form.errors)\n  end\nend\n```\n\nDefault types\n-------------\n\nThe default types you can use are:\n\n### :string\n\nCoerces the value into a string or nil if no value given. If the `:trim` option\nis given it also strips the preceding/trailing whitespaces and newlines.\n\n### :boolean\n\nCoerces the value into false (if no value given) or true otherwise.\n\n### :array\n\nCoerces the value into an Array. If it can't be coerced into an Array, it\nreturns an empty Array. An additional `:element_type` option with another type\ncan be specifed to coerce all the elements of the array into it.\n\nIf a Hash is passed instead of an array, it takes the Hash values.\n\n### :float\n\nCoerce the value into a Float, or nil of the value can't be coerced into a\nfloat.\n\n### :hash\n\nCoerces the value into a Hash. If it can't be coerced into a Hash, it returns\nan empty Hash. An additional `:key_type` and/or `:value_type` can be specified\nto coerce the keys/values of the hash respectively.\n\n### :integer\n\nCoerces the value into a Fixnum. If it can't be coerced it returns nil.\n\n### :date\n\nCoerces the value into a date. If the value doesn't have the `%Y-%m-%d` format\nit returns nil.\n\nDefault validations\n-------------------\n\n### validate_presence\n\nIf the value is falsy or an empty string it appends a `:blank` error to the\nattribute.\n\n### validate_uniqueness\n\nIf the value is present and the block passed returns false, it appends a\n`:taken` error to the attribute. Example:\n\n```ruby\nvalidate_uniqueness(:username) do |username|\n  User.where(:username =\u003e username).empty?\nend\n```\n\n### validate_email_format\n\nIf the value is present and doesn't match an emails format, it appends an\n`:invalid` error to the attribute.\n\n### validate_format\n\nIf the value is present and doesn't match the specified format, it appends an\n`:invalid` error to the attribute.\n\n### validate_length\n\nIf the value is present and shorter than the `:min` option, it appends a\n`:too_short` error to the attribute. If it's longer than the `:max` option, it\nappends a `:too_long` error to the attribute. Example:\n\n```ruby\nvalidate_length(:username, :min =\u003e 3, :max =\u003e 255)\nvalidate_length(:first_name, :max =\u003e 255)\n```\n\n### validate_inclusion\n\nIf the value is present and not included on the given list it appends a\n`:not_included` error to the attribute.\n\n### validate_range\n\nIf the value is present and less than the `:min` option, it appends a\n`:less_than` error to the attribute. If it's greater than the `:max` option, it\nappends a `:greater_than` error to the attribute. Example:\n\n```ruby\nvalidate_range(:age, :min =\u003e 18)\n```\n\n### validation_block\n\nThis is a helper method that only calls the given block if the form doesn't have\nany errors. This is particularly useful when some of the validations are costly\nto make and unnecessary if the form already has errors.\n\n```ruby\nvalidate_length(:username, :min =\u003e 7)\n\nvalidation_block do # Will only get called if previous validation passed\n  validate_uniqueness(:username) do |username|\n    User.where(:username =\u003e username).empty?\n  end\nend\n```\n\nExtensions\n----------\n\nThese forms were meant to be extended when necessary. These are a few examples\nof how they can be extended.\n\n### Extensions::Paginate\n\nAn extension to paginate results with Sequel Datasets.\n\n```ruby\nmodule Extensions\n  module Presenter\n\n    DEFAULT_PER_PAGE = 30\n    MAX_PER_PAGE = 100\n\n    def self.included(base)\n      base.attribute(:page, :type =\u003e :integer, :skip_reader =\u003e true)\n      base.attribute(:per_page, :type =\u003e :integer, :skip_reader =\u003e true)\n    end\n\n    def each(\u0026block)\n      results.each(\u0026block)\n    end\n\n    def total_pages\n      @total_pages ||= (1.0 * dataset.count / per_page).ceil\n    end\n\n    def any?\n      total_pages \u003e 0\n    end\n\n    def results\n      @results ||= begin\n        start = (page - 1) * per_page\n        _dataset = dataset.limit(per_page, start)\n        _dataset.all\n      end\n    end\n\n    def per_page\n      if @per_page \u0026\u0026 @per_page \u003e= 1 \u0026\u0026 per_page \u003c= MAX_PER_PAGE\n        @per_page\n      else\n        DEFAULT_PER_PAGE\n      end\n    end\n\n    def page\n      @page \u0026\u0026 @page \u003e 1 ? @page : 1\n    end\n\n  end\nend\n\nmodule Presenters\n  class UserPets \u003c Organ::Form\n\n    include Extensions::Paginate\n\n    attribute(:user_id, :type =\u003e :integer)\n\n    def dataset\n      Pet.where(:user_id =\u003e user_id)\n    end\n\n  end\nend\n\n# Sinatra app\nget \"/pets\" do\n  presenter = Presenter::UserPets.new({\n    :user_id =\u003e session[:user_id],\n    :page =\u003e params[:page],\n    :per_page =\u003e params[:per_page]\n  })\n  erb(:\"pets/index\", :locals =\u003e { :pets =\u003e presenter })\nend\n```\n\n### Extensions::Worker\n\nAn extension to create worker job handlers using\n[Ost](https://github.com/soveran/ost).\n\n```ruby\nmodule Extensions\n  module Worker\n\n    def self.queue_job(attributes)\n      queue \u003c\u003c JSON.generate(attributes)\n    end\n\n    def self.stop\n      queue.stop\n    end\n\n    def self.watch_queue\n      queue.each do |json_str|\n        attributes = JSON.parse(json_str)\n        new(attributes).perform\n      end\n    end\n\n    private\n\n    def self.queue\n      Ost[self.name]\n    end\n\n  end\nend\n\nmodule Workers\n  class EmailNotifier \u003c Organ::Form\n\n    include Extensions::Worker\n\n    attribute(:email)\n    attribute(:message)\n\n    def perform\n      # send message to email...\n    end\n\n  end\nend\n\n# Sinatra app\nget \"/queue_email\" do\n  Workers::EmailNotifier.queue_job(params[:notification])\n  status(204)\nend\n```\n\nAknowledgements\n---------------\n\nThis library was inspired mainly by @soveran\n[Scrivener](https://github.com/soveran/scrivener) and was made with the help ofn\n@grilix.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsborrazas%2Forgan","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsborrazas%2Forgan","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsborrazas%2Forgan/lists"}