Valuable
quick ruby modeling -- basically attr_accessor with default values, light-weight casting, and a constructor
Install / Use
/learn @mustmodify/ValuableREADME
Introducing Valuable
Valuable enables quick modeling... it's attr_accessor on steroids. Its simple interface allows you to build, change and discard models without hassles, so you can get on with the logic specific to your application.
When working with Rails, Sinatra etc., I find myself creating non-Active-Record classes to create testable classes for:
- reports
- events (model interactions between classes; this code does not belong in either a controller or an ORM model.)
- view helpers ( very hard to test in Rails unless they're in a class like EmployeePresenter or DashboardPresenter )
- incoming / outgoing API handlers (ie MapQuest::GeoCoder or LocalCache::GeoCoder )
- search (
Searchbut alsoEmployeeSearch,EmployeeSearch.new(company: co).incomplete_pto_for(year), etc. - factories
Here's an example of modeling an event, logic that doesn't belong in either a controller or a model:
class EmployeeHireAide < Valuable
has_value :employee, klass: Employee
has_value :hire_date, klass: :date
has_value :current_user
def fire
employee.save.tap do |success|
if success
add_note_about_hiring
create_documentation_checklist
create_user_account
end
end
end
def add_note_about_hiring
Note.create(notable: employee, author: current_user, event: 'Hire', body: "Hired employee on #{hire_date.to_s(:mdy)}")
end
def create_documentation_checklist
ChecklistTemplate.find_by_name('employee_documentation').create_checklist(reference: employee)
end
def create_user_account
... etc ...
end
end
Then in your controller:
class EmployeeController
def create
aide = EmployeeHireAide.new(employee: params[:employee], current_user: current_user, hire_date: params[:hire_date])
if !current_user.can_create?(:employee)
go_away
elsif aide.fire
redirect_to aide.employee
else
render action: :new
end
end
end
Valuable provides DRY decoration like attr_accessor, but includes default values and other formatting (like, "2" => 2), and a constructor that accepts an attributes hash. It provides a class-level list of attributes, an instance-level attributes hash, and more.
Tested with Rubinius, 1.8.7, 1.9.1, 1.9.2, 1.9.3
Version 0.9.x is considered stable.
Valuable was originally created to avoid the repetition of writing the constructor-accepts-a-hash method. It has evolved, but at its core are still the same concepts.
Contents
- Frequent Uses
- Methods ( Class-Level, Instance-Level )
- Installation
- Usage & Examples
- Constructor Accepts an Attributes Hash
- Default Values
- Nil Values
- Aliases
- Formatting Input
- Pre-Defined Formatters
- Extending Values
- Collections
- Formatting Collections
- Extending Collections
- Registering Formatters
- More about Attributes
- Advanced Input Parsing
- Advanced Defaults
- Advanced Collection Formatting
- Other Examples
Frequent Uses
Valuable was created to help you quickly model things. Things I find myself modeling:
- data imported from JSON, XML, etc
- the result of an API call
- a subset of some data in an ORM class say you have a class Person with street, city, state and zip. It might not make sense to store this in a separate table, but you can still create an Address model to hold address-related logic and state like geocode, post_office_box? and Address#==
- as a presenter that wraps a model This way you keep view-specific methods out of views and models.
- as a presenter that aggregates several models Generating a map might involve coordinating several different collections of data. Create a valuable class to handle that integration.
- to model search forms - Use Valuable to model an advanced search form. Create an attribute for each drop-down, check-box, and text field, and constants to store options. Integrates easily with Rails via @search = CustomerSearch.new(params[:search]) and form_for(@search, :url => ...)
- to model reports like search forms, reports can be stateful when they have critiera that can be selected via form.
- as a query builder ie, "I need to create an (Arel or SQL) query based off of form input." (see previous two points)
- experiments / spikes
- factories factories need well-defined input, so valuable is a great fit.
Methods
Class-Level Methods
has_value(field_name, options = {})
creates a getter and setter named field_name
options:
default- provide a default value
class Task < Valuable
has_value :status, :default => 'Active'
end
>> Task.new.status
=> 'Active'
-
alias- create setters and getters with the name of the attribute and also with the alias. See Aliases for more information. -
klass- pre-format the input with one of the predefined formatters, as a class, or with your custom formatter. See Formatting Input for more information.
class Person < Valuable
has_value :age, :klass => :integer
has_value :phone_number, :klass => PhoneNumber
end
>> Person.new(:age => '15').age.class
=> Fixnum
>> jenny = Person.new(:phone_number => '2018675309')
>> jenny.phone_number == PhoneNumber.new('2018675309')
=> true
parse_with- Sometimes you want to instantiate with a method other thannew... one example beingDate.parse
class Person
has_value :dob, :klass => Date, :parse_with => :parse
end
# this will call Date.parse('1976-07-26')
Person.new(:dob => '1976-07-26')
has_collection(field_name, options = {})
like has_value, this creates a getter and setter. The default value is an array.
options:
klass- apply pre-defined or custom formatters to each element of the array.alias- create additional getters and setters under this name.extend- extend the collection with the provided module or modules.
class Person
has_collection :friends
end
>> Person.new.friends
=> []
attributes
an array of attributes you have defined on a model.
class Person < Valuable
has_value :first_name
has_value :last_name
end
>> Person.attributes
=> [:first_name, :last_name]
defaults
A hash of the attributes with their default values. Attributes defined without default values do not appear in this list.
class Pastry < Valuable
has_value :primary_ingredient, :default => :sugar
has_value :att_with_no_default
end
>> Pastry.defaults
=> {:primary_ingredient => :sugar}
register_formatter(name, &block)
Allows you to provide custom code to pre-format attributes, if the included ones are not sufficient. For instance, you might wish to register an 'orientation' formatter that accepts either angles or 'N', 'S', 'E', 'W', and converts those to angles. See registering formatters for details and examples.
Note: as with other formatters, nil values will not be passed to the formatter. The attribute will simply be set to nil. See nil values. If this is an issue, let me know.
acts_as_permissive
Valuable classes typically raise an error if you instantiate them with attributes that have not been predefined. This method makes Valuable ignore any unknown attributes.
Instance-Level Methods
attributes
provides a hash of the attributes and their values.
class Party < Valuable
has_value :host
has_value :theme
has_value :time, :default => '6pm'
end
>> party = Party.new(:theme => 'Black and Whitle')
>> party.attributes
=> {:theme => 'Black and White', :time => '6pm'}
# note that the 'host' attribute was not set by default, at
# instantiation, or via the setter method party.host=, so
# it does not appear in the attributes hash.
update_attributes(atts={})
Accepts a hash of :attribute => :value and updates each associated attributes. Will raise an exception if any of the keys isn't already set up in the class, unless you call acts_as_permissive.
class Tomatoe
has_value :color
end
>> t = Tomatoe.new(:color => 'green')
>> t.color
=> 'green'
>> t.update_attributes(:color => 'red')
>> t.color
=> 'red'
write_attribute(att_name, value)
this method is called by all the setters and, obviously, update_attributes. Using a formatter (if specified), it updates the attributes hash.
class Chicken
has_value :gender
end
>> c = Chicken.new
>> c.gender
=> nil
>> c.write_attribute(:gender, 'F')
>> c.gender
=> 'F'
Installation
if using bundler, add this to your Gemfile:
gem 'valuable'
and the examples below should work.
Usage & Examples
class Person < Valuable
has_value :name
has_value :age, :klass => :integer
has_value :phone_number, :klass => PhoneNumber
# see /examples/phone_number.rb
end
params =
{
'person' =>
{
'name' => 'Mr. Freud',
'age' => "344",
'phone_number' => '8002195642',
'specialization_code' => "2106"
}
}
>> p = Person.new(params[:person])
>> p.age
=> 344
>> p.phone_number
=> (337) 326-3121
>> p.phone_number.class
=> PhoneNumber
"Yeah, I could have just done that myself."
"Right, but now yo
