Module: ApiHammer::Sinatra

Includes:
Halt
Defined in:
lib/api_hammer/sinatra.rb,
lib/api_hammer/sinatra/halt.rb

Defined Under Namespace

Modules: Halt

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Halt

#halt

Methods included from HaltMethods

#find_or_halt, #halt_accepted, #halt_already_reported, #halt_authentication_timeout, #halt_bad_gateway, #halt_bad_request, #halt_bandwidth_limit_exceeded, #halt_conflict, #halt_created, #halt_error, #halt_expectation_failed, #halt_failed_dependency, #halt_forbidden, #halt_found, #halt_gateway_timeout, #halt_gone, #halt_http_version_not_supported, #halt_im_a_teapot, #halt_im_used, #halt_insufficient_storage, #halt_internal_server_error, #halt_length_required, #halt_locked, #halt_loop_detected, #halt_method_not_allowed, #halt_moved_permanently, #halt_multi_status, #halt_multiple_choices, #halt_network_authentication_required, #halt_no_content, #halt_no_response, #halt_non_authoritative_information, #halt_not_acceptable, #halt_not_extended, #halt_not_found, #halt_not_implemented, #halt_not_modified, #halt_ok, #halt_partial_content, #halt_payment_required, #halt_permanent_redirect, #halt_precondition_failed, #halt_precondition_required, #halt_proxy_authentication_required, #halt_redirect, #halt_request_entity_too_large, #halt_request_header_fields_too_large, #halt_request_timeout, #halt_request_uri_too_long, #halt_requested_range_not_satisfiable, #halt_reset_content, #halt_see_other, #halt_service_unavailable, #halt_temporary_redirect, #halt_too_many_requests, #halt_unauthorized, #halt_unavailable_for_legal_reasons, #halt_unordered_collection, #halt_unprocessable_entity, #halt_unsupported_media_type, #halt_upgrade_required, #halt_use_proxy, #halt_variant_also_negotiates

Instance Attribute Details

#supported_media_typesObject



52
53
54
# File 'lib/api_hammer/sinatra.rb', line 52

def supported_media_types
  instance_variable_defined?(:@supported_media_types) ? @supported_media_types : self.class.supported_media_types
end

Class Method Details

.included(klass) ⇒ Object



10
11
12
13
14
# File 'lib/api_hammer/sinatra.rb', line 10

def self.included(klass)
  (@on_included || []).each do |included_proc|
    included_proc.call(klass)
  end
end

Instance Method Details

#check_accept(options = {}) ⇒ Object



99
100
101
# File 'lib/api_hammer/sinatra.rb', line 99

def check_accept(options = {})
  response_media_type(options.merge(:halt_if_unacceptable => true))
end

#check_params_and_object_consistent(path_params, object) ⇒ Object

for methods where parameters which are required in the path may also be specified in the body, this takes the path_params and the body object and ensures that they are consistent any place they are specified in the body.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/api_hammer/sinatra.rb', line 192

def check_params_and_object_consistent(path_params, object)
  errors = {}
  path_params.each do |(k, v)|
    if object.key?(k) && object[k] != v
      errors[k] = [I18n.t('app.errors.inconsistent_uri_and_entity',
        :key => k,
        :uri_value => v,
        :entity_value => object[k],
        :default => "Inconsistent data given in the request URI and request entity: %{key} was specified as %{uri_value} in the URI but %{entity_value} in the entity",
      )]
    end
  end
  if errors.any?
    halt_error(422, errors)
  end
end

#format_response(status, body_object, headers = {}) ⇒ Object

returns a rack response with the given object encoded in the appropriate format for the requests.

arguments are in the order of what tends to vary most frequently rather than rack's way, so headers come last



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/api_hammer/sinatra.rb', line 107

def format_response(status, body_object, headers={})
  if status == 204
    body = ''
  else
    body = case response_media_type
    when 'application/json'
      JSON.pretty_generate(body_object)
    when 'application/x-www-form-urlencoded'
      URI.encode_www_form(body_object)
    when 'application/xml'
      body_object.to_s
    when 'text/plain'
      body_object
    else
      # :nocov:
      raise NotImplementedError, "unsupported response media type #{response_media_type}"
      # :nocov:
    end
  end
  [status, headers.merge({'Content-Type' => response_media_type}), [body]]
end

#parsed_bodyObject

