DSL help?


#1

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


#2

Ezra Z. 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 :slight_smile:


#3

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


#4

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

Gabriele-

Ahh thanks for that I will give it a look.

Cheers-
-Ezra


#5

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


#6

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