Class: Xenon::API

Inherits:
Object
  • Object
show all
Includes:
Routing::Directives
Defined in:
lib/xenon/api.rb

Constant Summary collapse

DEFAULT_MARSHALLERS =
[JsonMarshaller.new]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Routing::SecurityDirectives

#authenticate, #authorize, #optional_authenticate

Methods included from Routing::RouteDirectives

#complete, #extract, #extract_request, #fail, #map_request, #map_response, #reject

Methods included from Routing::PathDirectives

#path, #path_end, #path_prefix

Methods included from Routing::ParamDirectives

#param_hash, #params

Methods included from Routing::MethodDirectives

#get, #request_method

Methods included from Routing::MarshallingDirectives

#body

Methods included from Routing::HeaderDirectives

#header, #optional_header, #respond_with_header

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



30
31
32
# File 'lib/xenon/api.rb', line 30

def context
  @context
end

Class Method Details

.marshallers(*marshallers) ⇒ Object



12
13
14
15
# 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) ⇒ Object



37
38
39
40
41
42
43
# 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) ⇒ Object



17
18
19
# 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) ⇒ Object



21
22
23
24
25
26
27
# 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

.routesObject



33
34
35
# File 'lib/xenon/api.rb', line 33

def routes
  @routes ||= []
end

Instance Method Details

#call(env) ⇒ Object



46
47
48
# File 'lib/xenon/api.rb', line 46

def call(env)
  dup.call!(env)
end

#call!(env) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 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) ⇒ Object



85
86
87
88
89
90
91
92
93
# 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) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# 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