doodle

introduction

Version 0.1.7

Doodle 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

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, include Doodle::Factory:
  class Date
    include Doodle::Factory
  end

You 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

Generated with
Rote