Project

foraneus

0.0
No commit activity in last 3 years
No release in over 3 years
Provides two way transformation mechanisms to external data.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

 Project Readme

Foraneus

Foraneus allows to:

  • parse data coming from external sources (like an HTTP request).
  • convert data back to a raw representation suitable for being used at the outside, like an HTML form.

No matter which source of data is fed into Foraneus (external or internal), any instance can return raw and parsed data.

Basic usage

  • Declaration:
class MyForm < Foraneus
  integer :delay
  float :duration
end
  • From the outside:
form = MyForm.parse(:delay => '5', :duration => '2.14')
form.delay    # => 5
form[:delay]  # => '5'
form.data   # => { :delay => 5, :duration => 2.14 }
form[]      # => { :delay => '5', :duration => '2.14' }
  • From the inside:
form = MyForm.raw(:delay => 5, :duration => 2.14)
form.delay    # => 5
form[:delay]  # => '5'
# the parsed attributes
form.data   # => { :delay => 5, :duration => 2.14 }

# the received attributes
form[]      # => { :delay => '5', :duration => '2.14' }

Declaration

Declare source classes by inheriting from Foraneus base class.

class MyForm < Foraneus
  field :delay, SomeCustomConverter.new
  float :duration
end

Fields are declared by two ways:

  • calling .field
  • calling a shortcut method, like .float

There are shortcut methods for any of the built-in converters: boolean, date, decimal, float, integer, noop, and string.

When no converter is passed to .field, Foraneus::Converters::Noop is assigned to the declared field.

Instantiation

Foraneus instances can be obtained by calling two methods: parse and raw.

Use .parse when:

  • data is coming from outside of the system, like an HTTP request.

Use .raw when:

  • data is coming from the inside of the system, like a business layer.

Converters

Converters have two interrelated responsibilities:

  • Parse data, like the string "3,000", into an object, like 3_000.
  • Serialize data, like integer 3_000, into string "3,000"

A converter is an object that responds to #parse(s), #raw(v), and #opts methods.

When #parse(s) raises a StandardError exception, or any of its descendants, the exception is rescued and a Foraneus::Error instance is added to Foraneus#errors map.

#opts should return the opts hash used to instantiate the converter.

Built-in converters:

  • Boolean
  • Date
  • Decimal
  • Float
  • Integer
  • Noop
  • String

Validations

Foraneus only validates that external data can be converted to the specified types. Smart validations, like date range inclusion, are out of the scope of this gem.

#valid? and #errors are handy methods that tell whether a Foraneus instance is valid or not.

Valid instance:

form.valid?     # => true
form.errors   # => {}

Invalid one:

form = MyForm.parse(:delay => 'INVALID')

form.valid?                     # => false

form.errors[:delay].key       # => 'ArgumentError'
form.errors[:delay].message   # => 'invalid value for Integer(): "INVALID"'

#errors is a map in which keys correspond to field names, and values are instances of Foraneus::Error.

The name of the exception raised by #parse is the error's key attribute, and the exception's message is set to the error's message attribute.

Data coming from the inside is assumed to be valid, so .raw won't return an instance having errors neither being invalid.

Required fields

Fields can be declared as required.

class MyForm < Foraneus
  integer :delay, :required => true
end

If an external value is not fed into a required field, an error with key KeyError will be assigned.

form = MyForm.parse

form.valid?                       # => false

form.errors[:delay].key         # => 'KeyError'
form.errors[:delay].message     # => 'required parameter not found: "delay"'

Absence of optional fields

Absent fields are treated as nil when invoking accessor methods.

MyForm = Class.new(Foraneus) { string :name }
form = MyForm.parse

form.name       # => nil

Data accessors don't include any absent field.

form.data       # => {}
form[]          # => {}

Blank values

By default, any blank value is treated as nil.

MyForm = Class.new(Foraneus) { string :name }

MyForm.parse(:name => '').name
# => nil

This behaviour can be modified by setting opt blanks_as_nil to false.

MyForm = Class.new(Foraneus) { string :name, :blanks_as_nil => false }

MyForm.parse(:name => '').name
# => ''

Default values

Define fields with default values:

MyForm = Class.new(Foraneus) { string :name, :default => 'Alice' }

Parse data from the ouside:

form = MyForm.parse

form.name             # => 'Alice'
form.data             # => { :name => 'Alice' }

form[:name]           # => nil, because data from the outside
                      #    don't include any value

form[]                # => {}

Convert values back from the inside:

form = MyForm.raw

form[:name]           # => 'Alice'
form.name             # => nil, because data from the inside
                      #    don't include any value

