apiculture 0.1.6 → 0.2.0

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.
@@ -1,102 +0,0 @@
1
- require_relative '../spec_helper'
2
- require_relative '../../lib/apiculture/method_documentation'
3
-
4
- describe Apiculture::MethodDocumentation do
5
- it 'generates HTML from an ActionDefinition with path, verb and both route and request params' do
6
- definition = Apiculture::ActionDefinition.new
7
-
8
- definition.description = "This action bakes pancakes"
9
- definition.parameters << Apiculture::Parameter.new(:name, 'Pancake name', true, String, :to_s)
10
- definition.parameters << Apiculture::Parameter.new(:thickness, 'Pancake **thick**ness', false, Float, :to_f)
11
- definition.parameters << Apiculture::Parameter.new(:diameter, 'Pancake diameter', false, Integer, :to_i)
12
-
13
- definition.route_parameters << Apiculture::RouteParameter.new(:pan_id, 'ID of the pancake frying pan')
14
- definition.http_verb = 'get'
15
- definition.path = '/pancake/:pan_id/bake'
16
- definition.responses << Apiculture::PossibleResponse.new(200, "Pancake has been baked", {diameter: 10, unit: "cm"})
17
- definition.responses << Apiculture::PossibleResponse.new(417, "Frying pan too cold", "ERR_NO_HEAT")
18
-
19
- documenter = described_class.new(definition)
20
-
21
- generated_html = documenter.to_html_fragment
22
- generated_markdown = documenter.to_markdown
23
-
24
- expect(generated_html).not_to include('<body>')
25
-
26
- expect(generated_html).to include('<h2>GET /pancake/:pan_id/bake</h2>')
27
- expect(generated_html).to include('<p>This action bakes pancakes</p>')
28
- expect(generated_html).to include('<h3>URL parameters</h3>')
29
- expect(generated_html).to include('ID of the pancake frying pan')
30
- expect(generated_html).to include('<h3>Request parameters</h3>')
31
- expect(generated_html).to include('<p>Pancake name</p>')
32
- expect(generated_html).to include('<p>Pancake has been baked</p>')
33
- expect(generated_html).to include('<p>Frying pan too cold</p>')
34
- end
35
-
36
- it 'generates HTML from an ActionDefinition without route params' do
37
- definition = Apiculture::ActionDefinition.new
38
-
39
- definition.description = "This action bakes pancakes"
40
- definition.parameters << Apiculture::Parameter.new(:name, 'Pancake name', true, String, :to_s)
41
- definition.parameters << Apiculture::Parameter.new(:thickness, 'Pancake **thick**ness', false, Float, :to_f)
42
- definition.parameters << Apiculture::Parameter.new(:diameter, 'Pancake diameter', false, Integer, :to_i)
43
-
44
- definition.http_verb = 'get'
45
- definition.path = '/pancake'
46
-
47
- documenter = described_class.new(definition)
48
- generated_html = documenter.to_html_fragment
49
-
50
- expect(generated_html).not_to include('<h3>URL parameters</h3>')
51
- end
52
-
53
- it 'generates HTML from an ActionDefinition without request params' do
54
- definition = Apiculture::ActionDefinition.new
55
-
56
- definition.description = "This action bakes pancakes"
57
-
58
- definition.route_parameters << Apiculture::RouteParameter.new(:pan_id, 'ID of the pancake frying pan')
59
- definition.http_verb = 'get'
60
- definition.path = '/pancake/:pan_id/bake'
61
-
62
- documenter = described_class.new(definition)
63
-
64
- generated_html = documenter.to_html_fragment
65
- generated_markdown = documenter.to_markdown
66
-
67
- expect(generated_html).not_to include('<h3>Request parameters</h3>')
68
- end
69
-
70
- it 'generates HTML from an ActionDefinition with a casted route param' do
71
- definition = Apiculture::ActionDefinition.new
72
-
73
- definition.description = "This adds a topping to a pancake"
74
-
75
- definition.route_parameters << Apiculture::RouteParameter.new(:topping_id, 'ID of the pancake topping', Integer, cast: :to_i)
76
- definition.http_verb = 'get'
77
- definition.path = '/pancake/:topping_id'
78
-
79
- documenter = described_class.new(definition)
80
-
81
- generated_html = documenter.to_html_fragment
82
- generated_markdown = documenter.to_markdown
83
- expect(generated_html).to include('<h3>URL parameters</h3>')
84
- expect(generated_html).to include('Type after cast')
85
- end
86
-
87
-
88
- it 'generates Markdown from an ActionDefinition with a mountpoint' do
89
- definition = Apiculture::ActionDefinition.new
90
-
91
- definition.description = "This action bakes pancakes"
92
-
93
- definition.route_parameters << Apiculture::RouteParameter.new(:pan_id, 'ID of the pancake frying pan')
94
- definition.http_verb = 'get'
95
- definition.path = '/pancake/:pan_id/bake'
96
-
97
- documenter = described_class.new(definition, '/api/v1')
98
-
99
- generated_markdown = documenter.to_markdown
100
- expect(generated_markdown).to include('## GET /api/v1/pancake/:pan_id')
101
- end
102
- end
@@ -1,461 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
-
3
- describe "Apiculture" do
4
- include Rack::Test::Methods
5
-
6
- before(:each) { @app_class = nil }
7
- def app
8
- @app_class or raise "No @app_class defined in the example"
9
- end
10
-
11
- context 'as API definition DSL' do
12
- it 'allows all the standard Siantra DSL to go through without modifications' do
13
- @app_class = Class.new(Apiculture::App) do
14
- extend Apiculture
15
-
16
- post '/things/*' do
17
- params.inspect
18
- end
19
- end
20
-
21
- post '/things/a/b/c/d', {'foo' => 'bar'}
22
- expect(last_response.body).to eq("{\"foo\"=>\"bar\", \"splat\"=>[\"a/b/c/d\"], \"captures\"=>[\"a/b/c/d\"]}")
23
- end
24
-
25
- it 'flags :captures as a reserved Sinatra parameter when used as a URL param' do
26
- expect {
27
- Class.new(Apiculture::App) do
28
- extend Apiculture
29
- route_param :captures, "Something it captures"
30
- api_method(:get, '/thing/:captures') { raise "Should never be called" }
31
- end
32
- }.to raise_error(/\:captures is a reserved magic parameter name/)
33
- end
34
-
35
- it 'flags :captures as a reserved Sinatra parameter when used as a request param' do
36
- expect {
37
- Class.new(Apiculture::App) do
38
- extend Apiculture
39
- param :captures, "Something it captures", String
40
- api_method(:get, '/thing') { raise "Should never be called" }
41
- end
42
- }.to raise_error(/\:captures is a reserved magic parameter name/)
43
- end
44
-
45
- it 'flags :splat as a reserved Sinatra parameter when used as a URL param' do
46
- expect {
47
- Class.new(Apiculture::App) do
48
- extend Apiculture
49
- route_param :splat, "Something it splats"
50
- api_method(:get, '/thing/:splat') { raise "Should never be called" }
51
- end
52
- }.to raise_error(/\:splat is a reserved magic parameter name/)
53
- end
54
-
55
- it 'flags :splat as a reserved Sinatra parameter when used as a request param' do
56
- expect {
57
- Class.new(Apiculture::App) do
58
- extend Apiculture
59
- param :splat, "Something it splats", String
60
- api_method(:get, '/thing') { raise "Should never be called" }
61
- end
62
- }.to raise_error(/\:splat is a reserved magic parameter name/)
63
- end
64
-
65
- it 'flags URL and request params of the same name' do
66
- expect {
67
- Class.new(Apiculture::App) do
68
- extend Apiculture
69
- route_param :id, 'Id of the thing'
70
- param :id, "Something it identifies (conflict)", String
71
- api_method(:get, '/thing/:id') { raise "Should never be called" }
72
- end
73
- }.to raise_error(/\:id mentioned twice/)
74
- end
75
-
76
- it "defines a basic API that can be called" do
77
- $created_thing = nil
78
- @app_class = Class.new(Apiculture::App) do
79
- extend Apiculture
80
-
81
- desc "Create a Thing with a name"
82
- route_param :id, "The ID of the thing"
83
- required_param :name, "Name of the thing", String
84
- api_method :post, '/thing/:id' do | thing_id |
85
- $created_thing = {id: thing_id, name: params[:name]}
86
- 'Wild success'
87
- end
88
- end
89
-
90
- post '/thing/123', {name: 'Monsieur Thing'}
91
- expect(last_response.body).to eq('Wild success')
92
- expect($created_thing).to eq({id: '123', name: 'Monsieur Thing'})
93
- end
94
-
95
- it "serves the API documentation at a given URL using serve_api_documentation_at" do
96
- $created_thing = nil
97
- @app_class = Class.new(Apiculture::App) do
98
- extend Apiculture
99
-
100
- desc "Create a Thing with a name"
101
- required_param :name, "Name of the thing", String
102
- api_method( :post, '/thing/:id') {}
103
- serve_api_documentation_at('/documentation')
104
- end
105
-
106
- get '/documentation'
107
- expect(last_response['Content-Type']).to include('text/html')
108
- expect(last_response.body).to include('Create a Thing')
109
- end
110
-
111
- it 'raises when a required param is not provided' do
112
- @app_class = Class.new(Apiculture::App) do
113
- extend Apiculture
114
-
115
- required_param :name, "Name of the thing", String
116
- api_method :post, '/thing' do
117
- raise "Should never be called"
118
- end
119
- end
120
-
121
- expect {
122
- post '/thing', {}
123
- }.to raise_error('Missing parameter :name')
124
- end
125
-
126
- it 'verifies the parameter type' do
127
- @app_class = Class.new(Apiculture::App) do
128
- extend Apiculture
129
-
130
- required_param :number, "Number of the thing", Integer
131
- api_method :post, '/thing' do
132
- raise "Should never be called"
133
- end
134
- end
135
-
136
- expect {
137
- post '/thing', {number: '123'}
138
- }.to raise_error('Received String, expected Integer for :number')
139
- end
140
-
141
- it 'supports an arbitrary object with === as a type specifier for a parameter' do
142
- custom_matcher = Class.new do
143
- def ===(value)
144
- value == "Magic word"
145
- end
146
- end.new
147
-
148
- @app_class = Class.new(Apiculture::App) do
149
- extend Apiculture
150
-
151
- required_param :pretty_please, "Only a magic word will do", custom_matcher
152
- api_method :post, '/thing' do
153
- 'Ohai!'
154
- end
155
- end
156
-
157
- post '/thing', {pretty_please: 'Magic word'}
158
- expect(last_response).to be_ok
159
-
160
- expect {
161
- post '/thing', {pretty_please: 'not the magic word you are looking for'}
162
- }.to raise_error(Apiculture::ParameterTypeMismatch)
163
- end
164
-
165
- it 'suppresses parameters that are not defined in the action definition' do
166
- @app_class = Class.new(Apiculture::App) do
167
- extend Apiculture
168
-
169
- api_method :post, '/thing' do
170
- raise ":evil_ssh_injection should have wiped from params{}" if params[:evil_ssh_injection]
171
- 'All is well'
172
- end
173
- end
174
-
175
- post '/thing', {evil_ssh_injection: 'I am Homakov!'}
176
- expect(last_response).to be_ok
177
- end
178
-
179
- it 'allows route parameters that are not mentioned in the action definition, but are given in Sinatra path' do
180
- @app_class = Class.new(Apiculture::App) do
181
- extend Apiculture
182
-
183
- api_method :post, '/api-thing/:id_of_thing' do |id|
184
- raise 'id_of_thing must be passed' unless id == '123456'
185
- raise "id_of_thing must be present in params, but they were #{params.inspect}" unless params.keys.include?('id_of_thing')
186
- raise "id_of_thing must be string-accessible in params" unless params['id_of_thing'] == '123456'
187
- raise "id_of_thing must be symbol-accessible in params" unless params[:id_of_thing] == '123456'
188
- 'All is well'
189
- end
190
-
191
- post '/vanilla-thing/:id_of_thing' do |id|
192
- raise 'id_of_thing must be passed' unless id == '123456'
193
- raise "id_of_thing must be present in params, but they were #{params.inspect}" unless params.keys.include?('id_of_thing')
194
- raise "id_of_thing must be string-accessible in params" unless params['id_of_thing'] == '123456'
195
- raise "id_of_thing must be symbol-accessible in params" unless params[:id_of_thing] == '123456'
196
- 'All is well'
197
- end
198
- end
199
-
200
- post '/vanilla-thing/123456'
201
- expect(last_response).to be_ok
202
-
203
- post '/api-thing/123456'
204
- expect(last_response).to be_ok
205
- end
206
-
207
- it 'does not clobber the status set in a separate mutating call when using json_response' do
208
- @app_class = Class.new(Apiculture::App) do
209
- extend Apiculture
210
-
211
- api_method :post, '/api/:id' do
212
- status 201
213
- json_response({was_created: true})
214
- end
215
- end
216
-
217
- post '/api/123'
218
- expect(last_response.status).to eq(201)
219
- end
220
-
221
- it 'raises when describing a route parameter that is not included in the path' do
222
- expect {
223
- Class.new(Apiculture::App) do
224
- extend Apiculture
225
- route_param :thing_id, "The ID of the thing"
226
- api_method(:get, '/thing/:id') { raise "Should never be called" }
227
- end
228
- }.to raise_error('Parameter :thing_id not present in path "/thing/:id"')
229
- end
230
-
231
- it 'returns a 404 when a non existing route is called' do
232
- @app_class = Class.new(Apiculture::App) do
233
- extend Apiculture
234
-
235
- api_method :post, '/api' do
236
- [1]
237
- end
238
- end
239
-
240
- post '/api-404'
241
- expect(last_response.status).to eq(404)
242
- end
243
-
244
- it 'applies a symbol typecast by calling a method on the parameter value' do
245
- @app_class = Class.new(Apiculture::App) do
246
- extend Apiculture
247
-
248
- required_param :number, "Number of the thing", Integer, :cast => :to_i
249
- api_method :post, '/thing' do
250
- raise "Not cast" unless params[:number] == 123
251
- 'Total success'
252
- end
253
- end
254
- post '/thing', {number: '123'}
255
- expect(last_response.body).to eq('Total success')
256
- end
257
-
258
- it 'ensures current behaviour for route params is not changed' do
259
- @app_class = Class.new(Apiculture::App) do
260
- extend Apiculture
261
-
262
- route_param :number, "Number of the thing"
263
- api_method :post, '/thing/:number' do
264
- raise "Casted to int" if params[:number] == 123
265
- 'Total success'
266
- end
267
- end
268
- post '/thing/123'
269
- expect(last_response.body).to eq('Total success')
270
- end
271
-
272
- it 'supports returning a rack triplet' do
273
- @app_class = Class.new(Apiculture::App) do
274
- extend Apiculture
275
- api_method :get, '/rack' do
276
- [402, {'X-Money-In-The-Bank' => 'yes, please'}, ['Buy bitcoin']]
277
- end
278
- end
279
- get '/rack'
280
- expect(last_response.status).to eq 402
281
- expect(last_response.body).to eq 'Buy bitcoin'
282
- end
283
-
284
- it 'ensures current behaviour when no route params are present does not change' do
285
- @app_class = Class.new(Apiculture::App) do
286
- extend Apiculture
287
-
288
- param :number, "Number of the thing", Integer, cast: :to_i
289
- api_method :post, '/thing' do
290
- raise "Behaviour changed" unless params[:number] == 123
291
- 'Total success'
292
- end
293
- end
294
- post '/thing', {number: '123'}
295
- expect(last_response.body).to eq('Total success')
296
- end
297
-
298
- it 'applies a symbol typecast by calling a method on the route parameter value' do
299
- @app_class = Class.new(Apiculture::App) do
300
- extend Apiculture
301
-
302
- route_param :number, "Number of the thing", Integer, :cast => :to_i
303
- api_method :post, '/thing/:number' do
304
- raise "Not cast" unless params[:number] == 123
305
- 'Total success'
306
- end
307
- end
308
- post '/thing/123'
309
- expect(last_response.body).to eq('Total success')
310
- end
311
-
312
-
313
- it 'cast block arguments to the right type', run: true do
314
- @app_class = Class.new(Apiculture::App) do
315
- extend Apiculture
316
-
317
- route_param :number, "Number of the thing", Integer, :cast => :to_i
318
- api_method :post, '/thing/:number' do |number|
319
- raise "Not cast" unless number.is_a?(Integer)
320
- 'Total success'
321
- end
322
- end
323
- post '/thing/123'
324
- expect(last_response.body).to eq('Total success')
325
-
326
- # Double checking that bignums are okay, too
327
- bignum = 10**30
328
- post "/thing/#{bignum}"
329
- expect(last_response.body).to eq('Total success')
330
- end
331
-
332
-
333
- it 'merges route_params and regular params' do
334
- @app_class = Class.new(Apiculture::App) do
335
- extend Apiculture
336
-
337
- param :number, "Number of the thing", Integer, :cast => :to_i
338
- route_param :id, "Id of the thingy", Integer, :cast => :to_i
339
- route_param :awesome, "Hash of the thingy"
340
-
341
- api_method :post, '/thing/:id/:awesome' do |id|
342
- raise 'Not merged' unless params.has_key?("id")
343
- raise 'Not merged' unless params.has_key?("awesome")
344
- 'Thanks'
345
- end
346
- end
347
- post '/thing/1/true', {number: '123'}
348
- expect(last_response.body).to eq('Thanks')
349
- end
350
-
351
-
352
- it 'applies a Proc typecast by calling the proc (for example - for ISO8601 time)' do
353
- @app_class = Class.new(Apiculture::App) do
354
- extend Apiculture
355
-
356
- required_param :when, "When it happened", Time, cast: ->(v){ Time.parse(v) }
357
- api_method :post, '/occurrence' do
358
- raise "Not cast" unless params[:when].year == 2015
359
- raise "Not cast" unless params[:when].month == 7
360
- 'Total success'
361
- end
362
- end
363
- post '/occurrence', {when: '2015-07-05T22:16:18Z'}
364
- expect(last_response.body).to eq('Total success')
365
- end
366
- end
367
-
368
- context 'Sinatra instance method extensions' do
369
- it 'adds support for json_response' do
370
- @app_class = Class.new(Apiculture::App) do
371
- extend Apiculture
372
- api_method :get, '/some-json' do
373
- json_response({foo: 'bar'})
374
- end
375
- end
376
-
377
- get '/some-json'
378
- expect(last_response).to be_ok
379
- expect(last_response['Content-Type']).to include('application/json')
380
- parsed_body = JSON.load(last_response.body)
381
- expect(parsed_body['foo']).to eq('bar')
382
- end
383
-
384
- it 'adds support for json_response to set http status code', run: true do
385
- @app_class = Class.new(Apiculture::App) do
386
- extend Apiculture
387
- api_method :post, '/some-json' do
388
- json_response({foo: 'bar'}, status: 201)
389
- end
390
- end
391
-
392
- post '/some-json'
393
- expect(last_response.status).to eq(201)
394
- end
395
-
396
- it 'adds support for json_halt' do
397
- @app_class = Class.new(Apiculture::App) do
398
- extend Apiculture
399
- api_method :get, '/simple-halt' do
400
- json_halt "Nein."
401
- raise "This should never be called"
402
- end
403
- api_method :get, '/halt-with-custom-status' do
404
- json_halt 'Nein.', status: 503
405
- raise "This should never be called"
406
- end
407
- api_method :get, '/halt-with-error-payload' do
408
- json_halt 'Nein.', teapot: true
409
- raise "This should never be called"
410
- end
411
- end
412
-
413
- get '/simple-halt'
414
- expect(last_response.status).to eq(400)
415
- expect(last_response['Content-Type']).to include('application/json')
416
- parsed_body = JSON.load(last_response.body)
417
- expect(parsed_body).to eq({"error"=>"Nein."})
418
-
419
- get '/halt-with-error-payload'
420
- expect(last_response.status).to eq(400)
421
- expect(last_response['Content-Type']).to include('application/json')
422
- parsed_body = JSON.load(last_response.body)
423
- expect(parsed_body).to eq({"error"=>"Nein.", "teapot"=>true})
424
- end
425
-
426
- # Mocks didn't play well with setting the status in a sinatra action
427
- class NilTestAction < Apiculture::Action
428
- def perform
429
- status 204
430
- nil
431
- end
432
- end
433
- it 'allows returning an empty body when the status is 204' do
434
- @app_class = Class.new(Apiculture::App) do
435
- extend Apiculture
436
- api_method :get, '/nil204' do
437
- action_result NilTestAction
438
- end
439
- end
440
-
441
- get '/nil204'
442
- expect(last_response.status).to eq(204)
443
- expect(last_response.body).to be_empty
444
- end
445
-
446
- it "does not allow returning an empty body when the status isn't 204" do
447
- # Mock out the perform call so that status doesn't change from the default of 200
448
- expect_any_instance_of(NilTestAction).to receive(:perform).with(any_args).and_return(nil)
449
- @app_class = Class.new(Apiculture::App) do
450
- extend Apiculture
451
- api_method :get, '/nil200' do
452
- action_result NilTestAction
453
- end
454
- end
455
-
456
- expect{
457
- get '/nil200'
458
- }.to raise_error(RuntimeError)
459
- end
460
- end
461
- end
data/spec/spec_helper.rb DELETED
@@ -1,15 +0,0 @@
1
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
- $LOAD_PATH.unshift(File.dirname(__FILE__))
3
-
4
- require 'rspec'
5
- require 'apiculture'
6
- require 'rack'
7
- require 'rack/test'
8
-
9
- # Requires supporting files with custom matchers and macros, etc,
10
- # in ./support/ and its subdirectories.
11
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
-
13
- RSpec.configure do |config|
14
-
15
- end