DSL help?

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

Ezra Zygmuntowicz ha scritto:

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.

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

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

Thanks rcoder-

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

Thanks-
-Ezra

···

On Jan 3, 2006, at 12:12 PM, rcoder wrote:

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

Gabriele-

  Ahh thanks for that I will give it a look.

Cheers-
-Ezra

···

On Jan 3, 2006, at 11:22 AM, gabriele renzi wrote:

Ezra Zygmuntowicz ha scritto:

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.

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

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

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' ) }