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 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
@@ -0,0 +1,3 @@
1
+ # ApiHammer
2
+
3
+ An API Tool
data/Rakefile.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'rake/testtask'
2
+ Rake::TestTask.new do |t|
3
+ t.name = 'test'
4
+ t.test_files = FileList['test/**/*_test.rb']
5
+ t.verbose = true
6
+ end
7
+ task 'default' => 'test'
8
+
9
+ require 'yard'
10
+ YARD::Rake::YardocTask.new do |t|
11
+ end
@@ -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,10 @@
1
+ require 'api_hammer/halt'
2
+ require 'api_hammer/check_required_params'
3
+
4
+ module ApiHammer::Rails
5
+ def self.included(klass)
6
+ (@on_included || []).each do |included_proc|
7
+ included_proc.call(klass)
8
+ end
9
+ end
10
+ 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,3 @@
1
+ module ApiHammer
2
+ VERSION = "0.0.1"
3
+ 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: