api_hammer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.simplecov +1 -0
- data/LICENSE.txt +22 -0
- data/README.md +3 -0
- data/Rakefile.rb +11 -0
- data/lib/api_hammer/check_required_params.rb +58 -0
- data/lib/api_hammer/halt.rb +403 -0
- data/lib/api_hammer/rails.rb +10 -0
- data/lib/api_hammer/request_logger.rb +81 -0
- data/lib/api_hammer/show_text_exceptions.rb +33 -0
- data/lib/api_hammer/tasks/gem_available_updates.rb +19 -0
- data/lib/api_hammer/trailing_newline.rb +38 -0
- data/lib/api_hammer/version.rb +3 -0
- data/lib/api_hammer/weblink.rb +110 -0
- data/lib/api_hammer.rb +9 -0
- data/test/check_required_params_test.rb +67 -0
- data/test/halt_test.rb +44 -0
- data/test/helper.rb +12 -0
- data/test/request_logger_test.rb +23 -0
- data/test/show_text_exceptions_test.rb +29 -0
- data/test/trailing_newline_test.rb +24 -0
- data/test/weblink_test.rb +70 -0
- metadata +201 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 00317bcabfb56b81ee77f410ce7526af594a229f
|
4
|
+
data.tar.gz: 1defdb0648ee78e474e14490c233995156868254
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 31451099bc9e3ec9ed17f73d16089598c9d1c1bf63f882c2baa80b375ae8714f4da936b60a489a13b08ae8487840bdbe904e6cb4ff867daba92e66766161bfbe
|
7
|
+
data.tar.gz: cf0e2d1a2a81d49465d15d9cce424ec2566d49a5710e3672a957e1e22a7979b964a000604eacac68d0e0c81a62895a0fcdd94d8c3acd1ad1ba52941c6cab017a
|
data/.simplecov
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
SimpleCov.start
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Ethan
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module ApiHammer::Rails
|
2
|
+
# halts with a 422 Unprocessable Entity and an appropriate error body if required params are missing
|
3
|
+
#
|
4
|
+
# simple:
|
5
|
+
#
|
6
|
+
# check_required_params(:id, :name)
|
7
|
+
#
|
8
|
+
# - `params[:id]` must be present
|
9
|
+
# - `params[:name]` must be present
|
10
|
+
#
|
11
|
+
# less simple:
|
12
|
+
#
|
13
|
+
# check_required_params(:id, :person => [:name, :height], :lucky_numbers => Array)
|
14
|
+
#
|
15
|
+
# - `params[:id]` must be present
|
16
|
+
# - `params[:person]` must be present and be a hash
|
17
|
+
# - `params[:person][:name]` must be present
|
18
|
+
# - `params[:person][:height]` must be present
|
19
|
+
# - `params[:lucky_numbers]` must be present and be an array
|
20
|
+
def check_required_params(*checks)
|
21
|
+
errors = Hash.new { |h,k| h[k] = [] }
|
22
|
+
check_required_params_helper(checks, params, errors, [])
|
23
|
+
halt_unprocessable_entity(errors) if errors.any?
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
# helper
|
28
|
+
def check_required_params_helper(check, subparams, errors, parents)
|
29
|
+
key = parents.join('#')
|
30
|
+
add_error = proc { |message| errors[key] << message unless errors[key].include?(message) }
|
31
|
+
if subparams.nil?
|
32
|
+
add_error.call("is required but was not provided")
|
33
|
+
elsif check
|
34
|
+
case check
|
35
|
+
when Array
|
36
|
+
check.each { |subcheck| check_required_params_helper(subcheck, subparams, errors, parents) }
|
37
|
+
when Hash
|
38
|
+
if subparams.is_a?(Hash)
|
39
|
+
check.each do |key, subcheck|
|
40
|
+
check_required_params_helper(subcheck, subparams[key], errors, parents + [key])
|
41
|
+
end
|
42
|
+
else
|
43
|
+
add_error.call("must be a Hash")
|
44
|
+
end
|
45
|
+
when Class
|
46
|
+
unless subparams.is_a?(check)
|
47
|
+
add_error.call("must be a #{check.name}")
|
48
|
+
end
|
49
|
+
else
|
50
|
+
if subparams.is_a?(Hash)
|
51
|
+
check_required_params_helper(nil, subparams[check], errors, parents + [check])
|
52
|
+
else
|
53
|
+
add_error.call("must be a Hash")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,403 @@
|
|
1
|
+
# the contents of this file are to let you halt a controller in its processing without having to have a
|
2
|
+
# return in the actual action. this lets helper methods which do things like parameter validation halt.
|
3
|
+
#
|
4
|
+
# it is desgined to function similarly to Sinatra's handling of throw(:halt), but is based around exceptions
|
5
|
+
# because rails doesn't catch anything, just rescues.
|
6
|
+
|
7
|
+
module ApiHammer
|
8
|
+
# an exception raised to stop processing an action and render the body given as the 'body' argument
|
9
|
+
# (which is expected to be a JSON-able object)
|
10
|
+
class Halt < StandardError
|
11
|
+
def initialize(message, body, render_options={})
|
12
|
+
super(message)
|
13
|
+
@body = body
|
14
|
+
@render_options = render_options
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :body, :render_options
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ApiHammer::Rails
|
22
|
+
unless instance_variables.any? { |iv| iv.to_s == '@halt_included' }
|
23
|
+
@halt_included = proc do |controller_class|
|
24
|
+
controller_class.send(:rescue_from, ApiHammer::Halt, :with => :handle_halt)
|
25
|
+
end
|
26
|
+
(@on_included ||= Set.new) << @halt_included
|
27
|
+
end
|
28
|
+
|
29
|
+
# handle a raised ApiHammer::Halt or subclass and render it
|
30
|
+
def handle_halt(halt)
|
31
|
+
render_options = halt.render_options ? halt.render_options.dup : {}
|
32
|
+
# rocket pants does not have a render method, just render_json
|
33
|
+
if respond_to?(:render_json, true)
|
34
|
+
render_json(halt.body || {}, render_options)
|
35
|
+
else
|
36
|
+
render_options[:json] = halt.body || {}
|
37
|
+
render(render_options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# halt and render the given body
|
42
|
+
def halt(status, body, render_options = {})
|
43
|
+
raise(ApiHammer::Halt.new(body.inspect, body, render_options.merge(:status => status)))
|
44
|
+
end
|
45
|
+
|
46
|
+
# halt and render the given errors in the body on the 'errors' key
|
47
|
+
def halt_error(status, errors, render_options = {})
|
48
|
+
halt(status, {'errors' => errors}, render_options)
|
49
|
+
end
|
50
|
+
|
51
|
+
module HaltMethods
|
52
|
+
=begin
|
53
|
+
# these methods are generated with the following script
|
54
|
+
|
55
|
+
require 'nokogiri'
|
56
|
+
require 'faraday'
|
57
|
+
wpcodes = Nokogiri::HTML(Faraday.get('https://en.wikipedia.org/wiki/List_of_HTTP_status_codes').body)
|
58
|
+
dts = wpcodes.css('dt')
|
59
|
+
puts(dts.map do |dt|
|
60
|
+
if dt.text =~ /\A\s*(\d+)\s+([a-z0-9 \-']+)/i
|
61
|
+
status = $1.to_i
|
62
|
+
name = $2.strip
|
63
|
+
underscore = name.split(/[\s-]/).map{|word| word.downcase.gsub(/\W/, '') }.join('_')
|
64
|
+
if ([100..199, 490..499, 520..599].map(&:to_a).inject([], &:+) + [306, 420, 440, 449, 450]).include?(status)
|
65
|
+
# exclude these. 1xx isn't really a thing that makes sense at this level and the others are
|
66
|
+
# nonstandard or particular particular web servers or such
|
67
|
+
nil
|
68
|
+
elsif [204, 205, 304].include?(status)
|
69
|
+
# ones with no body
|
70
|
+
%Q(
|
71
|
+
# halt with status #{status} #{name}
|
72
|
+
def halt_#{underscore}(render_options = {})
|
73
|
+
halt(#{status}, '', render_options)
|
74
|
+
end
|
75
|
+
)
|
76
|
+
elsif (400..599).include?(status)
|
77
|
+
# body goes on an errors object
|
78
|
+
%Q(
|
79
|
+
# halt with status #{status} #{name}, responding with the given errors object on the 'errors' key
|
80
|
+
def halt_#{underscore}(errors, render_options = {})
|
81
|
+
halt_error(#{status}, errors, render_options)
|
82
|
+
end
|
83
|
+
)
|
84
|
+
else
|
85
|
+
%Q(
|
86
|
+
# halt with status #{status} #{name}, responding with the given body object
|
87
|
+
def halt_#{underscore}(body, render_options = {})
|
88
|
+
halt(#{status}, body, render_options)
|
89
|
+
end
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end.compact.join)
|
94
|
+
|
95
|
+
=end
|
96
|
+
# halt with status 200 OK, responding with the given body object
|
97
|
+
def halt_ok(body, render_options = {})
|
98
|
+
halt(200, body, render_options)
|
99
|
+
end
|
100
|
+
|
101
|
+
# halt with status 201 Created, responding with the given body object
|
102
|
+
def halt_created(body, render_options = {})
|
103
|
+
halt(201, body, render_options)
|
104
|
+
end
|
105
|
+
|
106
|
+
# halt with status 202 Accepted, responding with the given body object
|
107
|
+
def halt_accepted(body, render_options = {})
|
108
|
+
halt(202, body, render_options)
|
109
|
+
end
|
110
|
+
|
111
|
+
# halt with status 203 Non-Authoritative Information, responding with the given body object
|
112
|
+
def halt_non_authoritative_information(body, render_options = {})
|
113
|
+
halt(203, body, render_options)
|
114
|
+
end
|
115
|
+
|
116
|
+
# halt with status 204 No Content
|
117
|
+
def halt_no_content(render_options = {})
|
118
|
+
halt(204, '', render_options)
|
119
|
+
end
|
120
|
+
|
121
|
+
# halt with status 205 Reset Content
|
122
|
+
def halt_reset_content(render_options = {})
|
123
|
+
halt(205, '', render_options)
|
124
|
+
end
|
125
|
+
|
126
|
+
# halt with status 206 Partial Content, responding with the given body object
|
127
|
+
def halt_partial_content(body, render_options = {})
|
128
|
+
halt(206, body, render_options)
|
129
|
+
end
|
130
|
+
|
131
|
+
# halt with status 207 Multi-Status, responding with the given body object
|
132
|
+
def halt_multi_status(body, render_options = {})
|
133
|
+
halt(207, body, render_options)
|
134
|
+
end
|
135
|
+
|
136
|
+
# halt with status 208 Already Reported, responding with the given body object
|
137
|
+
def halt_already_reported(body, render_options = {})
|
138
|
+
halt(208, body, render_options)
|
139
|
+
end
|
140
|
+
|
141
|
+
# halt with status 226 IM Used, responding with the given body object
|
142
|
+
def halt_im_used(body, render_options = {})
|
143
|
+
halt(226, body, render_options)
|
144
|
+
end
|
145
|
+
|
146
|
+
# halt with status 300 Multiple Choices, responding with the given body object
|
147
|
+
def halt_multiple_choices(body, render_options = {})
|
148
|
+
halt(300, body, render_options)
|
149
|
+
end
|
150
|
+
|
151
|
+
# halt with status 301 Moved Permanently, responding with the given body object
|
152
|
+
def halt_moved_permanently(body, render_options = {})
|
153
|
+
halt(301, body, render_options)
|
154
|
+
end
|
155
|
+
|
156
|
+
# halt with status 302 Found, responding with the given body object
|
157
|
+
def halt_found(body, render_options = {})
|
158
|
+
halt(302, body, render_options)
|
159
|
+
end
|
160
|
+
|
161
|
+
# halt with status 303 See Other, responding with the given body object
|
162
|
+
def halt_see_other(body, render_options = {})
|
163
|
+
halt(303, body, render_options)
|
164
|
+
end
|
165
|
+
|
166
|
+
# halt with status 304 Not Modified
|
167
|
+
def halt_not_modified(render_options = {})
|
168
|
+
halt(304, '', render_options)
|
169
|
+
end
|
170
|
+
|
171
|
+
# halt with status 305 Use Proxy, responding with the given body object
|
172
|
+
def halt_use_proxy(body, render_options = {})
|
173
|
+
halt(305, body, render_options)
|
174
|
+
end
|
175
|
+
|
176
|
+
# halt with status 307 Temporary Redirect, responding with the given body object
|
177
|
+
def halt_temporary_redirect(body, render_options = {})
|
178
|
+
halt(307, body, render_options)
|
179
|
+
end
|
180
|
+
|
181
|
+
# halt with status 308 Permanent Redirect, responding with the given body object
|
182
|
+
def halt_permanent_redirect(body, render_options = {})
|
183
|
+
halt(308, body, render_options)
|
184
|
+
end
|
185
|
+
|
186
|
+
# halt with status 400 Bad Request, responding with the given errors object on the 'errors' key
|
187
|
+
def halt_bad_request(errors, render_options = {})
|
188
|
+
halt_error(400, errors, render_options)
|
189
|
+
end
|
190
|
+
|
191
|
+
# halt with status 401 Unauthorized, responding with the given errors object on the 'errors' key
|
192
|
+
def halt_unauthorized(errors, render_options = {})
|
193
|
+
halt_error(401, errors, render_options)
|
194
|
+
end
|
195
|
+
|
196
|
+
# halt with status 402 Payment Required, responding with the given errors object on the 'errors' key
|
197
|
+
def halt_payment_required(errors, render_options = {})
|
198
|
+
halt_error(402, errors, render_options)
|
199
|
+
end
|
200
|
+
|
201
|
+
# halt with status 403 Forbidden, responding with the given errors object on the 'errors' key
|
202
|
+
def halt_forbidden(errors, render_options = {})
|
203
|
+
halt_error(403, errors, render_options)
|
204
|
+
end
|
205
|
+
|
206
|
+
# halt with status 404 Not Found, responding with the given errors object on the 'errors' key
|
207
|
+
def halt_not_found(errors, render_options = {})
|
208
|
+
halt_error(404, errors, render_options)
|
209
|
+
end
|
210
|
+
|
211
|
+
# halt with status 405 Method Not Allowed, responding with the given errors object on the 'errors' key
|
212
|
+
def halt_method_not_allowed(errors, render_options = {})
|
213
|
+
halt_error(405, errors, render_options)
|
214
|
+
end
|
215
|
+
|
216
|
+
# halt with status 406 Not Acceptable, responding with the given errors object on the 'errors' key
|
217
|
+
def halt_not_acceptable(errors, render_options = {})
|
218
|
+
halt_error(406, errors, render_options)
|
219
|
+
end
|
220
|
+
|
221
|
+
# halt with status 407 Proxy Authentication Required, responding with the given errors object on the 'errors' key
|
222
|
+
def halt_proxy_authentication_required(errors, render_options = {})
|
223
|
+
halt_error(407, errors, render_options)
|
224
|
+
end
|
225
|
+
|
226
|
+
# halt with status 408 Request Timeout, responding with the given errors object on the 'errors' key
|
227
|
+
def halt_request_timeout(errors, render_options = {})
|
228
|
+
halt_error(408, errors, render_options)
|
229
|
+
end
|
230
|
+
|
231
|
+
# halt with status 409 Conflict, responding with the given errors object on the 'errors' key
|
232
|
+
def halt_conflict(errors, render_options = {})
|
233
|
+
halt_error(409, errors, render_options)
|
234
|
+
end
|
235
|
+
|
236
|
+
# halt with status 410 Gone, responding with the given errors object on the 'errors' key
|
237
|
+
def halt_gone(errors, render_options = {})
|
238
|
+
halt_error(410, errors, render_options)
|
239
|
+
end
|
240
|
+
|
241
|
+
# halt with status 411 Length Required, responding with the given errors object on the 'errors' key
|
242
|
+
def halt_length_required(errors, render_options = {})
|
243
|
+
halt_error(411, errors, render_options)
|
244
|
+
end
|
245
|
+
|
246
|
+
# halt with status 412 Precondition Failed, responding with the given errors object on the 'errors' key
|
247
|
+
def halt_precondition_failed(errors, render_options = {})
|
248
|
+
halt_error(412, errors, render_options)
|
249
|
+
end
|
250
|
+
|
251
|
+
# halt with status 413 Request Entity Too Large, responding with the given errors object on the 'errors' key
|
252
|
+
def halt_request_entity_too_large(errors, render_options = {})
|
253
|
+
halt_error(413, errors, render_options)
|
254
|
+
end
|
255
|
+
|
256
|
+
# halt with status 414 Request-URI Too Long, responding with the given errors object on the 'errors' key
|
257
|
+
def halt_request_uri_too_long(errors, render_options = {})
|
258
|
+
halt_error(414, errors, render_options)
|
259
|
+
end
|
260
|
+
|
261
|
+
# halt with status 415 Unsupported Media Type, responding with the given errors object on the 'errors' key
|
262
|
+
def halt_unsupported_media_type(errors, render_options = {})
|
263
|
+
halt_error(415, errors, render_options)
|
264
|
+
end
|
265
|
+
|
266
|
+
# halt with status 416 Requested Range Not Satisfiable, responding with the given errors object on the 'errors' key
|
267
|
+
def halt_requested_range_not_satisfiable(errors, render_options = {})
|
268
|
+
halt_error(416, errors, render_options)
|
269
|
+
end
|
270
|
+
|
271
|
+
# halt with status 417 Expectation Failed, responding with the given errors object on the 'errors' key
|
272
|
+
def halt_expectation_failed(errors, render_options = {})
|
273
|
+
halt_error(417, errors, render_options)
|
274
|
+
end
|
275
|
+
|
276
|
+
# halt with status 418 I'm a teapot, responding with the given errors object on the 'errors' key
|
277
|
+
def halt_im_a_teapot(errors, render_options = {})
|
278
|
+
halt_error(418, errors, render_options)
|
279
|
+
end
|
280
|
+
|
281
|
+
# halt with status 419 Authentication Timeout, responding with the given errors object on the 'errors' key
|
282
|
+
def halt_authentication_timeout(errors, render_options = {})
|
283
|
+
halt_error(419, errors, render_options)
|
284
|
+
end
|
285
|
+
|
286
|
+
# halt with status 422 Unprocessable Entity, responding with the given errors object on the 'errors' key
|
287
|
+
def halt_unprocessable_entity(errors, render_options = {})
|
288
|
+
halt_error(422, errors, render_options)
|
289
|
+
end
|
290
|
+
|
291
|
+
# halt with status 423 Locked, responding with the given errors object on the 'errors' key
|
292
|
+
def halt_locked(errors, render_options = {})
|
293
|
+
halt_error(423, errors, render_options)
|
294
|
+
end
|
295
|
+
|
296
|
+
# halt with status 424 Failed Dependency, responding with the given errors object on the 'errors' key
|
297
|
+
def halt_failed_dependency(errors, render_options = {})
|
298
|
+
halt_error(424, errors, render_options)
|
299
|
+
end
|
300
|
+
|
301
|
+
# halt with status 425 Unordered Collection, responding with the given errors object on the 'errors' key
|
302
|
+
def halt_unordered_collection(errors, render_options = {})
|
303
|
+
halt_error(425, errors, render_options)
|
304
|
+
end
|
305
|
+
|
306
|
+
# halt with status 426 Upgrade Required, responding with the given errors object on the 'errors' key
|
307
|
+
def halt_upgrade_required(errors, render_options = {})
|
308
|
+
halt_error(426, errors, render_options)
|
309
|
+
end
|
310
|
+
|
311
|
+
# halt with status 428 Precondition Required, responding with the given errors object on the 'errors' key
|
312
|
+
def halt_precondition_required(errors, render_options = {})
|
313
|
+
halt_error(428, errors, render_options)
|
314
|
+
end
|
315
|
+
|
316
|
+
# halt with status 429 Too Many Requests, responding with the given errors object on the 'errors' key
|
317
|
+
def halt_too_many_requests(errors, render_options = {})
|
318
|
+
halt_error(429, errors, render_options)
|
319
|
+
end
|
320
|
+
|
321
|
+
# halt with status 431 Request Header Fields Too Large, responding with the given errors object on the 'errors' key
|
322
|
+
def halt_request_header_fields_too_large(errors, render_options = {})
|
323
|
+
halt_error(431, errors, render_options)
|
324
|
+
end
|
325
|
+
|
326
|
+
# halt with status 444 No Response, responding with the given errors object on the 'errors' key
|
327
|
+
def halt_no_response(errors, render_options = {})
|
328
|
+
halt_error(444, errors, render_options)
|
329
|
+
end
|
330
|
+
|
331
|
+
# halt with status 451 Unavailable For Legal Reasons, responding with the given errors object on the 'errors' key
|
332
|
+
def halt_unavailable_for_legal_reasons(errors, render_options = {})
|
333
|
+
halt_error(451, errors, render_options)
|
334
|
+
end
|
335
|
+
|
336
|
+
# halt with status 451 Redirect, responding with the given errors object on the 'errors' key
|
337
|
+
def halt_redirect(errors, render_options = {})
|
338
|
+
halt_error(451, errors, render_options)
|
339
|
+
end
|
340
|
+
|
341
|
+
# halt with status 500 Internal Server Error, responding with the given errors object on the 'errors' key
|
342
|
+
def halt_internal_server_error(errors, render_options = {})
|
343
|
+
halt_error(500, errors, render_options)
|
344
|
+
end
|
345
|
+
|
346
|
+
# halt with status 501 Not Implemented, responding with the given errors object on the 'errors' key
|
347
|
+
def halt_not_implemented(errors, render_options = {})
|
348
|
+
halt_error(501, errors, render_options)
|
349
|
+
end
|
350
|
+
|
351
|
+
# halt with status 502 Bad Gateway, responding with the given errors object on the 'errors' key
|
352
|
+
def halt_bad_gateway(errors, render_options = {})
|
353
|
+
halt_error(502, errors, render_options)
|
354
|
+
end
|
355
|
+
|
356
|
+
# halt with status 503 Service Unavailable, responding with the given errors object on the 'errors' key
|
357
|
+
def halt_service_unavailable(errors, render_options = {})
|
358
|
+
halt_error(503, errors, render_options)
|
359
|
+
end
|
360
|
+
|
361
|
+
# halt with status 504 Gateway Timeout, responding with the given errors object on the 'errors' key
|
362
|
+
def halt_gateway_timeout(errors, render_options = {})
|
363
|
+
halt_error(504, errors, render_options)
|
364
|
+
end
|
365
|
+
|
366
|
+
# halt with status 505 HTTP Version Not Supported, responding with the given errors object on the 'errors' key
|
367
|
+
def halt_http_version_not_supported(errors, render_options = {})
|
368
|
+
halt_error(505, errors, render_options)
|
369
|
+
end
|
370
|
+
|
371
|
+
# halt with status 506 Variant Also Negotiates, responding with the given errors object on the 'errors' key
|
372
|
+
def halt_variant_also_negotiates(errors, render_options = {})
|
373
|
+
halt_error(506, errors, render_options)
|
374
|
+
end
|
375
|
+
|
376
|
+
# halt with status 507 Insufficient Storage, responding with the given errors object on the 'errors' key
|
377
|
+
def halt_insufficient_storage(errors, render_options = {})
|
378
|
+
halt_error(507, errors, render_options)
|
379
|
+
end
|
380
|
+
|
381
|
+
# halt with status 508 Loop Detected, responding with the given errors object on the 'errors' key
|
382
|
+
def halt_loop_detected(errors, render_options = {})
|
383
|
+
halt_error(508, errors, render_options)
|
384
|
+
end
|
385
|
+
|
386
|
+
# halt with status 509 Bandwidth Limit Exceeded, responding with the given errors object on the 'errors' key
|
387
|
+
def halt_bandwidth_limit_exceeded(errors, render_options = {})
|
388
|
+
halt_error(509, errors, render_options)
|
389
|
+
end
|
390
|
+
|
391
|
+
# halt with status 510 Not Extended, responding with the given errors object on the 'errors' key
|
392
|
+
def halt_not_extended(errors, render_options = {})
|
393
|
+
halt_error(510, errors, render_options)
|
394
|
+
end
|
395
|
+
|
396
|
+
# halt with status 511 Network Authentication Required, responding with the given errors object on the 'errors' key
|
397
|
+
def halt_network_authentication_required(errors, render_options = {})
|
398
|
+
halt_error(511, errors, render_options)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
include HaltMethods
|
403
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'term/ansicolor'
|
3
|
+
require 'json'
|
4
|
+
require 'addressable/uri'
|
5
|
+
|
6
|
+
module ApiHammer
|
7
|
+
# Rack middleware for logging. much like Rack::CommonLogger but with a log message that isn't an unreadable
|
8
|
+
# mess of dashes and unlabeled numbers.
|
9
|
+
#
|
10
|
+
# two lines:
|
11
|
+
#
|
12
|
+
# - an info line, colored prettily to show a brief summary of the request and response
|
13
|
+
# - a debug line of json to record all relevant info. this is a lot of stuff jammed into one line, not
|
14
|
+
# pretty, but informative.
|
15
|
+
class RequestLogger < Rack::CommonLogger
|
16
|
+
include Term::ANSIColor
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
began_at = Time.now
|
20
|
+
|
21
|
+
# this is closed after the app is called, so read it before
|
22
|
+
env["rack.input"].rewind
|
23
|
+
@request_body = env["rack.input"].read
|
24
|
+
env["rack.input"].rewind
|
25
|
+
|
26
|
+
status, header, body = @app.call(env)
|
27
|
+
header = ::Rack::Utils::HeaderHash.new(header)
|
28
|
+
body_proxy = ::Rack::BodyProxy.new(body) { log(env, status, header, began_at, body) }
|
29
|
+
[status, header, body_proxy]
|
30
|
+
end
|
31
|
+
|
32
|
+
def log(env, status, headers, began_at, body)
|
33
|
+
now = Time.now
|
34
|
+
|
35
|
+
request = Rack::Request.new(env)
|
36
|
+
response = Rack::Response.new('', status, headers)
|
37
|
+
|
38
|
+
request_uri = Addressable::URI.new(
|
39
|
+
:scheme => request.scheme,
|
40
|
+
:host => request.host,
|
41
|
+
:port => request.port,
|
42
|
+
:path => request.path,
|
43
|
+
:query => (request.query_string unless request.query_string.empty?)
|
44
|
+
)
|
45
|
+
status_color = case status.to_i
|
46
|
+
when 200..299
|
47
|
+
:intense_green
|
48
|
+
when 400..499
|
49
|
+
:intense_yellow
|
50
|
+
when 500..599
|
51
|
+
:intense_red
|
52
|
+
else
|
53
|
+
:white
|
54
|
+
end
|
55
|
+
status_s = bold(send(status_color, status.to_s))
|
56
|
+
@logger.info "#{status_s} : #{bold(intense_cyan(request.request_method))} #{intense_cyan(request_uri.normalize)}"
|
57
|
+
data = {
|
58
|
+
'request' => {
|
59
|
+
'method' => request.request_method,
|
60
|
+
'uri' => request_uri.normalize.to_s,
|
61
|
+
'length' => request.content_length,
|
62
|
+
'Content-Type' => request.content_type,
|
63
|
+
'remote_addr' => env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"],
|
64
|
+
'User-Agent' => request.user_agent,
|
65
|
+
'body' => @request_body,
|
66
|
+
}.reject{|k,v| v.nil? },
|
67
|
+
'response' => {
|
68
|
+
'status' => status,
|
69
|
+
'length' => headers['Content-Length'] || body.to_enum.map(&::Rack::Utils.method(:bytesize)).inject(0, &:+),
|
70
|
+
'Location' => response.location,
|
71
|
+
'Content-Type' => response.content_type,
|
72
|
+
}.reject{|k,v| v.nil? },
|
73
|
+
'began_at' => began_at.utc.to_i,
|
74
|
+
'duration' => now - began_at,
|
75
|
+
}
|
76
|
+
json_data = JSON.dump(data)
|
77
|
+
@logger.debug json_data
|
78
|
+
$ZMQ_LOGGER.log json_data if defined?($ZMQ_LOGGER)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ApiHammer
|
2
|
+
# Rack middleware to rescue any exceptions and return an appropriate message for the current
|
3
|
+
# environment, with a nice concise bit of text for errors.
|
4
|
+
#
|
5
|
+
# ideally this should be placed as close to the application itself as possible (last middleware
|
6
|
+
# used) so that the exception will not bubble past other middleware, skipping it.
|
7
|
+
#
|
8
|
+
# like Sinatra::ShowExceptions or Rack::ShowExceptions, but not a huge blob of html. (note:
|
9
|
+
# those middlewares have a #prefers_plain_text? method which makes them behave like this, but
|
10
|
+
# it's simpler and more reliable to roll our own than monkey-patch those)
|
11
|
+
class ShowTextExceptions
|
12
|
+
def initialize(app, options)
|
13
|
+
@app=app
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
def call(env)
|
17
|
+
begin
|
18
|
+
@app.call(env)
|
19
|
+
rescue Exception => e
|
20
|
+
full_error_message = (["#{e.class}: #{e.message}"] + e.backtrace.map{|l| " #{l}" }).join("\n")
|
21
|
+
if @options[:logger]
|
22
|
+
@options[:logger].error(full_error_message)
|
23
|
+
end
|
24
|
+
if @options[:full_error]
|
25
|
+
body = full_error_message
|
26
|
+
else
|
27
|
+
body = "Internal Server Error\n"
|
28
|
+
end
|
29
|
+
[500, {'Content-Type' => 'text/plain'}, [body]]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
namespace :gem do
|
2
|
+
desc 'informs you of available gem updates, newer than the gem versions in your Gemfile or Gemfile.lock'
|
3
|
+
task :available_updates do
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.definition.dependencies.each do |dependency|
|
6
|
+
lock_version = Bundler.definition.specs.detect { |spec| spec.name == dependency.name }.version
|
7
|
+
remote_specs = Gem::SpecFetcher.fetcher.detect(:latest) { |name_tuple| name_tuple.name == dependency.name }
|
8
|
+
remote_specs.reject! do |(remote_spec, source)|
|
9
|
+
remote_spec.version <= lock_version
|
10
|
+
end
|
11
|
+
if remote_specs.any?
|
12
|
+
puts "LOCAL #{dependency.name} #{dependency.requirement} (locked at #{lock_version})"
|
13
|
+
remote_specs.each do |(remote_spec, source)|
|
14
|
+
puts "\tREMOTE AVAILABLE: #{remote_spec.name} #{remote_spec.version} #{remote_spec.platform}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module ApiHammer
|
4
|
+
# Rack middleware which adds a trailing newline to any responses which do not include one.
|
5
|
+
#
|
6
|
+
# one effect of this is to make curl more readable, as without this, the prompt that follows the
|
7
|
+
# request will be on the same line.
|
8
|
+
#
|
9
|
+
# does not add a newline to blank responses.
|
10
|
+
class TrailingNewline
|
11
|
+
def initialize(app)
|
12
|
+
@app=app
|
13
|
+
end
|
14
|
+
class TNLBodyProxy < Rack::BodyProxy
|
15
|
+
def each
|
16
|
+
last_has_newline = false
|
17
|
+
blank = true
|
18
|
+
@body.each do |e|
|
19
|
+
last_has_newline = e =~ /\n\z/m
|
20
|
+
blank = false if e != ''
|
21
|
+
yield e
|
22
|
+
end
|
23
|
+
yield "\n" unless blank || last_has_newline
|
24
|
+
end
|
25
|
+
include Enumerable
|
26
|
+
end
|
27
|
+
def call(env)
|
28
|
+
status, headers, body = *@app.call(env)
|
29
|
+
if env['REQUEST_METHOD'].downcase != 'head'
|
30
|
+
body = TNLBodyProxy.new(body){}
|
31
|
+
if headers["Content-Length"]
|
32
|
+
headers["Content-Length"] = body.map(&Rack::Utils.method(:bytesize)).inject(0, &:+).to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
[status, headers, body]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
|
3
|
+
module ApiHammer
|
4
|
+
# a RFC5988 Web Link
|
5
|
+
#
|
6
|
+
# https://tools.ietf.org/html/rfc5988
|
7
|
+
class Weblink
|
8
|
+
# Weblink::Error, base class for all errors of the Weblink class
|
9
|
+
class Error < StandardError; end
|
10
|
+
# error parsing a Weblink
|
11
|
+
class ParseError < Error; end
|
12
|
+
# error when attempting an operation that requires a context URI which was not provided
|
13
|
+
class NoContextError < Error; end
|
14
|
+
|
15
|
+
# parses an array of Web Links from the value an HTTP Link header, as described in
|
16
|
+
# https://tools.ietf.org/html/rfc5988#section-5
|
17
|
+
#
|
18
|
+
# returns an Array of Weblink objects
|
19
|
+
def self.parse_link_value(link_value, context_uri=nil)
|
20
|
+
links = []
|
21
|
+
|
22
|
+
return links unless link_value
|
23
|
+
|
24
|
+
attr_char = /[a-zA-Z0-9!#\$&+\-.^_`|~]/ # defined in https://tools.ietf.org/html/rfc5987#section-3.2.1
|
25
|
+
ptoken = %r([a-zA-Z0-9!#\$%&'()*+\-./:<=>?@\[\]^_`{|}~])
|
26
|
+
quoted_string = /"([^"]*)"/
|
27
|
+
|
28
|
+
require 'strscan'
|
29
|
+
ss = StringScanner.new(link_value)
|
30
|
+
parse_fail = proc do
|
31
|
+
raise ParseError, "Unable to parse link value: #{link_value} " +
|
32
|
+
"around character #{ss.pos}: #{ss.peek(link_value.length - ss.pos)}"
|
33
|
+
end
|
34
|
+
|
35
|
+
while !ss.eos?
|
36
|
+
# get the target_uri, within some angle brackets
|
37
|
+
ss.scan(/\s*<([^>]+)>/) || parse_fail.call
|
38
|
+
target_uri = ss[1]
|
39
|
+
attributes = {}
|
40
|
+
# get the attributes: semicolon, some attr_chars, an optional asterisk, equals, and a quoted
|
41
|
+
# string or series of unquoted ptokens
|
42
|
+
while ss.scan(/\s*;\s*(#{attr_char.source}+\*?)\s*=\s*(?:#{quoted_string.source}|(#{ptoken.source}+))\s*/)
|
43
|
+
attributes[ss[1]] = ss[2] || ss[3]
|
44
|
+
end
|
45
|
+
links << new(target_uri, attributes, context_uri)
|
46
|
+
unless ss.eos?
|
47
|
+
# either the string ends or has a comma followed by another link
|
48
|
+
ss.scan(/\s*,\s*/) || parse_fail.call
|
49
|
+
end
|
50
|
+
end
|
51
|
+
links
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(target_uri, attributes, context_uri=nil)
|
55
|
+
@target_uri = to_addressable_uri(target_uri)
|
56
|
+
@attributes = attributes
|
57
|
+
@context_uri = to_addressable_uri(context_uri)
|
58
|
+
end
|
59
|
+
|
60
|
+
# the context uri of the link, as an Addressable URI. this URI must be absolute, and the target_uri
|
61
|
+
# may be resolved against it. this is most typically the request URI of a request to a service
|
62
|
+
attr_reader :context_uri
|
63
|
+
|
64
|
+
# RFC 5988 calls it IRI, but nobody else does. we'll throw in an alias.
|
65
|
+
alias_method :context_iri, :context_uri
|
66
|
+
|
67
|
+
# returns the target URI as an Addressable::URI
|
68
|
+
attr_reader :target_uri
|
69
|
+
# RFC 5988 calls it IRI, but nobody else does. we'll throw in an alias.
|
70
|
+
alias_method :target_iri, :target_uri
|
71
|
+
|
72
|
+
# attempts to make target_uri absolute, using context_uri if available. raises if
|
73
|
+
# there is not information available to make an absolute target URI
|
74
|
+
def absolute_target_uri
|
75
|
+
if target_uri.absolute?
|
76
|
+
target_uri
|
77
|
+
elsif context_uri
|
78
|
+
context_uri + target_uri
|
79
|
+
else
|
80
|
+
raise NoContextError, "Target URI is relative but no Context URI given - cannot determine absolute target URI"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# link attributes
|
85
|
+
attr_reader :attributes
|
86
|
+
|
87
|
+
# subscript returns an attribute of this Link, if defined, otherwise nil
|
88
|
+
def [](attribute_key)
|
89
|
+
@attributes[attribute_key]
|
90
|
+
end
|
91
|
+
|
92
|
+
# link rel attribute
|
93
|
+
def rel
|
94
|
+
self['rel']
|
95
|
+
end
|
96
|
+
alias_method :relation_type, :rel
|
97
|
+
|
98
|
+
# compares relation types in a case-insensitive manner as mandated in
|
99
|
+
# https://tools.ietf.org/html/rfc5988#section-4.1
|
100
|
+
def rel?(other_rel)
|
101
|
+
rel && other_rel && rel.downcase == other_rel.downcase
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
# if uri is nil, returns nil; otherwise, tries to return a Addressable::URI
|
106
|
+
def to_addressable_uri(uri)
|
107
|
+
uri.nil? || uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/api_hammer.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'api_hammer/version'
|
2
|
+
|
3
|
+
module ApiHammer
|
4
|
+
autoload :Rails, 'api_hammer/rails'
|
5
|
+
autoload :RequestLogger, 'api_hammer/request_logger.rb'
|
6
|
+
autoload :ShowTextExceptions, 'api_hammer/show_text_exceptions.rb'
|
7
|
+
autoload :TrailingNewline, 'api_hammer/trailing_newline.rb'
|
8
|
+
autoload :Weblink, 'api_hammer/weblink.rb'
|
9
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
class FakeController
|
5
|
+
def self.rescue_from(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
include(ApiHammer::Rails)
|
9
|
+
attr_accessor :params
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'ApiHammer::Rails#check_required_params' do
|
13
|
+
def controller_with_params(params)
|
14
|
+
FakeController.new.tap { |c| c.params = params }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'a moderately complex set of checks' do
|
18
|
+
let(:checks) { [:id, :person => [:name, :height], :lucky_numbers => Array] }
|
19
|
+
|
20
|
+
it 'passes with a moderately complex example' do
|
21
|
+
c = controller_with_params(:id => '99', :person => {:name => 'hammer', :height => '3'}, :lucky_numbers => ['2'])
|
22
|
+
c.check_required_params(checks)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'is missing id' do
|
26
|
+
c = controller_with_params(:person => {:name => 'hammer', :height => '3'}, :lucky_numbers => ['2'])
|
27
|
+
err = assert_raises(ApiHammer::Halt) { c.check_required_params(checks) }
|
28
|
+
assert_equal({'errors' => {'id' => ['is required but was not provided']}}, err.body)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'is missing person' do
|
32
|
+
c = controller_with_params(:id => '99', :lucky_numbers => ['2'])
|
33
|
+
err = assert_raises(ApiHammer::Halt) { c.check_required_params(checks) }
|
34
|
+
assert_equal({'errors' => {'person' => ['is required but was not provided']}}, err.body)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'is has the wrong type for person' do
|
38
|
+
c = controller_with_params(:id => '99', :person => ['hammer', '3'], :lucky_numbers => ['2'])
|
39
|
+
err = assert_raises(ApiHammer::Halt) { c.check_required_params(checks) }
|
40
|
+
assert_equal({'errors' => {'person' => ['must be a Hash']}}, err.body)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'is missing person#name' do
|
44
|
+
c = controller_with_params(:id => '99', :person => {:height => '3'}, :lucky_numbers => ['2'])
|
45
|
+
err = assert_raises(ApiHammer::Halt) { c.check_required_params(checks) }
|
46
|
+
assert_equal({'errors' => {'person#name' => ['is required but was not provided']}}, err.body)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'is missing lucky_numbers' do
|
50
|
+
c = controller_with_params(:id => '99', :person => {:name => 'hammer', :height => '3'})
|
51
|
+
err = assert_raises(ApiHammer::Halt) { c.check_required_params(checks) }
|
52
|
+
assert_equal({'errors' => {'lucky_numbers' => ['is required but was not provided']}}, err.body)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'has the wrong type for lucky_numbers' do
|
56
|
+
c = controller_with_params(:id => '99', :person => {:name => 'hammer', :height => '3'}, :lucky_numbers => '2')
|
57
|
+
err = assert_raises(ApiHammer::Halt) { c.check_required_params(checks) }
|
58
|
+
assert_equal({'errors' => {'lucky_numbers' => ['must be a Array']}}, err.body)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'has multiple problems' do
|
62
|
+
c = controller_with_params({})
|
63
|
+
err = assert_raises(ApiHammer::Halt) { c.check_required_params(checks) }
|
64
|
+
assert_equal({'errors' => {'id' => ['is required but was not provided'], 'person' => ['is required but was not provided'], 'lucky_numbers' => ['is required but was not provided']}}, err.body)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/test/halt_test.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
class FakeController
|
5
|
+
def self.rescue_from(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
include(ApiHammer::Rails)
|
9
|
+
|
10
|
+
attr_reader :rendered
|
11
|
+
def render(opts)
|
12
|
+
@rendered = opts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'ApiHammer::Rails#halt' do
|
17
|
+
it 'raises ApiHammer::Halt' do
|
18
|
+
haltex = assert_raises(ApiHammer::Halt) { FakeController.new.halt(200, {}) }
|
19
|
+
assert_equal({}, haltex.body)
|
20
|
+
assert_equal(200, haltex.render_options[:status])
|
21
|
+
end
|
22
|
+
describe 'status-specific halts' do
|
23
|
+
it 'halts ok' do
|
24
|
+
haltex = assert_raises(ApiHammer::Halt) { FakeController.new.halt_ok({}) }
|
25
|
+
assert_equal({}, haltex.body)
|
26
|
+
assert_equal(200, haltex.render_options[:status])
|
27
|
+
end
|
28
|
+
it 'halts unprocessable entity' do
|
29
|
+
haltex = assert_raises(ApiHammer::Halt) { FakeController.new.halt_unprocessable_entity({}) }
|
30
|
+
assert_equal({'errors' => {}}, haltex.body)
|
31
|
+
assert_equal(422, haltex.render_options[:status])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'ApiHammer::Rails#handle_halt' do
|
37
|
+
it 'renders the things from the error' do
|
38
|
+
controller = FakeController.new
|
39
|
+
haltex = (FakeController.new.halt_unprocessable_entity({}) rescue $!)
|
40
|
+
controller.handle_halt(haltex)
|
41
|
+
assert_equal(422, controller.rendered[:status])
|
42
|
+
assert_equal({'errors' => {}}, controller.rendered[:json])
|
43
|
+
end
|
44
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('../lib', File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
require 'simplecov'
|
4
|
+
|
5
|
+
# NO EXPECTATIONS
|
6
|
+
ENV["MT_NO_EXPECTATIONS"] = ''
|
7
|
+
|
8
|
+
require 'minitest/autorun'
|
9
|
+
require 'minitest/reporters'
|
10
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
11
|
+
|
12
|
+
require 'api_hammer'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
|
2
|
+
require 'helper'
|
3
|
+
require 'logger'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
describe ApiHammer::RequestLogger do
|
7
|
+
let(:logio) { StringIO.new }
|
8
|
+
let(:logger) { Logger.new(logio) }
|
9
|
+
|
10
|
+
it 'logs' do
|
11
|
+
app = ApiHammer::RequestLogger.new(proc { |env| [200, {}, []] }, logger)
|
12
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
13
|
+
assert_match(/200/, logio.string)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'colors by status' do
|
17
|
+
{200 => :intense_green, 400 => :intense_yellow, 500 => :intense_red, 300 => :white}.each do |status, color|
|
18
|
+
app = ApiHammer::RequestLogger.new(proc { |env| [status, {}, []] }, logger)
|
19
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
20
|
+
assert(logio.string.include?(Term::ANSIColor.send(color, status.to_s)))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
describe ApiHammer::ShowTextExceptions do
|
5
|
+
it 'lets normal responses through untouched' do
|
6
|
+
orig_response = [200, {}, []]
|
7
|
+
app = ApiHammer::ShowTextExceptions.new(proc { |env| orig_response }, {})
|
8
|
+
app_response = app.call(Rack::MockRequest.env_for('/'))
|
9
|
+
assert_equal(orig_response, app_response)
|
10
|
+
end
|
11
|
+
it '500s' do
|
12
|
+
app = ApiHammer::ShowTextExceptions.new(proc { |env| raise }, :full_error => true)
|
13
|
+
assert_equal(500, app.call(Rack::MockRequest.env_for('/')).first)
|
14
|
+
end
|
15
|
+
it 'includes the full error' do
|
16
|
+
app = ApiHammer::ShowTextExceptions.new(proc { |env| raise 'foo' }, :full_error => true)
|
17
|
+
assert_match(/RuntimeError: foo/, app.call(Rack::MockRequest.env_for('/')).last.to_enum.to_a.join)
|
18
|
+
end
|
19
|
+
it 'does not include the full error' do
|
20
|
+
app = ApiHammer::ShowTextExceptions.new(proc { |env| raise }, :full_error => false)
|
21
|
+
assert_equal("Internal Server Error\n", app.call(Rack::MockRequest.env_for('/')).last.to_enum.to_a.join)
|
22
|
+
end
|
23
|
+
it 'logs' do
|
24
|
+
logio=StringIO.new
|
25
|
+
app = ApiHammer::ShowTextExceptions.new(proc { |env| raise 'foo' }, :logger => Logger.new(logio))
|
26
|
+
app.call(Rack::MockRequest.env_for('/'))
|
27
|
+
assert_match(/RuntimeError: foo/, logio.string)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
describe ApiHammer::TrailingNewline do
|
5
|
+
it 'adds a trailing newline when one is missing' do
|
6
|
+
app = ApiHammer::TrailingNewline.new(proc { |env| [200, {}, ["foo"]] })
|
7
|
+
assert_equal("foo\n", app.call(Rack::MockRequest.env_for('/')).last.to_enum.to_a.join)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'does not add a trailing newline when one is present' do
|
11
|
+
app = ApiHammer::TrailingNewline.new(proc { |env| [200, {}, ["foo\n"]] })
|
12
|
+
assert_equal("foo\n", app.call(Rack::MockRequest.env_for('/')).last.to_enum.to_a.join)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'does not add a trailing newline when the response is blank' do
|
16
|
+
app = ApiHammer::TrailingNewline.new(proc { |env| [200, {}, []] })
|
17
|
+
assert_equal([], app.call(Rack::MockRequest.env_for('/')).last.to_enum.to_a)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'updates Content-Length if present' do
|
21
|
+
app = ApiHammer::TrailingNewline.new(proc { |env| [200, {'Content-Length' => '3'}, ['foo']] })
|
22
|
+
assert_equal('4', app.call(Rack::MockRequest.env_for('/'))[1]['Content-Length'])
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
describe ApiHammer::Weblink do
|
5
|
+
describe '#parse_link_value' do
|
6
|
+
it 'parses link headers' do
|
7
|
+
examples = [
|
8
|
+
# one link with some attributes
|
9
|
+
[ %q(<http://example.com>; rel=foo; title=example; title*="an example"),
|
10
|
+
'http://example.com',
|
11
|
+
{'rel' => 'foo', 'title' => 'example', 'title*' => 'an example'},
|
12
|
+
],
|
13
|
+
# two links
|
14
|
+
[ %q(<http://example.com>; rel=foo; title=example; title*="an example", <http://example2.com>; rel=bar),
|
15
|
+
'http://example.com',
|
16
|
+
{'rel' => 'foo', 'title' => 'example', 'title*' => 'an example'},
|
17
|
+
'http://example2.com',
|
18
|
+
{'rel' => 'bar'},
|
19
|
+
],
|
20
|
+
# spaces
|
21
|
+
[ %q( <http://example.com> ;rel = foo ;title=example; title*="an example" ),
|
22
|
+
'http://example.com',
|
23
|
+
{'rel' => 'foo', 'title' => 'example', 'title*' => 'an example'},
|
24
|
+
],
|
25
|
+
# empty returns no links
|
26
|
+
[''],
|
27
|
+
]
|
28
|
+
examples.each do |example|
|
29
|
+
link_value = example.shift
|
30
|
+
links = ApiHammer::Weblink.parse_link_value(link_value)
|
31
|
+
assert_equal(example.size, links.size * 2)
|
32
|
+
example.each_slice(2).zip(links).each do |((target_uri, attributes), link)|
|
33
|
+
assert_equal(Addressable::URI.parse(target_uri), link.target_uri)
|
34
|
+
assert_equal(attributes, link.attributes)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'gives an absolute uri based on context' do
|
40
|
+
link = ApiHammer::Weblink.parse_link_value('</bar>; rel=foo', 'http://example.com/foo').first
|
41
|
+
assert_equal(Addressable::URI.parse('http://example.com/bar'), link.absolute_target_uri)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'errors without context, trying to generate an absolute uri' do
|
45
|
+
link = ApiHammer::Weblink.parse_link_value('</bar>; rel=foo').first
|
46
|
+
assert_raises(ApiHammer::Weblink::NoContextError) { link.absolute_target_uri }
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns an empty array for nil link header' do
|
50
|
+
assert_equal([], ApiHammer::Weblink.parse_link_value(nil))
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'parse errors' do
|
54
|
+
examples = [
|
55
|
+
# missing >
|
56
|
+
%q(<http://example.com),
|
57
|
+
# missing <
|
58
|
+
%q(http://example.com>; rel=foo),
|
59
|
+
# , instead of ;
|
60
|
+
%q(<http://example.com>, rel=foo),
|
61
|
+
# non-ptoken characters (\,) unquoted
|
62
|
+
%q(<http://example.com>; rel=b\\ar; title=example),
|
63
|
+
%q(<http://example.com>; rel=b,ar; title=example),
|
64
|
+
]
|
65
|
+
examples.each do |example|
|
66
|
+
assert_raises(ApiHammer::Weblink::ParseError) { ApiHammer::Weblink.parse_link_value(example) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_hammer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ethan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: term-ansicolor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: addressable
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: minitest-reporters
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yard
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: actually a set of small API-related tools. very much unlike a hammer
|
140
|
+
at all, which is one large tool.
|
141
|
+
email:
|
142
|
+
- ethan@unth
|
143
|
+
executables: []
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".simplecov"
|
148
|
+
- LICENSE.txt
|
149
|
+
- README.md
|
150
|
+
- Rakefile.rb
|
151
|
+
- lib/api_hammer.rb
|
152
|
+
- lib/api_hammer/check_required_params.rb
|
153
|
+
- lib/api_hammer/halt.rb
|
154
|
+
- lib/api_hammer/rails.rb
|
155
|
+
- lib/api_hammer/request_logger.rb
|
156
|
+
- lib/api_hammer/show_text_exceptions.rb
|
157
|
+
- lib/api_hammer/tasks/gem_available_updates.rb
|
158
|
+
- lib/api_hammer/trailing_newline.rb
|
159
|
+
- lib/api_hammer/version.rb
|
160
|
+
- lib/api_hammer/weblink.rb
|
161
|
+
- test/check_required_params_test.rb
|
162
|
+
- test/halt_test.rb
|
163
|
+
- test/helper.rb
|
164
|
+
- test/request_logger_test.rb
|
165
|
+
- test/show_text_exceptions_test.rb
|
166
|
+
- test/trailing_newline_test.rb
|
167
|
+
- test/weblink_test.rb
|
168
|
+
homepage: https://github.com/notEthan/api_hammer
|
169
|
+
licenses:
|
170
|
+
- MIT
|
171
|
+
metadata: {}
|
172
|
+
post_install_message:
|
173
|
+
rdoc_options: []
|
174
|
+
require_paths:
|
175
|
+
- lib
|
176
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
requirements: []
|
187
|
+
rubyforge_project:
|
188
|
+
rubygems_version: 2.2.2
|
189
|
+
signing_key:
|
190
|
+
specification_version: 4
|
191
|
+
summary: an API tool
|
192
|
+
test_files:
|
193
|
+
- test/check_required_params_test.rb
|
194
|
+
- test/halt_test.rb
|
195
|
+
- test/helper.rb
|
196
|
+
- test/request_logger_test.rb
|
197
|
+
- test/show_text_exceptions_test.rb
|
198
|
+
- test/trailing_newline_test.rb
|
199
|
+
- test/weblink_test.rb
|
200
|
+
- ".simplecov"
|
201
|
+
has_rdoc:
|