Reindeer - Moose sugar in ruby
Takes Ruby's existing OO features and extends them with some sugar borrowed from Moose.
Installation
gem install reindeer
Usage
require 'reindeer'
class Point < Reindeer
has :x, is: :rw, is_a: Integer
has :y, is: :rw, is_a: Integer
end
class Point3D < Point
has :z, is: :rw, is_a: Integer
endFeatures
These features are supported to a lesser or greater extent:
Construction
The build method can be used where one may have previously used
initialize. It is called after all attributes have been setup so,
laziness permitting, the object should be in a known state.
Another facet of this feature is that each build method is called in
the inheritance chain from most-derived to least.
Attributes
Declared with the alternative syntax has they provide
additional functionality while still remaining pure Ruby attributes
under the hood.
Their values can be passed to the new constructor in a hash where
the symbolic keys map to attributes of the same. When a value is
specified for a lazy attribute it obviates the laziness.
The following options are supported:
is (aka accessors)
Available in 3 flavours:
:ro:rw:bare
The first two provide accessors like attr_reader and
attr_{reader,writer} combined. The third explicitly provides no
accessors which can be useful when delegators are specified.
The default behaviour is :ro.
required (aka required attributes)
If specified with a true value then the attribute must be specified
at build time. Additionally required attributes can't be lazy
attributes.
default (aka default attribute values)
Can take either a value or something callable (e.g a Proc). If a
value is provided it is cloned and if a callable is provided it is
called without any arguments. The resulting value is used to
populate the attribute if it wasn't provided to the constructor at
object construction time or on first access if the attribute is
lazy.
lazy (aka lazily evaluated)
Expects a Boolean and if true then the attribute's value isn't
generated until it is accessed (if at all). If specified the attribute
must also either have a builder or default specified otherwise an
Reindeer::Meta::Attribute::AttributeError is thrown.
lazy_build
If passed true makes the attribute lazy and expects a private
builder method of the same name as the attribute, but prefixed with
build_, to be defined e.g given has :foo, lazy_build: true the
private instance method build_foo should be defined. In addition
clearer and predicate methods will be installed with the prefixes
clear_ and has_ respectively e.g clear_foo! and has_foo?.
handles (aka delegation methods)
Given an array of symbols each one adds an instance method that delegates to a method of the same name on the attribute value.
type_of (aka type constraints)
Expects a class that composes the Reindeer::Role::TypeConstraint
role. At the point a value is about set against an attribute it is
checked against the type constraint, if valid then the value is set if
not then an Reindeer::TypeConstraint::Invalid exception is raised.
Roles
These are implemented in terms of Module and act to serve a similar
purpose. What they provide in addition to Module are required
methods and the attributes described above.
To compose a role in a Reindeer class two expressions are required,
with and meta.compose!, the former behaves like include and the
latter brings in the role attributes and asserts the existence of any
required methods e.g
module Breakable
include Reindeer::Role
has :is_broken, default: -> { false }
requires :fix!
end
class Egg < Reindeer
with Breakable
def fix!
throw :no_dice if is_broken
end
meta.compose!
endThe .does? method can be used to inspect which roles have been
consumed e.g Egg.does?(Breakable) == true.
For further elaboration on the subject of roles see the Moose::Manual::Roles documentation.
Class constraints and Type constraints
Given that Ruby has a well established class system one need only assert an attribute is of a given (existing) class a Reindeer will go to the trouble of asserting that when the attribute value is set e.g
class AccountSqlTable < Reindeer
has :id, is_a: Fixnum
has :owner, is_a: String
has :amount, is_a: Float
# ...
endHowever if you need a specific type of class (e.g strings of a certain
length) then a custom type constraint is needed. These can be defined
simply by composing the Reindeer::Role::TypeConstraint and
implementing a verify method e.g
class Varchar255 < Reindeer
with Reindeer::Role::TypeConstraint
def verify(v)
v.length <= 255
end
meta.compose!
end
class AccountSqlTable # continued from above
has :summary, type_of: Varchar255
endNB The distinction between class and type constraints seems apt at this point but is by no means set in stone. Hopefully the passage of time shall enlighten us on the matter.
Contributing
Pull requests welcome.
Author
Dan Brook <dan@broquaint.com>