Reading from file, create a class with variables

Discussion in 'Ruby' started by Pelle Strul, May 6, 2008.

  1. Pelle Strul

    Pelle Strul Guest

    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.
    --
    Posted via http://www.ruby-forum.com/.
     
    Pelle Strul, May 6, 2008
    #1
    1. Advertising

  2. Pelle Strul 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...
    --
    Posted via http://www.ruby-forum.com/.
     
    Claes Rembrandt, May 7, 2008
    #2
    1. Advertising

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

    On 5/7/08, Claes Rembrandt <> wrote:
    >
    > Pelle Strul 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...
    > --
    > Posted via http://www.ruby-forum.com/.
    >
    >
     
    Daniel Finnie, May 7, 2008
    #3
  4. On Tue, May 6, 2008 at 11:33 AM, Pelle Strul <> 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'
    >
    > 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.
     
    Jesús Gabriel y Galán, May 7, 2008
    #4
  5. On Wed, May 7, 2008 at 5:03 PM, Daniel Finnie <> 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 :)

    # 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
     
    Sean O'Halpin, May 8, 2008
    #5
  6. 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
     
    Sean O'Halpin, May 8, 2008
    #6
  7. Pelle Strul

    ara.t.howard Guest

    On May 6, 2008, at 3:33 AM, Pelle Strul 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/
    --
    we can deny everything, except that we have the possibility of being
    better. simply reflect on that.
    h.h. the 14th dalai lama
     
    ara.t.howard, May 8, 2008
    #7
  8. Always a pleasure reading your code. One question: why are you
    aliasing instance_eval?

    Regards,
    Sean
     
    Sean O'Halpin, May 10, 2008
    #8
  9. Pelle Strul

    ara.t.howard Guest

    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/
    --
    we can deny everything, except that we have the possibility of being
    better. simply reflect on that.
    h.h. the 14th dalai lama
     
    ara.t.howard, May 10, 2008
    #9
  10. On Sat, May 10, 2008 at 5:44 PM, ara.t.howard <> 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.

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

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

    Regards,
    Sean
     
    Sean O'Halpin, May 10, 2008
    #10
  11. Pelle Strul

    ara.t.howard Guest

    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/
    --
    we can deny everything, except that we have the possibility of being
    better. simply reflect on that.
    h.h. the 14th dalai lama
     
    ara.t.howard, May 10, 2008
    #11
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Replies:
    1
    Views:
    4,444
    Craig Deelsnyder
    Jan 26, 2005
  2. E11
    Replies:
    1
    Views:
    4,850
    Thomas Weidenfeller
    Oct 12, 2005
  3. Bogdan Tudor
    Replies:
    7
    Views:
    394
    Tom Hawtin
    Jan 11, 2007
  4. chandan
    Replies:
    2
    Views:
    782
    chandan
    Dec 3, 2007
  5. Replies:
    9
    Views:
    990
Loading...

Share This Page