Reading from file, create a class with variables

P

Pelle Strul

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.
 
C

Claes Rembrandt

Pelle said:
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...
 
D

Daniel Finnie

[Note: parts of this message were removed to make it a legal post.]

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
http://ruby-doc.org/core/classes/Object.html#M000371 ).

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

Pelle said:
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...
 
J

Jesús Gabriel y Galán

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.
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.
 
S

Sean O'Halpin

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

Sean O'Halpin

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
 
A

ara.t.howard

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/
 
S

Sean O'Halpin

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

Regards,
Sean
 
A

ara.t.howard

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/
 
S

Sean O'Halpin

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.
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.

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.

Yup, and very instructive it is too :)

Regards,
Sean
 
A

ara.t.howard

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/
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top