Hi,
I think that the Doodle Rubygem might be a good fit for this purpose
Indeed it is - see the code below. This requires the latest version
0.1.6 (which among other things renames 'attributes' to avoid name
clashes). Classes that define classes are the happiest classes
# see ruby-talk:300767
require 'rubygems'
# note: this requires doodle 0.1.6+
require 'doodle'
# set up classes to manage class definitions of form:
# title
erson
# attribute :name, String
# attribute :age, Fixnum
# constraint :name, 'name != nil'
# constraint :name, 'name.size > 1'
module ClassDef
class Attribute < Doodle
has :name, :kind => Symbol
has :kind, :kind => Class
end
class Constraint < Doodle
has :key, :kind => Symbol
has :condition, :kind => String
end
class Definition < Doodle
has :title, :kind => Symbol
has :attributes, :collect => Attribute
has :constraints, :collect => Constraint
# the names don't have to match - you could have this, for example:
# has :validations, :collect => { :constraint => Validation }
# create a new object from a string containing Ruby source for
# an initialization block - this method works with any Doodle
# class
def self.load(str, context = self.to_s + '.load')
begin
new(&eval("proc { #{str} }", binding, context))
rescue SyntaxError, Exception => e
raise e, e.to_s, [caller[-1]]
end
end
end
# this is the core method that defines a class
def self.define(source, namespace = Object, superclass = Doodle)
# read class definition
cd = Definition.load(source, 'example')
# create anonymous class
klass = Class.new(superclass) do
include Doodle::Core if !(superclass <= Doodle)
cd.attributes.each do |attribute|
has attribute.name, :kind => attribute.kind
end
# the constraints as given work as class level constraints in
# Doodle so that's what we're setting up here
cd.constraints.each do |constraint|
must "have " + constraint.condition do
instance_eval(constraint.condition)
end
end
end
# associate anonymous class with constant name
namespace.const_set(cd.title, klass)
# and add factory method/shorthand constructor (if wanted) - has
to happen ~after~ class has a name
klass.class_eval { include Doodle::Factory }
klass
end
end
# demo
source = %[
title
erson
attribute :name, String
attribute :age, Fixnum
constraint :name, 'name != nil'
constraint :name, 'name.size > 1'
constraint :name, 'name =~ /^[A-Z]/'
constraint :age, 'age >= 0'
]
# install class definitions in their own namespace - you don't have to
# do this - this is just to show how
module Outer
module Inner
end
end
ClassDef.define(source, Outer::Inner)
# return value or exception from block
def try(&block)
begin
block.call
rescue Exception => e
e
end
end
module Outer::Inner
# example use of newly defined class (run this file with xmpfilter
to display output)
try { person = Person.new :name => "Arthur", :age => 42 } # =>
#<Outer::Inner:
erson:0xb7d3b410 @age=42, @name="Arthur">
try { person = Person.new :name => "arthur", :age => 42 } # =>
#<Doodle::ValidationError: Outer::Inner:
erson must have name =~
/^[A-Z]/>
try { person = Person.new :name => "Arthur", :age => -1 } # =>
#<Doodle::ValidationError: Outer::Inner:
erson must have age >= 0>
try { person = Person.new :name => "", :age => -1 } # =>
#<Doodle::ValidationError: Outer::Inner:
erson must have name.size >
1>
try { person = Person.new :name => nil, :age => -1 } # =>
#<Doodle::ValidationError: Outer::Inner:
erson.name must be String -
got NilClass(nil)>
try { person = Person.new :name => "", :age => 42 } # =>
#<Doodle::ValidationError: Outer::Inner:
erson must have name.size >
1>
try { person = Person.new :name => nil, :age => 42 } # =>
#<Doodle::ValidationError: Outer::Inner:
erson.name must be String -
got NilClass(nil)>
try { person = Person.new :name => "Arthur", :age => "1" } # =>
#<Doodle::ValidationError: Outer::Inner:
erson.age must be Fixnum -
got String("1")>
# or using Doodle postitional args
try { person = Person.new("Arthur", 42) } # =>
#<Outer::Inner:
erson:0xb7d2ae6c @age=42, @name="Arthur">
# and shorthand constructor
try { person = Person("Arthur", 42) } # =>
#<Outer::Inner:
erson:0xb7d2893c @age=42, @name="Arthur">
try { person = Person
name => "Arthur", :age => 42) } # =>
#<Outer::Inner:
erson:0xb7d263f8 @age=42, @name="Arthur">
end
require 'yaml'
try { Outer::Inner:
erson.new
name => "Arthur", :age => 42).to_yaml
} # => "--- !ruby/object:Outer::Inner:
erson \nage: 42\nname:
Arthur\n"
Apologies if this looks a mess - you may have to edit line breaks to
get the code to work.
Regards,
Sean