introduction
Version 0.1.7Doodle is a Ruby library and gem for simplifying the definition of Ruby classes by making attributes and their properties more declarative.
Doodle is eco-friendly – it does not globally modify Object, Class or Module, nor does it pollute instances with its own instance variables (i.e. it plays nice with yaml).
Doodle has been tested with Ruby 1.8.6, Ruby 1.9.0 and JRuby 1.1.1. It has not yet been tested with Rubinius (but will be soon :)
Please feel free to post bug reports, feature requests, and any comments or discussion topics to the doodle Google group
features
-
initialization
- using positional arguments
- with named arguments
- by block
- defaults
- initial values
- validation at attribute and class levels
- conversions for attributes and classes
- collectors to help in defining simple DSLs
- works for classes, instances and singletons
Putting all this together, you can initialize objects like this:
event = Event "Festival" do
date '2008-04-01'
place "The muddy field"
place "Beer tent" do
event "Drinking"
end
end
pp event
# >> #<Event:0x111dd4c
# >> @date=#<Date: 4909115/2,0,2299161>,
# >> @locations=
# >> [#<Location:0x1117be0 @events=[], @name="The muddy field">,
# >> #<Location:0x1114148
# >> @events=[#<Event:0x11115b0 @locations=[], @name="Drinking">],
# >> @name="Beer tent">],
# >> @name="Festival">
from a class definition like this:
require 'rubygems'
require 'date'
require 'pp'
require 'doodle'
class Location < Doodle
has :name, :kind => String
has :events, :collect => :Event
end
class Event
# or if you want to inherit from another class
include Doodle::Core
has :name, :kind => String
has :date do
kind Date
default { Date.today }
must 'be >= today' do |value|
value >= Date.today
end
from String do |s|
Date.parse(s)
end
end
has :locations, :collect => {:place => "Location"}
end
installation
On Linux or Mac OS X:
$ sudo gem install doodle
On Windows:
C:\> gem install doodle
initialization
require 'rubygems'
require 'doodle'
class Event < Doodle
has :date
end
With this declaration, you can now initialize an instance of Event in the following ways.
using positional arguments
event = Event Date.today
named arguments
event = Event :date => Date.today
block initialization
event = Event do
date Date.today
end
Of course, if you insist on typing new, you
can:
event = Event.new(:date => Date.today)
defaults
Doodle assumes that attributes specified without defaults or initial values (see below) are required. If you try to initialize an instance without providing values for all required attributes, Doodle will raise an exception. For example:
class Event < Doodle
has :date
end
event = Event()
# or
event = Event.new
will result in:
ArgumentError: #<Event:0x72b118> missing required attribute 'date'
To specify a default, use the default
option:
class Event < Doodle
has :date, :default => Date.today
end
Now we can create an Event without specifying a date:
event = Event()
=> #<Event:0x71343c>
event.date
=> #<Date: 4909061/2,0,2299161>
You can of course override the default on initialization or later:
event = Event(:date => Date.new(2008, 03, 01))
=> #<Event:0x7036e0 @date=#<Date: 4909053/2,0,2299161>>
event.date = Date.new(2008, 03, 05)
=> #<Date: 4909061/2,0,2299161>
Note that if you do not specify a value for an attribute with a default, no instance variable is created.
Side note: to use the class method constructor syntax with other classes, includeDoodle::Factory:class Date include Doodle::Factory endYou can now write:
date = Date(2008, 03, 01)
With the default specified as above, the date
attribute will take on the value current when the class is
defined. If you want the default value to be recalculated every
time, use a Proc object or block as the default value.
class Event < Doodle
has :time, :default => proc { Time.now }
end
# or
class Event < Doodle
has :time do
default { Time.now }
end
end
event.time
=> Wed Mar 05 14:44:01 +0000 2008
event.time
=> Wed Mar 05 14:44:03 +0000 2008
This becomes more useful when you have dependent attributes, as in this example:
class Event < Doodle
has :start_date, :default => Date.today
has :end_date, :default => proc { start_date + 1 }
end
event.start_date.to_s
=> "2008-03-05"
event.end_date.to_s
=> "2008-03-06"
initial values
Sometimes, you don’t want default values set when the class is defined or recalculated every time. Instead you want initial values which are set when the object is instantiated. Also, default values are fine for simple scalar objects, but can cause problems with aggregates such as Arrays because you end up sharing them among all instances:
class Event < Doodle
has :locations, :default => []
end
e1 = Event()
e2 = Event()
e1.locations.object_id == e2.locations.object_id
=> true
In these cases, use init instead of
default:
class Event < Doodle
has :locations, :init => []
end
e1 = Event()
e2 = Event()
e1.locations.object_id == e2.locations.object_id
=> false
Internally, Doodle copies the init value by
cloning it. Unlike default, init
does create an instance variable.
However, we can set still the date attribute to
any value we like.
event = Event :start_date => "Hello"
event.start_date
=> "Hello"
event.end_date
=> TypeError: can't convert Fixnum into String
Hmmm… that doesn’t seem right. We can restrict the kinds of
values the date attribute will accept with
validations.
validations
by kind
class Event < Doodle
has :start_date, :kind => Date
end
Doodle uses kind_of? to test the value, so
kind can be set to any Module (e.g. Enumerable),
Class or superclass that makes sense for your application.
If you now try to initialize Event#date with something that isn’t a Date:
event = Event.new(:date => "Hello")
you’ll get a Doodle::ValidationError exception:
date must be Date - got String("Hello") (Doodle::ValidationError)
specific validations with must
To specify a validation on an attribute, use
must inside the attribute’s definition block:
class Event < Doodle
has :start_date, :kind => Date do
must "be >= today" do |value|
value >= Date.today
end
end
end
event = Event :start_date => Date.today
event.start_date = Date.parse('2001-01-01')
=> start_date must be >= today - got Date(#<Date: 4903821/2,0,2299161>) (Doodle::ValidationError)
The must block should return true if the value
is valid, false otherwise. A failed validation will raise a
Doodle::ValidationError exception.
Attribute validations happen before the instance variable is changed.
You can also specify validations for the object as a whole:
class Event < Doodle
has :start_date, :kind => Date do
must "be >= today" do |value|
value >= Date.today
end
end
has :end_date, :kind => Date do
default { start_date }
end
must "have end_date >= start_date" do
end_date >= start_date
end
end
event = Event :start_date => Date.today
event.end_date = Date.parse('2001-01-01')
=> #<Event:0x657ed0 @start_date=#<Date: 4909061/2,0,2299161>, @end_date=#<Date: 4903821/2,0,2299161>> must have end_date >= start_date (Doodle::ValidationError)
Note that you don’t need to project the value into the block but you can if you like – its value is the object instance itself.
Object level validations occur after all instance variables have been set.
validating data loaded from a yaml source
YAML::load sets an object’s
instance variables directly, bypassing its attribute accessors.
In a Doodle context, this means that loading from a yaml source
bypasses attribute validations. You can apply validations
‘manually’ by using the validate! method (the
exclamation mark denotes that this method can raise an
exception). For convenience, validate! returns the
validated object on success, so you can get a validated object
from a yaml source using the following:
foo = YAML::load(yaml_source).validate!
The following will raise an exception, complaining that ‘name’ is missing:
require "rubygems"
require "doodle"
require "yaml"
class Foo < Doodle
has :name
has :date
end
str = %[
--- !ruby/object:Foo
date: 2000-07-01
]
# load from string
foo = YAML::load(str).validate!
# => #<Foo:0x10c4cb0> missing required attribute 'name' (ArgumentError)
conversions
Even when you want to restrict an attribute to a particular
kind, it is often convenient to allow initialization from
values that can be converted to the target type. To do this in
Doodle, use the from method inside an attribute’s
definition block:
class Event < Doodle
has :start_date, :kind => Date do
from String do |value|
Date.parse(value)
end
end
has :end_date, :kind => Date do
from String do |value|
Date.parse(value)
end
end
end
event = Event '2008-03-05', '2008-03-06'
event.start_date.to_s # => "2008-03-05"
event.end_date.to_s # => "2008-03-06"
event.start_date = '2001-01-01'
event.start_date # => #<Date: 4903821/2,0,2299161>
event.start_date.to_s # => "2001-01-01"
You can pass multiple classes or modules to
from – it’s up to you to ensure that it makes
sense to do so. For example:
from Symbol, String do |name|
Event(:name => name.to_s)
end
from also works on the class level:
class Event < Doodle
...
from String do |value|
args = value.split(' to ')
new(*args)
end
end
event = Event.from '2008-03-05 to 2008-03-06'
event.start_date.to_s # => "2008-03-05"
event.end_date.to_s # => "2008-03-06"
Note: the class level interface to from may
change in a future version.
collectors
Collectors provide a means to define a convenience method you can use to add items to a collection. For example:
class Location < Doodle
has :name
end
class Event < Doodle
has :locations, :init => [], :collect => Location
end
event = Event do
location "Stage 1"
location "Stage 2"
end
In the example above, we want locations to be
an array of Location objects. Using the
:collect => Location option defines a method
called location (derived from the class name).
Each time the location method is called a new
Location object is added to the
locations array
You can leave out the :init option if all you
want is to collect into an array – doodle will supply one for
you. Otherwise, you need to supply an Enumerable that provides
the << method.
To use a specific name for the collecting method, pass a
hash containing {:method_name => ClassName} to
the collect option like this:
has :locations, :collect => {:place => Location}
...
event = Event do
place "Stage 1"
place "Stage 2"
end
ClassName can be a ClassConstant, a :Symbol or
a “String”.
Finally, if you don’t want to restrict the value to a particular class, you can just specify a method name:
has :locations, :collect => :place
...
event = Event do
place "Stage 1"
place 42
end
keyed collections
You can also build keyed collections using
:collect. Use
:key => :method_name
to tell doodle which method to call on the collected object.
If you don’t specify an :init value, doodle will
create a hash to hold the collected items. For example:
class Item < Doodle
has :name
has :value
end
class Foo < Doodle
has :list, :collect => Item, :key => :name
end
foo = Foo do
item 'A', 1
item 'B', 2
end
foo.list['A'] # => #<Item:0xb7cf76ac @value=1, @name="A">
If you do specify an :init value, it
should provide a #[] method that accepts the type
of the :key specified.
classes and singletons
You can also use has for class attributes:
class Foo < Doodle
class << self
has :doc, :default => "This is the Foo class"
end
end
or singleton instances:
foo = Foo.new
class << foo
has :special, :default => 'This object is special'
end
p foo.special
# => 'This object is special'
links
- Rubyforge project
- Post bug reports, questions, answers to the doodle Google group