returns the parsed contents of the request body.

checks the Content-Type of the request, and unless it's supported (or omitted - in which case assumed to be the first supported media type), halts with 415.

if the body is not parseable, then halts with 400.



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/api_hammer/sinatra.rb', line 145

def parsed_body
  request_media_type = request.media_type
  unless request_media_type =~ /\S/
    fallback = true
    request_media_type = supported_media_types.first
  end
  case request_media_type
  when 'application/json'
    begin
      return JSON.parse(request_body)
    rescue JSON::ParserError
      if fallback
        t_key = 'app.errors.request.body_parse_fallback_json'
        default = "Error encountered attempting to parse the request body. No Content-Type was specified and parsing as JSON failed. Supported media types are %{supported_media_types}. JSON parser error: %{error_class}: %{error_message}"
      else
        t_key = 'app.errors.request.body_parse_indicated_json'
        default = "Error encountered attempting to parse the JSON request body: %{error_class}: %{error_message}"
      end
      message = I18n.t(t_key,
        :default => default,
        :error_class => $!.class,
        :error_message => $!.message,
        :supported_media_types => supported_media_types.join(', ')
      )
      errors = {'json' => [message]}
      halt_error(400, errors)
    end
  else
    if supported_media_types.include?(request_media_type)
      # :nocov:
      raise NotImplementedError, "handling request body with media type #{request_media_type} not implemented"
      # :nocov:
    end
    logger.error "received Content-Type of #{request.content_type.inspect}; halting with 415"
    message = I18n.t('app.errors.request.content_type',
      :default => "Unsupported Content-Type of %{content_type} given for the request body. Supported media types are %{supported_media_types}",
      :content_type => request.content_type,
      :supported_media_types => supported_media_types.join(', ')
    )
    errors = {'Content-Type' => [message]}
    halt_error(415, errors)
  end
end

#request_bodyObject

reads the request body



130
131
132
133
134
135
136
137
# File 'lib/api_hammer/sinatra.rb', line 130

def request_body
  # rewind in case anything in the past has left this un-rewound 
  request.body.rewind
  request.body.read.tap do
    # rewind in case anything in the future expects this to have been left rewound 
    request.body.rewind
  end
end

#response_media_type(options = {}) ⇒ Object

finds the best match (highest q) for those supported_media_types indicated as acceptable by the Accept header.

If the Accept header is not present, assumes that any supported media type is acceptable, and returns the first one.

if the :halt_if_unacceptable option is true and no supported media type is acceptable, this halts with 406.

if the :halt_if_unacceptable option is false (or omitted) and no supported media type is acceptable, this returns the first supported media type.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/api_hammer/sinatra.rb', line 65

def response_media_type(options={})
  options = {:halt_if_unacceptable => false}.merge(options)
  env = options[:env] || (respond_to?(:env) ? self.env : raise(ArgumentError, "must pass env"))
  supported_media_types = options[:supported_media_types] || self.supported_media_types
  accept = env['HTTP_ACCEPT']
  if accept =~ /\S/
    begin
      best_media_type = Rack::Accept::Request.new(env).best_media_type(supported_media_types)
    rescue RuntimeError => e
      # TODO: this is a crappy way to recognize this exception 
      raise unless e.message =~ /Invalid header value/
    end
    if best_media_type
      best_media_type
    else
      if options[:halt_if_unacceptable]
        logger.error "received Accept header of #{accept.inspect}; halting with 406"
        message = I18n.t('app.errors.request.accept',
          :default => "The request indicated that no supported media type is acceptable. Supported media types are: %{supported_media_types}. The request specified Accept: %{accept}",
          :accept => accept,
          :supported_media_types => supported_media_types.join(', ')
        )
        halt_error(406, {'Accept' => [message]})
      else
        supported_media_types.first
      end
    end
  elsif supported_media_types && supported_media_types.any?
    supported_media_types.first
  else
    raise "No media types are defined. Please set supported_media_types."
  end
end

#route_missingObject

override Sinatra::Base#route_missing



39
40
41
42
43
44
45
# File 'lib/api_hammer/sinatra.rb', line 39

def route_missing
  message = I18n.t('app.errors.request.route_404',
    :default => "Not a known route: %{method} %{path}",
    :method => env['REQUEST_METHOD'], :path => env['PATH_INFO']
  )
  halt_error(404, {'route' => [message]})
end