Forum: Ruby DSL help?

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
6076c22b65b36f5d75c30bdcfb2fda85?d=identicon&s=25 Ezra Zygmuntowicz (Guest)
on 2006-01-03 19:56
(Received via mailing list)
Hey rubyists-

	I was wondering if someone could help me make this small dsl I wrote
into a little bit better syntax. The Cond class just takes a block in
its constructor and converts what's inside the block into an sql
where clause with ? syntax like this:

c = Cond.new do
   first_name â??=â??, â??Ezraâ??
   start_date â??betweenâ??, â??2006-01-01â?², â??2006-01-30â?²
   last_name â??likeâ??, â??Zyg%â??
   sql â??hosts.id = something.id'
end

p c.where
#=> ["first_name = ? and start_date between ? and ? and last_name
LIKE ? and hosts.id = something.id", "Ezra", "2006-01-01",
"2006-01-30", "Zyg%"]

	I would like to be able to get rid of the quotes around the
operators '=', '<=', 'LIKE' and so on to become =, <= and LIKE . Also
I would like to be able to get rid of the need for commas inside the
block as well. Inside of the Cond class initialize method it justs
uses instance_eval &block to get the block contents and then uses
method_missing to build a nested array for each statement.

	Anyone have any better ideas to offer on how to make this interface
a little nicer? Thanks in advance.


  class Cond
     # Uses a block to initialize the condition:
     # c = InVisible::Cond.new do
     #    month '<=', 11
     #    year '=', 2005
     #    name 'LIKE', 'ruby%'
     #  end
     #
     # c.where -> ["month <= ? and year = ? and name LIKE ?", 11,
2005, "ruby%"]
     #
     # to include direct SQL, use like this:
     # c = InVisible::Cond.new do
     #   sql "hosts.id = logs.host_id and hosts.name", 'like',
"123.23.45.67"
     # end
     # if a value needs to by typed (f.e. in Postgres: "ip <
inet ?"), use a form of:
     # c = InVisible::Cond.new do
     #    ip '= inet', '123.34.56.78/24'
     # end
     #
     # to expand an existing condition, use the << method
     # c << ['age', '>', 30]

       def initialize(&block)
         @args = []
         instance_eval(&block) if block_given?
       end

       def method_missing(sym, *args)
           @args << [sym,args.flatten].flatten
       end

       def <<(*args)
          @args << [args.flatten].flatten
       end

       def where(args=@args)
           q = []
           ary = []
           args.each do |pair|
             iv = pair[1..99]
             unless iv.last.nil? || iv.last.to_s == ''
                 if pair[0].to_s =~ /^sql.*/ then
                    pair[0] = iv.shift
                 end
                 case iv.size
                 when 0:
                     q << "#{pair[0]}"   # the case when there is
only one (sql) statements
                 when 1:
       	            q << "#{pair[0]} = ?"
       	            ary << iv.last
       	        when 2:
       	            operator = iv[0]
       	            q << "#{pair[0]} #{operator} ?"
   	                ary << iv.last
   	            when 3:
   	                op = case iv[0]
                             when 'between': "between ? and ?"
                         end
                     q << "#{pair[0]} #{op}"
                     ary << iv[-2] << iv[-1]
   	            end
       	    end
           end
           return [q.join(" and ")].concat(ary)
       end

  end


Cheers-
-Ezra
32edd0717b3144d5c58a352d613abdc9?d=identicon&s=25 gabriele renzi (Guest)
on 2006-01-03 20:24
(Received via mailing list)
Ezra Zygmuntowicz ha scritto:
>   last_name â??likeâ??, â??Zyg%â??
> would like to be able to get rid of the need for commas inside the
> block as well. Inside of the Cond class initialize method it justs  uses
> instance_eval &block to get the block contents and then uses
> method_missing to build a nested array for each statement.

