api_hammer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: