Reading from file, create a class with variables

Hi, I’m trying to load a file with specifications like:

title :Person
attribute :name, String
attribute :age, Fixnum
constraint :name, ‘name != nil’
constraint :name, ‘name.size > 0’
constraint :name, ‘name =~ /^[A-Z]/’
constraint :age, ‘age >= 0’

After which I want to create a class Person, with variables name being a
string, variable age being a fixed number and also the constraints for
them. So far I’ve been using this guide:
http://www.artima.com/rubycs/articles/ruby_as_dsl.html

The problem is by using this I only get the variable @title = :Person
@attribute = name, String and @constraint = age, ‘age >= 0’. I know why
I’m only getting these variables, but I cant find a way to somehow read
the specification and creating the variables @name being a String, @age
being a Fixnum and these different constraints to be used later on in
for example if-conditions.

Any ideas how these specifications can be read and declared?

FYI: The formatting cant be changed and the names (name, age, Person)
can be different.

Pelle S. wrote:

Hi, I’m trying to load a file with specifications like:

title :Person
attribute :name, String
attribute :age, Fixnum
constraint :name, ‘name != nil’
constraint :name, ‘name.size > 0’
constraint :name, ‘name =~ /^[A-Z]/’
constraint :age, ‘age >= 0’

Im also curious about how this could be solved…

Hi,

I think that the Doodle Rubygem might be a good fit for this purpose,
unless
this is just an exercise to further your Ruby-fu.

To make sure something is a string, you can use the kind_of? or is_a?
methods. I would use kind_of? in this case (for the differences, check
class Object - RDoc Documentation ).

Then, you just have to make sure things set with object.name = “ssdfd”
are
Strings using define_method:

attr_name = :age
attr_type = Fixnum

