module CouchdbRubyServer

Constants

COMPILED_PROCS
MIME_TYPES
TYPES_MIME
VERSION

Public Class Methods

compile_proc(source, binding, name) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 64
def self.compile_proc source, binding, name
  source_hash = source.hash
  fn = COMPILED_PROCS[source_hash]
  return fn if fn
  begin
    fn = eval source, binding, name, 1
    raise "#{name} must respond_to #call" unless fn.respond_to?(:call)
    COMPILED_PROCS[source_hash] = fn
  rescue Exception
    respond_error :compilation_error, $!
  end
  fn
end
emit(key, value) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 91
def self.emit(key, value)
  if @@view_emit.nil?
    @@map_results.push [key, value]
  else
    @@view_emit = true
  end
end
enable_tracing() click to toggle source
# File lib/couchdb-ruby-server/tracer.rb, line 3
def self.enable_tracing
  @@trace = true
end
get_row() click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 116
def self.get_row
  return nil if @@last_row
  if not @@got_row
    @@got_row = true
    render_headers
    respond ["start", @@chunks, @@start_resp]
    @@start_resp = {}
  else
    respond ["chunks", @@chunks]
  end
  @@chunks = []
  json = Serializer.load($stdin.gets)
  if json[0] == 'list_end'
    @@last_row = true
    return nil
  elsif json[0] != 'list_row'
    respond_fatal :list_error, "not a row: #{json[0]}"
  end
  json[1]
end
log(message) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 38
def self.log message
  respond ['log', "#{message}"]
end
maybe_wrap_response(resp) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 138
def self.maybe_wrap_response resp
  {:body=>''}.merge(
    resp.respond_to?(:to_hash) ? resp.to_hash : {:body=>"#{resp}"})
end
provides(*args, &block) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 99
def self.provides(*args, &block)
  @@provides_used << {:format => args[0], :block => block}.merge(
    args.size > 1 ? {:mime => args[1]} : {}
  )
  nil
end
render_headers() click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 168
def self.render_headers
  @@start_resp[:headers] ||= {}
  @@start_resp[:headers]["Content-Type"] ||= @@resp_mime if @@resp_mime
end
reset_list() click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 156
def self.reset_list
  @@got_row = false
  @@last_row = false
  @@chunks = []
  @@start_resp = {}
end
reset_provides() click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 163
def self.reset_provides
  @@provides_used = []
  @@resp_mime = nil
end
respond(obj) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 42
def self.respond obj
  trace {"+ respond(#{obj.inspect})"}
  puts Serializer.dump(obj)
rescue
  puts('["log", "' + "respond error converting object to JSON: #{$!.message}\n#{$!.backtrace.join("\n")}" + '"]')
  puts 'false'
end
respond_error(error, reason) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 50
def self.respond_error error, reason
  if reason.respond_to?(:message) && reason.respond_to?(:backtrace)
    reason =  "#{reason.class.name}: #{reason.message}\n#{reason.backtrace.join("\n")}"
  end
  respond ['error', error, reason]
end
respond_fatal(error, reason) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 57
def self.respond_fatal error, reason
  respond_error error, reason
  exit(2)
