{"id":26618375,"url":"https://github.com/snmgian/foraneus","last_synced_at":"2025-08-21T19:20:17.297Z","repository":{"id":16681576,"uuid":"19437602","full_name":"snmgian/foraneus","owner":"snmgian","description":"Provides validation and transformation mechanisms to external data.","archived":false,"fork":false,"pushed_at":"2017-03-28T01:35:41.000Z","size":113,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-03-14T11:48:02.485Z","etag":null,"topics":["forms","parsing","ruby"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/snmgian.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-05-04T22:28:17.000Z","updated_at":"2015-04-25T23:54:54.000Z","dependencies_parsed_at":"2022-08-25T22:03:39.988Z","dependency_job_id":null,"html_url":"https://github.com/snmgian/foraneus","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snmgian%2Fforaneus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snmgian%2Fforaneus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snmgian%2Fforaneus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snmgian%2Fforaneus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/snmgian","download_url":"https://codeload.github.com/snmgian/foraneus/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244972696,"owners_count":20541011,"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","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":["forms","parsing","ruby"],"created_at":"2025-03-24T08:21:18.825Z","updated_at":"2025-03-24T08:21:19.508Z","avatar_url":"https://github.com/snmgian.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Foraneus\n\nForaneus allows to:\n - parse data coming from external sources (like an HTTP request).\n - convert data back to a raw representation suitable for being used at the outside, like an HTML\nform.\n\nNo matter which source of data is fed into Foraneus (external or internal), any instance can return\nraw and parsed data.\n\n## Basic usage\n\n - Declaration:\n\n  ``` ruby\n  class MyForm \u003c Foraneus\n    integer :delay\n    float :duration\n  end\n  ```\n\n - From the outside:\n\n  ``` ruby\n  form = MyForm.parse(:delay =\u003e '5', :duration =\u003e '2.14')\n  ```\n\n  ``` ruby\n  form.delay    # =\u003e 5\n  form[:delay]  # =\u003e '5'\n  ```\n\n  ``` ruby\n  form.data   # =\u003e { :delay =\u003e 5, :duration =\u003e 2.14 }\n  form[]      # =\u003e { :delay =\u003e '5', :duration =\u003e '2.14' }\n  ```\n\n - From the inside:\n\n  ``` ruby\n  form = MyForm.raw(:delay =\u003e 5, :duration =\u003e 2.14)\n  ```\n\n  ``` ruby\n  form.delay    # =\u003e 5\n  form[:delay]  # =\u003e '5'\n  ```\n\n  ``` ruby\n  # the parsed attributes\n  form.data   # =\u003e { :delay =\u003e 5, :duration =\u003e 2.14 }\n\n  # the received attributes\n  form[]      # =\u003e { :delay =\u003e '5', :duration =\u003e '2.14' }\n  ```\n\n## Declaration\n\nDeclare source classes by inheriting from `Foraneus` base class.\n\n  ``` ruby\n  class MyForm \u003c Foraneus\n    field :delay, SomeCustomConverter.new\n    float :duration\n  end\n  ```\n\nFields are declared by two ways:\n\n - calling `.field`\n - calling a shortcut method, like `.float`\n\n\nThere are shortcut methods for any of the built-in converters: boolean, date, decimal, float, integer,\nnoop, and string.\n\nWhen no converter is passed to `.field`, `Foraneus::Converters::Noop` is assigned to the declared\nfield.\n\n## Instantiation\n\nForaneus instances can be obtained by calling two methods: `parse` and `raw`.\n\nUse `.parse` when:\n - data is coming from outside of the system, like an HTTP request.\n\nUse `.raw` when:\n - data is coming from the inside of the system, like a business layer.\n\n## Converters\n\nConverters have two interrelated responsibilities:\n\n - Parse data, like the string `\"3,000\"`, into an object, `like 3_000`.\n - Serialize data, like integer `3_000`, into string `\"3,000\"`\n\nA converter is an object that responds to `#parse(s)`, `#raw(v)`, and `#opts` methods.\n\nWhen `#parse(s)` raises a StandardError exception, or any of its descendants, the exception is\nrescued and a `Foraneus::Error` instance is added to `Foraneus#errors` map.\n\n`#opts` should return the opts hash used to instantiate the converter.\n\nBuilt-in converters:\n\n - Boolean\n - Date\n - Decimal\n - Float\n - Integer\n - Noop\n - String\n\n## Validations\n\nForaneus only validates that external data can be converted to the specified types. Smart\nvalidations, like date range inclusion, are out of the scope of this gem.\n\n`#valid?` and `#errors` are handy methods that tell whether a Foraneus instance is valid or not.\n\nValid instance:\n\n  ``` ruby\n  form.valid?     # =\u003e true\n  form.errors   # =\u003e {}\n  ```\n\nInvalid one:\n\n  ``` ruby\n  form = MyForm.parse(:delay =\u003e 'INVALID')\n\n  form.valid?                     # =\u003e false\n\n  form.errors[:delay].key       # =\u003e 'ArgumentError'\n  form.errors[:delay].message   # =\u003e 'invalid value for Integer(): \"INVALID\"'\n  ```\n\n`#errors` is a map in which keys correspond to field names, and values are instances of\n`Foraneus::Error`.\n\nThe name of the exception raised by `#parse` is the error's `key` attribute, and the exception's\nmessage is set to the error's `message` attribute.\n\nData coming from the inside is assumed to be valid, so `.raw` won't return an instance having\nerrors neither being invalid.\n\n## Required fields\n\nFields can be declared as required.\n\n  ``` ruby\n  class MyForm \u003c Foraneus\n    integer :delay, :required =\u003e true\n  end\n  ```\n\nIf an external value is not fed into a required field, an error with key `KeyError` will be assigned.\n\n  ``` ruby\n  form = MyForm.parse\n\n  form.valid?                       # =\u003e false\n\n  form.errors[:delay].key         # =\u003e 'KeyError'\n  form.errors[:delay].message     # =\u003e 'required parameter not found: \"delay\"'\n  ```\n\n## Absence of optional fields\n\nAbsent fields are treated as `nil` when invoking accessor methods.\n\n  ``` ruby\n  MyForm = Class.new(Foraneus) { string :name }\n  form = MyForm.parse\n\n  form.name       # =\u003e nil\n  ```\n\nData accessors don't include any absent field.\n\n  ``` ruby\n  form.data       # =\u003e {}\n  form[]          # =\u003e {}\n  ```\n\n## Blank values\n\nBy default, any blank value is treated as nil.\n\n  ``` ruby\n  MyForm = Class.new(Foraneus) { string :name }\n\n  MyForm.parse(:name =\u003e '').name\n  # =\u003e nil\n  ```\n\nThis behaviour can be modified by setting opt `blanks_as_nil` to false.\n\n  ``` ruby\n  MyForm = Class.new(Foraneus) { string :name, :blanks_as_nil =\u003e false }\n\n  MyForm.parse(:name =\u003e '').name\n  # =\u003e ''\n  ```\n\n## Default values\n\nDefine fields with default values:\n\n  ``` ruby\n  MyForm = Class.new(Foraneus) { string :name, :default =\u003e 'Alice' }\n  ```\n\nParse data from the ouside:\n\n  ``` ruby\n  form = MyForm.parse\n\n  form.name             # =\u003e 'Alice'\n  form.data             # =\u003e { :name =\u003e 'Alice' }\n\n  form[:name]           # =\u003e nil, because data from the outside\n                        #    don't include any value\n\n  form[]                # =\u003e {}\n  ```\n\nConvert values back from the inside:\n\n  ``` ruby\n  form = MyForm.raw\n\n  form[:name]           # =\u003e 'Alice'\n  form.name             # =\u003e nil, because data from the inside\n                        #    don't include any value\n  ```\n\n## Prevent name clashes\n\nIt is possible to rename methods `#errors` and `#data` so it will not conflict with defined fields.\n\n  ``` ruby\n  MyForm = Class.new(Foraneus) {\n    field :errors\n    field :data\n\n    accessors[:errors] = :non_clashing_errors\n    accessors[:data] = :non_clashing_data\n  }\n  ```\n\n  ``` ruby\n  form = MyForm.parse(:errors =\u003e 'some errors', :data =\u003e 'some data')\n\n  form.errors                 # =\u003e 'some errors'\n  form.data                   # =\u003e 'some data'\n\n  form.non_clashing_errors    # []\n  form.non_clashing_data      # { :errors =\u003e 'some errors', :data =\u003e 'some data' }\n  ```\n\n## Nesting\nForms can also have form fields.\n\n  ``` ruby\n  class Profile \u003c Foraneus\n    string  :email\n\n    form    :coords do\n      integer :x\n      integer :y\n    end\n  end\n  ```\n\n  ``` ruby\n  profile = Profile.parse(:email =\u003e 'mail@example.org', :coords =\u003e { :x =\u003e '1', :y =\u003e '2' })\n\n  profile.email     # =\u003e mail@example.org\n\n  profile.coords.x # =\u003e 1\n  profile.coords.y # =\u003e 2\n\n  profile.coords[:x] # =\u003e '1'\n  profile.coords[:y] # =\u003e '2'\n\n  ```\n\n  ``` ruby\n  profile.coords.data # =\u003e { :x =\u003e 1, :y =\u003e 2 }\n  profile.coords[]    # =\u003e { :x =\u003e '1', :y =\u003e '2' }\n  ```\n\n  ``` ruby\n  profile[:coords] # =\u003e  { :x =\u003e '1', :y =\u003e '2' }\n  ```\n\n  ``` ruby\n  profile.data # =\u003e { :email =\u003e 'mail.example.org', :coords =\u003e { :x =\u003e 1, :y =\u003e 2 } }\n  profile[] # =\u003e { :email =\u003e 'mail@example.org', :coords =\u003e { :x =\u003e '1', :y =\u003e '2' } }\n  ```\n\n - Absence\n  ``` ruby\n  profile = Profile.parse\n\n  profile.coords # =\u003e nil\n  profile.data   # =\u003e {}\n  profile[]      # =\u003e {}\n  ```\n\n - Nullity\n  ``` ruby\n  profile = Profile.parse(:coords =\u003e nil)\n\n  profile.coords # =\u003e nil\n  profile.data   # =\u003e { :coords =\u003e nil }\n  profile[]      # =\u003e { :coords =\u003e nil }\n  ```\n\n - Emptiness\n  ``` ruby\n  profile = Profile.parse(:coords =\u003e {})\n\n  profile.coords.x    # =\u003e nil\n  profile.coords.y    # =\u003e nil\n\n  profile.coords.data # =\u003e {}\n  profile.coords[]    # =\u003e {}\n\n  profile.data  # =\u003e { :coords =\u003e {} }\n  profile[] # =\u003e { :coords =\u003e {} }\n  ```\n\n - Validations\n  ``` ruby\n  profile = Profile.parse(:coords =\u003e { :x =\u003e '0', :y =\u003e '0' })\n\n  profile.coords.valid? # =\u003e true\n  profile.coords.errors # =\u003e {}\n  ```\n\n  ``` ruby\n  profile = Profile.parse(:coords =\u003e { :x =\u003e 'FIVE' })\n\n  profile.coords.x  # =\u003e nil\n\n  profile.coords.valid? # =\u003e false\n\n  profile.coords.errors[:x].key # =\u003e 'ArgumentError'\n  profile.coords.errors[:x].message   # =\u003e 'invalid value for Integer(): \"FIVE\"'\n\n  profile.valid?  # =\u003e false\n\n  profile.errors[:coords].key # =\u003e 'NestedFormError'\n  profile.errors[:coords].message # =\u003e 'Invalid nested form: coords'\n  ```\n\n  ``` ruby\n  profile = Profile.parse(:coords =\u003e 'FIVE,SIX')\n\n  profile.coords    # =\u003e nil\n\n\n  profile.valid?  # =\u003e false\n  profile.errors[:coords].key # =\u003e 'NestedFormError'\n  profile.errors[:coords].message # =\u003e 'Invalid nested form: coords'\n  ```\n\n - From the inside:\n  ``` ruby\n  profile = Profile.raw(:email =\u003e 'mail@example.org', :coords =\u003e { :x =\u003e 1, :y =\u003e 2 })\n  ```\n\n  ``` ruby\n  profile.coords.x  # =\u003e 1\n  profile.coords.data  # =\u003e { :x =\u003e 1, :y =\u003e 2 }\n\n  profile.coords[:x] # =\u003e '1'\n  profile.coords[]  # =\u003e { :x =\u003e '1', :y =\u003e '2' }\n  ```\n\n  ``` ruby\n  profile.data # =\u003e { :email =\u003e 'email.example.org', :coords =\u003e { :x =\u003e 0, :y =\u003e 0 } }\n  profile[] # =\u003e { :email =\u003e 'email@example.org', :coords =\u003e { :x =\u003e '0', :y =\u003e '0' } }\n  ```\n  - Absence\n\n  ```\n  profile = Profile.raw\n\n  profile.coords # =\u003e nil\n  profile.data   # =\u003e {}\n  profile[]      # =\u003e {}\n  ```\n\n  - Nullity\n  ``` ruby\n  profile = Profile.raw(:coords =\u003e nil)\n\n  profile.coords # =\u003e nil\n  profile.data   # =\u003e { :coords =\u003e nil }\n  profile[]      # =\u003e { :coords =\u003e nil }\n  ```\n\n  - Emptiness\n  ``` ruby\n  profile = Profile.raw(:coords =\u003e {})\n\n  profile.coords.x  # =\u003e nil\n  profile.coords.y  # =\u003e nil\n\n  profile.coords.data # =\u003e {}\n  profile.coords[]    # =\u003e {}\n\n  profile.data      # =\u003e { :coords =\u003e {} }\n  profile[]      # =\u003e { :coords =\u003e {} }\n  ```\n\n\n## Installation\n\n - Install `foraneus` as a gem.\n\n    ``` shell\n    gem install foraneus\n    ```\n\n## Running tests\n\nTests are written in MiniTest. To run them all just execute the following from your command line:\n\n  ``` shell\n  ruby spec/runner.rb\n  ```\n\nExecute the following when ruby 1.8.7:\n\n  ``` shell\n  ruby -rubygems spec/runner.rb\n  ```\n\nTo run a specific test case:\n\n  ``` shell\n    ruby -Ispec -Ilib spec/lib/foraneus_spec.rb\n  ```\n\nWhen running under ruby 1.8.7:\n\n  ``` shell\n    ruby -rubygems -Ispec -Ilib spec/lib/foraneus_spec.rb\n  ```\n\n## Code documentation\n\nDocumentation is written in Yard. To see it in a browser, execute this command:\n\n  ``` shell\n  yard server --reload\n  ```\n\nThen point the browser to `http://localhost:8808/`.\n\n## Badges\n\n[![Build Status](https://travis-ci.org/snmgian/foraneus.svg?branch=master)](https://travis-ci.org/snmgian/foraneus) [![Code Climate](https://codeclimate.com/github/snmgian/foraneus.png)](https://codeclimate.com/github/snmgian/foraneus)\n\n## License\n\nThis software is licensed under the [LGPL][lgpl] license.\n\n[lgpl]: https://www.gnu.org/licenses/lgpl.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnmgian%2Fforaneus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsnmgian%2Fforaneus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnmgian%2Fforaneus/lists"}