look on RAA for the "Criteria" package, you may find it interesting and
a moe tested than your own. At least you can fish in it for good ideas
if you don't want to use it :)
788622ab7e1f792ca653eeacbe5c1db3?d=identicon&s=25 rcoder (Guest)
on 2006-01-03 21:15
(Received via mailing list)
Below is just my 20-minute version -- consider it a source of ideas for
doing a more complete DSL, not a real library.

With it, you can do something like the following:

q = Query.new do
  foo == 'bar'       => exact value
  baz <=> (1..100)   => 'between'
  woo =~ 'substri%'  => 'like'
  fiz < 10           => lt, gt, leq, geq, etc., should all "just work"
end

q.to_sql =>
["foo = ? AND baz BETWEEN ? AND ? AND fiz < ?", ["bar", 1, 100, 10]]

(#to_sql returns query and array of bind params)

# query.rb
class Clause
  attr_reader :name, :test, :value

  def initialize(name)
    @name = name
  end

  def ==(other)
    @test = :equals
    @value = other
  end

  def =~(pattern)
    @test = :like
    @value = pattern
  end

  def <=>(range)
    @test = :between
    @value = range
  end

  def to_sql
    case @test
    when :equals
      ["#{@name} = ?", @value]
    when :like
      ["#{@name} LIKE ?", @value]
    when :between
      ["#{@name} BETWEEN ? AND ?", [@value.begin, @value.end]]
    else
      ["#{@name} #{@test} ?", @value]
    end
  end

  def method_missing(name, *args)
    @test = name
    @value = args.first
  end
end

class Query
  attr_reader :vars

  def initialize(&block)
    @vars = []
    instance_eval(&block)
  end

  def method_missing(name, *args)
    puts "Query#method_missing(#{([name]+args).join(', ')})" if $DEBUG
    cv = Clause.new(name)
    @vars << cv
    cv
  end

  def to_sql(bool='AND')
    params = []
    query = []

    @vars.each do |cv|
      q,p = cv.to_sql
      query << q
      params << p
    end

    [query.join(" #{bool} "), params.flatten]
  end
end
6076c22b65b36f5d75c30bdcfb2fda85?d=identicon&s=25 Ezra Zygmuntowicz (Guest)
on 2006-01-03 21:46
(Received via mailing list)
On Jan 3, 2006, at 12:12 PM, rcoder wrote:

>   fiz < 10           => lt, gt, leq, geq, etc., should all "just work"
> end
>
> q.to_sql =>
> ["foo = ? AND baz BETWEEN ? AND ? AND fiz < ?", ["bar", 1, 100, 10]]

Thanks rcoder-

	That will definitely help me move forward. I appreciate it.


Thanks-
-Ezra
6076c22b65b36f5d75c30bdcfb2fda85?d=identicon&s=25 Ezra Zygmuntowicz (Guest)
on 2006-01-03 21:50
(Received via mailing list)
On Jan 3, 2006, at 11:22 AM, gabriele renzi wrote:

>>   sql â??hosts.id = something.id'
>> contents and then uses  method_missing to build a nested array for
>> each statement.
>
> look on RAA for the "Criteria" package, you may find it interesting
> and a moe tested than your own. At least you can fish in it for
> good ideas if you don't want to use it :)
>


Gabriele-

	Ahh thanks for that I will give it a look.


Cheers-
-Ezra
Bbd147034f04bc4f8f3c3652cd2a2194?d=identicon&s=25 Francis Hwang (Guest)
on 2006-01-03 22:53
(Received via mailing list)
gabriele renzi wrote:
> look on RAA for the "Criteria" package, you may find it interesting and
> a moe tested than your own. At least you can fish in it for good ideas
> if you don't want to use it :)

Or Lafcadio ( http://rubyforge.org/projects/lafcadio/ ) which
unabashedly stole the idea from Criteria and altered it slightly. Maybe
improved it? Who's to say.

For example:

users = User.get { |u| u.fname.equals( 'Francis' ) & u.lname.equals(
'Hwang' ) }
This topic is locked and can not be replied to.