end
run() click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 199
def self.run
  trace {'+ run'}
  $stdout.sync = true
  log "CouchdbRubyServer started, RUBY_VERSION: #{RUBY_VERSION}"
  #log "RUBY_GC_MALLOC_LIMIT=#{ENV['RUBY_GC_MALLOC_LIMIT']}"
  while cmd = $stdin.gets
    cmd = Serializer.load(cmd)
    trace {"cmd = #{cmd.inspect}"}
    case cmd[0]
    when "reset"
      @@callables = []
      respond(true)
    when "add_fun"
      fn = compile_proc cmd[1], binding, "map (#{cmd[1][0..100]}...)"
      next unless fn
      @@callables << fn
      respond(true)
    when "map_doc"
      results = []
      @@view_emit = nil
      doc = cmd[1]
      doc.freeze
      @@callables.each do |callable|
        @@map_results = []
        begin
          callable.call(doc)
          results.push(@@map_results)
        rescue
          log "map raised exception with doc._id #{doc['_id']}", $!
          results.push([])
        end
      end
      respond(results)
    when "reduce"
      ks, vs = cmd[2].transpose
      run_reduce(cmd[1], ks, vs, false, binding)
    when "rereduce"
      run_reduce(cmd[1], nil, cmd[2], true, binding)
    when "ddoc"
      cmd.shift
      ddoc_id = cmd.shift
      if ddoc_id == 'new'
        ddoc_id = cmd.shift
        @@ddocs[ddoc_id] = cmd.shift
        respond true
      else
        ddoc = @@ddocs[ddoc_id]
        if not ddoc
          respond_fatal :query_protocol_error, "uncached design doc: #{ddoc_id}"
        end
        point = ddoc
        path = cmd.shift
        begin
          path.each do |level|
            point = (point[level] || raise("missing #{level} function in #{ddoc_id}"))
          end
        rescue
          respond_error :not_found, $!
          next
        end
        next unless (fn = compile_proc point, binding, "#{ddoc['_id']}/#{path.join("/")}")
        begin
          response = nil
          args = cmd.shift
          case path[0]
          when "updates"
            respond update_case fn, args
          when "lists"
            #head = args[0]
            reset_list
            reset_provides
            tail = fn.call(*args)
            begin
              tail = run_provides(args[1]) unless @@provides_used.empty?
            rescue
              respond_error :not_acceptable, $!
              next
            end
            get_row unless @@got_row
            @@chunks << tail unless tail.nil?
            respond ["end", @@chunks]
          when "shows"
            respond ["resp", maybe_wrap_response(fn.call(*args))]
          when "filters"
            res = []
            args[0].each do |doc|
              res.push(fn.call(doc, args[1]) ? true : false)
            end
            respond [true, res]
          when "views"
            res = []
            args[0].each do |doc|
              @@view_emit = false
              fn.call doc
              res.push(@@view_emit ? true : false)
            end
            respond [true, res]
          when "validate_doc_update"
            begin
              fn.call(*args)
              respond 1
            rescue
              respond({:forbidden => "bad doc"})
            end
          else
            respond_fatal :unknown_command, "unknown ddoc command #{path[0]}"
            next
          end # case ddoc
        rescue Fatal
          respond_fatal $!.key, $!.message
        rescue Error
          respond_error $!.key, $!.message
        rescue
          respond_error :render_error, $!
        end
      end
    else
      respond_fatal :unknown_command, "unknown command #{cmd[0]}"
    end
  end
end
run_provides(req) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 173
def self.run_provides req
  format = :unknown
  best_fun = if req['query'] and req['query']['format']
    format = req['query']['format']
    @@provides_used.find do |p|
      p[:format].to_s == format
    end
  elsif req['headers'] && (format = req['headers']['Accept'])
    f = nil
    format.split(',').each do |a|
      break if (f = a =~ /^\*\/\*/ ? @@provides_used.first :
        @@provides_used.find do |p|
          a == p[:mime] or p[:format] == TYPES_MIME[a]
        end)
    end
    f
  else
    @@provides_used.first
  end
  raise "format #{format} not supported" unless best_fun
  raise "no block in provides" unless best_fun[:block] &&
                                      best_fun[:block].respond_to?(:call)
  @@resp_mime = best_fun[:mime] || MIME_TYPES[best_fun[:format]]
  best_fun[:block].call
end
run_reduce(srcs, ks, vs, rereduce, b) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 143
def self.run_reduce(srcs, ks, vs, rereduce, b)
  fns = srcs.collect { |src| compile_proc(src, b, "reduce (#{src[0..100]}...)") || return }
  reductions = fns.collect { |fn|
    begin
      fn.call(ks, vs, rereduce)
    rescue
      log "#{rereduce ? 'rereduce' : 'reduce'} raised exception", $!
      nil
    end
  }
  respond [true, reductions]
end
send(chunk) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 111
def self.send(chunk)
  @@chunks << "#{chunk}"
  nil
end
start(resp) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 106
def self.start(resp)
  @@start_resp = resp if resp
  nil
end
trace(str = nil) click to toggle source
# File lib/couchdb-ruby-server/tracer.rb, line 6
def self.trace(str = nil)
  return unless @@trace
  $stderr.puts("QS_TRACE: #{block_given? ? yield : str}")
end
update_case(fn, args) click to toggle source
# File lib/couchdb-ruby-server/server.rb, line 13
def self.update_case fn, args
  result = fn.call(*args)
  return ["up", result[0], maybe_wrap_response(result[1])]
end