apiculture 0.1.6 → 0.2.0

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