Prevent name clashes

It is possible to rename methods #errors and #data so it will not conflict with defined fields.

MyForm = Class.new(Foraneus) {
  field :errors
  field :data

  accessors[:errors] = :non_clashing_errors
  accessors[:data] = :non_clashing_data
}
form = MyForm.parse(:errors => 'some errors', :data => 'some data')

form.errors                 # => 'some errors'
form.data                   # => 'some data'

form.non_clashing_errors    # []
form.non_clashing_data      # { :errors => 'some errors', :data => 'some data' }

Nesting

Forms can also have form fields.

class Profile < Foraneus
  string  :email

  form    :coords do
    integer :x
    integer :y
  end
end
profile = Profile.parse(:email => 'mail@example.org', :coords => { :x => '1', :y => '2' })

profile.email     # => mail@example.org

profile.coords.x # => 1
profile.coords.y # => 2

profile.coords[:x] # => '1'
profile.coords[:y] # => '2'
profile.coords.data # => { :x => 1, :y => 2 }
profile.coords[]    # => { :x => '1', :y => '2' }
profile[:coords] # =>  { :x => '1', :y => '2' }
profile.data # => { :email => 'mail.example.org', :coords => { :x => 1, :y => 2 } }
profile[] # => { :email => 'mail@example.org', :coords => { :x => '1', :y => '2' } }
  • Absence
profile = Profile.parse

profile.coords # => nil
profile.data   # => {}
profile[]      # => {}
  • Nullity
profile = Profile.parse(:coords => nil)

profile.coords # => nil
profile.data   # => { :coords => nil }
profile[]      # => { :coords => nil }
  • Emptiness
profile = Profile.parse(:coords => {})

profile.coords.x    # => nil
profile.coords.y    # => nil

profile.coords.data # => {}
profile.coords[]    # => {}

profile.data  # => { :coords => {} }
profile[] # => { :coords => {} }
  • Validations
profile = Profile.parse(:coords => { :x => '0', :y => '0' })

profile.coords.valid? # => true
profile.coords.errors # => {}
profile = Profile.parse(:coords => { :x => 'FIVE' })

profile.coords.x  # => nil

profile.coords.valid? # => false

profile.coords.errors[:x].key # => 'ArgumentError'
profile.coords.errors[:x].message   # => 'invalid value for Integer(): "FIVE"'

profile.valid?  # => false

profile.errors[:coords].key # => 'NestedFormError'
profile.errors[:coords].message # => 'Invalid nested form: coords'
profile = Profile.parse(:coords => 'FIVE,SIX')

profile.coords    # => nil


profile.valid?  # => false
profile.errors[:coords].key # => 'NestedFormError'
profile.errors[:coords].message # => 'Invalid nested form: coords'
  • From the inside:
profile = Profile.raw(:email => 'mail@example.org', :coords => { :x => 1, :y => 2 })
profile.coords.x  # => 1
profile.coords.data  # => { :x => 1, :y => 2 }

profile.coords[:x] # => '1'
profile.coords[]  # => { :x => '1', :y => '2' }
profile.data # => { :email => 'email.example.org', :coords => { :x => 0, :y => 0 } }
profile[] # => { :email => 'email@example.org', :coords => { :x => '0', :y => '0' } }
  • Absence
profile = Profile.raw

profile.coords # => nil
profile.data   # => {}
profile[]      # => {}
  • Nullity
profile = Profile.raw(:coords => nil)

profile.coords # => nil
profile.data   # => { :coords => nil }
profile[]      # => { :coords => nil }
  • Emptiness
profile = Profile.raw(:coords => {})

profile.coords.x  # => nil
profile.coords.y  # => nil

profile.coords.data # => {}
profile.coords[]    # => {}

profile.data      # => { :coords => {} }
profile[]      # => { :coords => {} }

Installation

  • Install foraneus as a gem.

    gem install foraneus

Running tests

Tests are written in MiniTest. To run them all just execute the following from your command line:

ruby spec/runner.rb

Execute the following when ruby 1.8.7:

ruby -rubygems spec/runner.rb

To run a specific test case:

  ruby -Ispec -Ilib spec/lib/foraneus_spec.rb

When running under ruby 1.8.7:

  ruby -rubygems -Ispec -Ilib spec/lib/foraneus_spec.rb

Code documentation

Documentation is written in Yard. To see it in a browser, execute this command:

yard server --reload

Then point the browser to http://localhost:8808/.

Badges

Build Status Code Climate

License

This software is licensed under the LGPL license.