Module: ApiHammer::Sinatra

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

Defined Under Namespace

Modules: Halt

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

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 ⇒ Object



95
96
97
# File 'lib/api_hammer/sinatra.rb', line 95

def check_accept
  response_media_type(: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.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/api_hammer/sinatra.rb', line 182

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



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/api_hammer/sinatra.rb', line 103

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)
    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_body ⇒ Object

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.



135
136
137
138
139
140
141
142
143
144
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
# File 'lib/api_hammer/sinatra.rb', line 135

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_body ⇒ Object

reads the request body



120
121
122
123
124
125
126
127
# File 'lib/api_hammer/sinatra.rb', line 120

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.



62
63
64
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
# File 'lib/api_hammer/sinatra.rb', line 62

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"))
  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_missing ⇒ Object

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

#supported_media_types ⇒ Object



49
50
51
# File 'lib/api_hammer/sinatra.rb', line 49

def supported_media_types
  self.class.supported_media_types
end