klass.class_eval do
define_method “#{attr_name}=” do |arg|
self.instance_variable_set(“@#{attr_name}”, arg) if arg.type_of?
attr_type
end
end

And something similar for the initialize method.

Dan

On Tue, May 6, 2008 at 11:33 AM, Pelle S. [email protected] wrote:

After which I want to create a class Person, with variables name being a
string, variable age being a fixed number and also the constraints for
them.

Any ideas how these specifications can be read and declared?

This is my first try at solving this problem. It might not be very good,
and I would also like people’s comments on my solution, since I would
like to learn how to do these things better:

class ClassGenerator
def initialize
@constraints = {}
@attr_names = []
end

def title the_title
@class_name = the_title
end

def attribute attr, klazz
@attr_names << attr
# First constraint is to check the class
@constraints[attr] = [“#{attr}.is_a? #{klazz}”]
end

def constraint attr, constr
@constraints[attr] << constr
end

def generate data
instance_eval data
klazz = Class.new
klazz.class_eval “attr_reader #{@attr_names.map{|k|
“:#{k}”}.join(”,“)}”
init_params = @attr_names.join(“,”)
initialize_body = “”
@attr_names.each do |attr|
@constraints[attr].each do |constraint|
initialize_body << “raise ArgumentError.new(‘#{constraint}’)
unless #{constraint};”
end
initialize_body << “@#{attr}=#{attr};”
end
klazz.class_eval “def initialize(#{init_params});
#{initialize_body}; end”
Object.const_set @class_name, klazz
end
end

The data could be read from a file, obviously

data =<<EOD
title :Person
attribute :name, String
attribute :age, Fixnum
constraint :name, ‘name != nil’
constraint :name, ‘name.size > 0’
constraint :name, ‘name =~ /^[A-Z]/’
constraint :age, ‘age >= 0’
EOD

This should create a Person class, with

an initialize method with a param for each attribute

which checks the constraints raising ArgumentError

if not passed, and assigning to an instance variable

It also creates attr_readers for the attributes.

ClassGenerator.new.generate data

So now we can do:

a = Person.new “A”, 3
puts a.name
puts a.age

These should fail with ArgumentError

The msg of the error contains the constraint

Person.new “A”, -3
Person.new “a”, 3
Person.new ‘’, 3

Hope this helps and I would appreciate any comment on my code.

Jesus.

It’s not evident in the example I gave in my last post that Doodle
validations work on the attributes themselves, not just in
initialization, e.g.

person = Outer::Inner::Person.new(:name => “Arthur”, :age => 42)
try { person.age = “42” } # => #<Doodle::ValidationError:
Outer::Inner::Person.age must be Fixnum - got String(“42”)>

Regards,
Sean

On Wed, May 7, 2008 at 5:03 PM, Daniel F. [email protected] wrote:

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 :slight_smile:

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 :Person

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 :Person
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::Person:0xb7d3b410 @age=42, @name=“Arthur”>
try { person = Person.new :name => “arthur”, :age => 42 } # =>
#<Doodle::ValidationError: Outer::Inner::Person must have name =~
/^[A-Z]/>
try { person = Person.new :name => “Arthur”, :age => -1 } # =>
#<Doodle::ValidationError: Outer::Inner::Person must have age >= 0>
try { person = Person.new :name => “”, :age => -1 } # =>
#<Doodle::ValidationError: Outer::Inner::Person must have name.size >
1>
try { person = Person.new :name => nil, :age => -1 } # =>
#<Doodle::ValidationError: Outer::Inner::Person.name must be String -
got NilClass(nil)>
try { person = Person.new :name => “”, :age => 42 } # =>
#<Doodle::ValidationError: Outer::Inner::Person must have name.size >
1>
try { person = Person.new :name => nil, :age => 42 } # =>
#<Doodle::ValidationError: Outer::Inner::Person.name must be String -
got NilClass(nil)>
try { person = Person.new :name => “Arthur”, :age => “1” } # =>
#<Doodle::ValidationError: Outer::Inner::Person.age must be Fixnum -
got String(“1”)>

or using Doodle postitional args

try { person = Person.new(“Arthur”, 42) } # =>
#<Outer::Inner::Person:0xb7d2ae6c @age=42, @name=“Arthur”>

and shorthand constructor

try { person = Person(“Arthur”, 42) } # =>
#<Outer::Inner::Person:0xb7d2893c @age=42, @name=“Arthur”>
try { person = Person(:name => “Arthur”, :age => 42) } # =>
#<Outer::Inner::Person:0xb7d263f8 @age=42, @name=“Arthur”>
end
require ‘yaml’
try { Outer::Inner::Person.new(:name => “Arthur”, :age => 42).to_yaml
} # => “— !ruby/object:Outer::Inner::Person \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

On May 6, 2008, at 3:33 AM, Pelle S. wrote:

Hi, I’m trying to load a file with specifications like:

title :Person
attribute :name, String
attribute :age, Fixnum
constraint :name, ‘name != nil’
constraint :name, ‘name.size > 0’
constraint :name, ‘name =~ /^[A-Z]/’
constraint :age, ‘age >= 0’

i’d do something like

cfp:~ > cat a.rb
class Specd
alias_method ‘eval’, ‘instance_eval’

instance_methods.each{|m| undef_method m unless m[%r/__/]}

def Specd.build config
c = Class.new
new(c).eval(config)
Object.send :const_set, c.title, c
Object.send :const_get, c.title
end

def initialize c
@class = c
@singleton_class =
class << @class
self
end
end

def title name
@class.module_eval{ @title = name }
@singleton_class.module_eval{ attr_accessor :title }
end

def attribute name, type
constraint name, type
end

def constraint key, constraint
@class.module_eval do
key = key.to_s

   ( ( @@constraints ||= Hash.new )[key] ||= [] ).push( constraint )

   reader, writer, ivar = "#{ key }", "#{ key }=", "@#{ key }"

   unless instance_methods(false).include?(key)
     attr reader

     define_method(writer) do |value|
       previous = instance_variable_get ivar
       begin
         instance_variable_set ivar, value
         @@constraints[key].each do |constr|
           ok =
             case constr
               when Module
                 constr === value
               when String
                 instance_eval(constr)
               else
                 true
             end
           raise ArgumentError, "#{ value.inspect } [#{ constr }]"

unless ok
end
rescue Object => e
instance_variable_set ivar, previous
raise
end
end
end
end
end
end

Specd.build(
<<-text
title :Person
constraint :name, ‘name =~ /^Z/’
attribute :name, String
attribute :age, Fixnum
constraint :name, ‘name != nil’
constraint :name, ‘name.size > 0’
constraint :age, ‘age >= 0’
text
)

person = Person.new
person.name = ‘Zaphod’
person.age = 42

p person

begin
person.name = ‘lowercase’
rescue
puts $!.message
end

begin
person.age = -42
rescue
puts $!.message
end

cfp:~ > ruby a.rb
#<Person:0x24db0 @age=42, @name=“Zaphod”>
“lowercase” [name =~ /^Z/]
-42 [age >= 0]

a @ http://codeforpeople.com/

On May 10, 2008, at 10:21 AM, Sean O’Halpin wrote:

Always a pleasure reading your code. One question: why are you
aliasing instance_eval?

Regards,
Sean

hmmm. well this:

class Specd
alias_method ‘eval’, ‘instance_eval’

instance_methods.each{|m| undef_method m unless m[%r/__/]}

def Specd.build config
c = Class.new
new(c).eval(config)
Object.send :const_set, c.title, c
Object.send :const_get, c.title
end

basically says

  • keep a handle on instance_eval

  • blow away all public methods

  • build Specd objects by creating a class (which has only a few
    instance methods like ‘attribute’ and ‘constraint’) and evaluating the
    config definition in there.

so i needed the alias to be able to do the instance eval.

the point of blowing away all the methods in spec’d is so i can
intercept any DSL-ish methods and apply them to the class i’m
building. using this sort of DSL wrapper allows me to build up a
class with a dsl without littering the class itself with useless DSL
crap. for instance

class Model

  has_many :foos

end

relies on Model having a has_many method. this is sometimes not
desirable as it may require, for instance, inheriting from some
abstract base type. with the ‘dsl as wrapper approach’ one can do

Model = DSL.build do

  has_many :foos

end

and Model can be a totally ‘normal’ class - all the special DSL-y
goodness on how to build up stuff is contained in the DSL object,
which has a @model instance and all methods stripped except the dsl
ones.

this code works very similarly

http://codeforpeople.com/lib/ruby/configuration/configuration-0.0.5/lib/configuration.rb

it’s more complex for sure but the usage should make it clear enough

http://codeforpeople.com/lib/ruby/configuration/configuration-0.0.5/README

cheers.

a @ http://codeforpeople.com/

Always a pleasure reading your code. One question: why are you
aliasing instance_eval?

Regards,
Sean

On Sat, May 10, 2008 at 5:44 PM, ara.t.howard [email protected]
wrote:

class Specd
alias_method ‘eval’, ‘instance_eval’

instance_methods.each{|m| undef_method m unless m[%r/__/]}

D’oh! Overlooked the obvious.

the point of blowing away all the methods in spec’d is so i can intercept
any DSL-ish methods and apply them to the class i’m building. using this
sort of DSL wrapper allows me to build up a class with a dsl without
littering the class itself with useless DSL crap.

Namespaced methods would help. Agreed about littering - I’m currently
refactoring doodle to be as unobtrusive as possible.

Model = DSL.build do

has_many :foos

end

and Model can be a totally ‘normal’ class - all the special DSL-y goodness
on how to build up stuff is contained in the DSL object, which has a @model
instance and all methods stripped except the dsl ones.

You could also have

DSL(Model) do
has_many :foos
end

or the like to decorate existing classes. I’m using a similar approach
in one of my ‘least footprint’ experimental versions of doodle.
In miniature, something like this:

class ClassExtensions
attr_accessor :model
def initialize(model, &block)
@model = model
end

DSL method

def has(*args)
model.class_eval { attr_accessor(*args) }
end
end

def DSL(klass, &block)
ce = ClassExtensions.new(klass)
ce.instance_eval(&block)
end

class Foo
end

DSL(Foo) do
has :name
end

foo = Foo.new
foo.name = ‘Trillian’
foo.name # => “Trillian”
p Foo.methods - Object.methods

>> []

which is basically the same approach as yours I believe.

this code works very similarly

http://codeforpeople.com/lib/ruby/configuration/configuration-0.0.5/lib/configuration.rb

Yup, and very instructive it is too :slight_smile:

Regards,
Sean

On May 10, 2008, at 12:29 PM, Sean O’Halpin wrote:

DSL(Foo) do
has :name
end

foo = Foo.new
foo.name = ‘Trillian’
^^^^^^
heh

foo.name # => “Trillian”
p Foo.methods - Object.methods

>> []

which is basically the same approach as yours I believe.

yup, exactly. it’s so liberating to have clean slate for the dsl -
this is my current preferred approach.

cheers.

a @ http://codeforpeople.com/