class Xenon::API

Constants

DEFAULT_MARSHALLERS

Attributes

context[R]

Public Class Methods

marshallers(*marshallers) click to toggle source
# File lib/xenon/api.rb, line 12
def marshallers(*marshallers)
  @marshallers = marshallers unless marshallers.nil? || marshallers.empty?
  (@marshallers.nil? || @marshallers.empty?) ? DEFAULT_MARSHALLERS : @marshallers
end
method_missing(name, *args, &block) click to toggle source
Calls superclass method
# File lib/xenon/api.rb, line 37
def method_missing(name, *args, &block)
  if instance_methods.include?(name)
    routes << [name, args, block]
  else
    super
  end
end
request_marshaller(content_type) click to toggle source
# File lib/xenon/api.rb, line 17
def request_marshaller(content_type)
  marshallers.find { |m| m.unmarshal?(content_type.media_type) }
end
response_marshaller(media_ranges) click to toggle source
# File lib/xenon/api.rb, line 21
def response_marshaller(media_ranges)
  weighted = marshallers.map do |marshaller|
    media_range = media_ranges.find { |media_range| marshaller.marshal?(media_range) }
    [marshaller, media_range ? media_range.q : 0]
  end
  weighted.select { |_, q| q > 0 }.sort_by { |_, q| q }.map { |m, _| m }.last
end
routes() click to toggle source
# File lib/xenon/api.rb, line 33
def routes
  @routes ||= []
end

Public Instance Methods

call(env) click to toggle source
# File lib/xenon/api.rb, line 46
def call(env)
  dup.call!(env)
end
call!(env) click to toggle source
# File lib/xenon/api.rb, line 50
def call!(env)
  @context = Routing::Context.new(Request.new(Rack::Request.new(env)), Response.new)

  accept = @context.request.header('Accept')
  response_marshaller = accept ? self.class.response_marshaller(accept.media_ranges) : self.class.marshallers.first

  catch :complete do
    begin
      if response_marshaller
        self.class.routes.each do |route|
          name, args, block = route
          route_block = proc { instance_eval(&block) }
          send(name, *args, &route_block)
        end
      else
        reject :accept, supported: self.class.marshallers.map(&:media_type)
      end
      handle_rejections(@context.rejections)
    rescue => e
      handle_error(e)
    end
  end

  response_marshaller ||= self.class.marshallers.first
  headers = @context.response.headers.set(Headers::ContentType.new(response_marshaller.content_type))
  if @context.request.allow_response_body?
    body = response_marshaller.marshal(@context.response.body)
  else
    # TODO: Suppress the Content-Lenth heder
    body = []
  end
  resp = @context.response.copy(headers: headers, body: body)
  [resp.status, resp.headers.map { |h| [h.name, h.to_s] }.to_h, resp.body]
end
handle_error(e) click to toggle source
# File lib/xenon/api.rb, line 85
def handle_error(e)
  puts "handle_error: #{e.class}: #{e}\n  #{e.backtrace.join("\n  ")}"
  case e
  when ParseError
    fail 400, e.message
  else
    fail 500, e.message # TODO: Only if verbose errors configured
  end
end
handle_rejections(rejections) click to toggle source
# File lib/xenon/api.rb, line 95
def handle_rejections(rejections)
  puts "handle_rejections: #{rejections}"
  if rejections.empty?
    fail 404
  else
    rejection = rejections.first
    case rejection.reason
    when :accept
      fail 406, "Supported media types: #{rejection[:supported].join(", ")}"
    when :forbidden
      fail 403
    when :header
      fail 400, "Missing required header: #{rejection[:required]}"
    when :method
      supported = rejections.take_while { |r| r.reason == :method }.flat_map { |r| r[:supported] }
      fail 405, "Supported methods: #{supported.map(&:upcase).join(", ")}"
    when :unauthorized
      if rejection[:scheme]
        challenge = Headers::Challenge.new(rejection[:scheme], rejection.info.except(:scheme))
        respond_with_header Headers::WWWAuthenticate.new(challenge) do
          fail 401
        end
      else
        fail 401
      end
    else
      fail 500
    end
  end
end