apiculture 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,113 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe "Apiculture.api_documentation" do
4
+ let(:app) {
5
+ Class.new(Sinatra::Base) do
6
+ extend Apiculture
7
+
8
+ markdown_string 'This API is very important. Because it has to do with pancakes.'
9
+
10
+ documentation_build_time!
11
+
12
+ desc 'Order a pancake'
13
+ required_param :diameter, "Diameter of the pancake", Integer
14
+ param :topping, 'Type of topping', String
15
+ responds_with 200, 'When the pancake is created succesfully', {id: 'abdef..c21'}
16
+ api_method :post, '/pancakes' do
17
+ end
18
+
19
+ desc 'Check the pancake status'
20
+ route_param :id, 'Pancake ID to check status on'
21
+ responds_with 200, 'When the pancake is found', {status: 'Baking'}
22
+ responds_with 404, 'When no such pancake exists', {status: 'No such pancake'}
23
+ api_method :get, '/pancake/:id' do
24
+ end
25
+
26
+ desc 'Throw away the pancake'
27
+ route_param :id, 'Pancake ID to delete'
28
+ api_method :delete, '/pancake/:id' do
29
+ end
30
+ end
31
+ }
32
+
33
+ it 'generates app documentation as HTML without the body element' do
34
+ docco = app.api_documentation
35
+ generated_html = docco.to_html_fragment
36
+
37
+ expect(generated_html).not_to include('<body')
38
+ expect(generated_html).to include('Pancake ID to check status on')
39
+ expect(generated_html).to include('Pancake ID to delete')
40
+ end
41
+
42
+ it 'generates app documentation in HTML' do
43
+ docco = app.api_documentation
44
+ generated_html = docco.to_html
45
+
46
+ if ENV['SHOW_TEST_DOC']
47
+ File.open('t.html', 'w') do |f|
48
+ f.write(generated_html)
49
+ f.flush
50
+ `open #{f.path}`
51
+ end
52
+ end
53
+
54
+ expect(generated_html).to include('<body')
55
+ expect(generated_html).to include('Pancake ID to check status on')
56
+ expect(generated_html).to include('When the pancake is created succesfully')
57
+ expect(generated_html).to include('"id": "abdef..c21"')
58
+ end
59
+
60
+ it 'generates app documentation in Markdown' do
61
+ docco = app.api_documentation
62
+ generated_markdown = docco.to_markdown
63
+
64
+ expect(generated_markdown).not_to include('<body')
65
+ expect(generated_markdown).to include('## POST /pancakes')
66
+ end
67
+
68
+ it 'generates app documentation honoring the mount point' do
69
+ overridden = Class.new(Sinatra::Base) do
70
+ extend Apiculture
71
+ mounted_at '/api/v2/'
72
+ api_method :get, '/pancakes' do
73
+ end
74
+ end
75
+
76
+ generated_markdown = overridden.api_documentation.to_markdown
77
+ expect(generated_markdown).to include('## GET /api/v2/pancakes')
78
+ end
79
+
80
+ it 'generates app documentation injecting the inline Markdown strings' do
81
+ app_class = Class.new(Sinatra::Base) do
82
+ extend Apiculture
83
+ markdown_string '# This describes important stuff'
84
+ api_method :get, '/pancakes' do
85
+ end
86
+ markdown_string '# This describes even more important stuff'
87
+ markdown_string 'This is a paragraph'
88
+ end
89
+
90
+ generated_html = app_class.api_documentation.to_html
91
+ expect(generated_html).to include('<h2>GET /pancakes</h2>')
92
+ expect(generated_html).to include('<h1>This describes even more important stuff')
93
+ expect(generated_html).to include('<h1>This describes important stuff')
94
+ expect(generated_html).to include('<p>This is a paragraph')
95
+ end
96
+
97
+ context 'with a file containing Markdown that has to be spliced into the docs' do
98
+ before(:each) { File.open('./TEST.md', 'w') {|f| f << "# This is an important header"} }
99
+ after(:each) { File.unlink('./TEST.md') }
100
+ it 'splices the contents of the file using markdown_file' do
101
+ app_class = Class.new(Sinatra::Base) do
102
+ extend Apiculture
103
+ markdown_file './TEST.md'
104
+ api_method :get, '/pancakes' do
105
+ end
106
+ end
107
+
108
+ generated_html = app_class.api_documentation.to_html
109
+ expect(generated_html).to include('<h2>GET /pancakes</h2>')
110
+ expect(generated_html).to include('<h1>This is an important header')
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,80 @@
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 thickness', 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
+
17
+ documenter = described_class.new(definition)
18
+
19
+ generated_html = documenter.to_html_fragment
20
+ generated_markdown = documenter.to_markdown
21
+
22
+ expect(generated_html).not_to include('<body>')
23
+
24
+ expect(generated_html).to include('<h2>GET /pancake/:pan_id/bake</h2>')
25
+ expect(generated_html).to include('<p>This action bakes pancakes</p>')
26
+ expect(generated_html).to include('<h3>URL parameters</h3>')
27
+ expect(generated_html).to include('ID of the pancake frying pan')
28
+ expect(generated_html).to include('<h3>Request parameters</h3>')
29
+ expect(generated_html).to include('<td>Pancake name</td>')
30
+ end
31
+
32
+ it 'generates HTML from an ActionDefinition without route params' do
33
+ definition = Apiculture::ActionDefinition.new
34
+
35
+ definition.description = "This action bakes pancakes"
36
+ definition.parameters << Apiculture::Parameter.new(:name, 'Pancake name', true, String, :to_s)
37
+ definition.parameters << Apiculture::Parameter.new(:thickness, 'Pancake thickness', false, Float, :to_f)
38
+ definition.parameters << Apiculture::Parameter.new(:diameter, 'Pancake diameter', false, Integer, :to_i)
39
+
40
+ definition.http_verb = 'get'
41
+ definition.path = '/pancake'
42
+
43
+ documenter = described_class.new(definition)
44
+ generated_html = documenter.to_html_fragment
45
+
46
+ expect(generated_html).not_to include('<h3>URL parameters</h3>')
47
+ end
48
+
49
+ it 'generates HTML from an ActionDefinition without request params' do
50
+ definition = Apiculture::ActionDefinition.new
51
+
52
+ definition.description = "This action bakes pancakes"
53
+
54
+ definition.route_parameters << Apiculture::RouteParameter.new(:pan_id, 'ID of the pancake frying pan')
55
+ definition.http_verb = 'get'
56
+ definition.path = '/pancake/:pan_id/bake'
57
+
58
+ documenter = described_class.new(definition)
59
+
60
+ generated_html = documenter.to_html_fragment
61
+ generated_markdown = documenter.to_markdown
62
+
63
+ expect(generated_html).not_to include('<h3>Request parameters</h3>')
64
+ end
65
+
66
+ it 'generates Markdown from an ActionDefinition with a mountpoint' do
67
+ definition = Apiculture::ActionDefinition.new
68
+
69
+ definition.description = "This action bakes pancakes"
70
+
71
+ definition.route_parameters << Apiculture::RouteParameter.new(:pan_id, 'ID of the pancake frying pan')
72
+ definition.http_verb = 'get'
73
+ definition.path = '/pancake/:pan_id/bake'
74
+
75
+ documenter = described_class.new(definition, '/api/v1')
76
+
77
+ generated_markdown = documenter.to_markdown
78
+ expect(generated_markdown).to include('## GET /api/v1/pancake/:pan_id')
79
+ end
80
+ end
@@ -0,0 +1,263 @@
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(Sinatra::Base) do
14
+ settings.show_exceptions = false
15
+ settings.raise_errors = true
16
+ extend Apiculture
17
+
18
+ post '/things/*' do
19
+ return params.inspect
20
+ end
21
+ end
22
+
23
+ post '/things/a/b/c/d', {'foo' => 'bar'}
24
+ expect(last_response.body).to eq("{\"foo\"=>\"bar\", \"splat\"=>[\"a/b/c/d\"], \"captures\"=>[\"a/b/c/d\"]}")
25
+ end
26
+
27
+ it 'flags :captures as a reserved Sinatra parameter when used as a URL param' do
28
+ expect {
29
+ Class.new(Sinatra::Base) do
30
+ extend Apiculture
31
+ route_param :captures, "Something it captures"
32
+ api_method(:get, '/thing/:captures') { raise "Should never be called" }
33
+ end
34
+ }.to raise_error(/\:captures is a reserved magic parameter name/)
35
+ end
36
+
37
+ it 'flags :captures as a reserved Sinatra parameter when used as a request param' do
38
+ expect {
39
+ Class.new(Sinatra::Base) do
40
+ extend Apiculture
41
+ param :captures, "Something it captures", String
42
+ api_method(:get, '/thing') { raise "Should never be called" }
43
+ end
44
+ }.to raise_error(/\:captures is a reserved magic parameter name/)
45
+ end
46
+
47
+ it 'flags :splat as a reserved Sinatra parameter when used as a URL param' do
48
+ expect {
49
+ Class.new(Sinatra::Base) do
50
+ extend Apiculture
51
+ route_param :splat, "Something it splats"
52
+ api_method(:get, '/thing/:splat') { raise "Should never be called" }
53
+ end
54
+ }.to raise_error(/\:splat is a reserved magic parameter name/)
55
+ end
56
+
57
+ it 'flags :splat as a reserved Sinatra parameter when used as a request param' do
58
+ expect {
59
+ Class.new(Sinatra::Base) do
60
+ extend Apiculture
61
+ param :splat, "Something it splats", String
62
+ api_method(:get, '/thing') { raise "Should never be called" }
63
+ end
64
+ }.to raise_error(/\:splat is a reserved magic parameter name/)
65
+ end
66
+
67
+ it 'flags URL and request params of the same name' do
68
+ expect {
69
+ Class.new(Sinatra::Base) do
70
+ extend Apiculture
71
+ route_param :id, 'Id of the thing'
72
+ param :id, "Something it identifies (conflict)", String
73
+ api_method(:get, '/thing/:id') { raise "Should never be called" }
74
+ end
75
+ }.to raise_error(/\:id mentioned twice/)
76
+ end
77
+
78
+ it "defines a basic API that can be called" do
79
+ $created_thing = nil
80
+ @app_class = Class.new(Sinatra::Base) do
81
+ settings.show_exceptions = false
82
+ settings.raise_errors = true
83
+
84
+ extend Apiculture
85
+
86
+ desc "Create a Thing with a name"
87
+ route_param :id, "The ID of the thing"
88
+ required_param :name, "Name of the thing", String
89
+ api_method :post, '/thing/:id' do | thing_id |
90
+ $created_thing = {id: thing_id, name: params[:name]}
91
+ 'Wild success'
92
+ end
93
+ end
94
+
95
+ post '/thing/123', {name: 'Monsieur Thing'}
96
+ expect(last_response.body).to eq('Wild success')
97
+ expect($created_thing).to eq({id: '123', name: 'Monsieur Thing'})
98
+ end
99
+
100
+ it "serves the API documentation at a given URL using serve_api_documentation_at" do
101
+ $created_thing = nil
102
+ @app_class = Class.new(Sinatra::Base) do
103
+ settings.show_exceptions = false
104
+ settings.raise_errors = true
105
+
106
+ extend Apiculture
107
+
108
+ desc "Create a Thing with a name"
109
+ required_param :name, "Name of the thing", String
110
+ api_method( :post, '/thing/:id') {}
111
+ serve_api_documentation_at('/documentation')
112
+ end
113
+
114
+ get '/documentation'
115
+ expect(last_response['Content-Type']).to include('text/html')
116
+ expect(last_response.body).to include('Create a Thing')
117
+ end
118
+
119
+ it 'raises when a required param is not provided' do
120
+ @app_class = Class.new(Sinatra::Base) do
121
+ settings.show_exceptions = false
122
+ settings.raise_errors = true
123
+ extend Apiculture
124
+
125
+ required_param :name, "Name of the thing", String
126
+ api_method :post, '/thing' do
127
+ raise "Should never be called"
128
+ end
129
+ end
130
+
131
+ expect {
132
+ post '/thing', {}
133
+ }.to raise_error('Missing parameter :name')
134
+ end
135
+
136
+ it 'verifies the parameter type' do
137
+ @app_class = Class.new(Sinatra::Base) do
138
+ settings.show_exceptions = false
139
+ settings.raise_errors = true
140
+ extend Apiculture
141
+
142
+ required_param :number, "Number of the thing", Integer
143
+ api_method :post, '/thing' do
144
+ raise "Should never be called"
145
+ end
146
+ end
147
+
148
+ expect {
149
+ post '/thing', {number: '123'}
150
+ }.to raise_error('Received String, expected Integer for :number')
151
+ end
152
+
153
+ it 'suppresses parameters that are not defined in the action definition' do
154
+ @app_class = Class.new(Sinatra::Base) do
155
+ settings.show_exceptions = false
156
+ settings.raise_errors = true
157
+ extend Apiculture
158
+
159
+ api_method :post, '/thing' do
160
+ raise ":evil_ssh_injection should have wiped from params{}" if params[:evil_ssh_injection]
161
+ 'All is well'
162
+ end
163
+ end
164
+
165
+ post '/thing', {evil_ssh_injection: 'I am Homakov!'}
166
+ expect(last_response).to be_ok
167
+ end
168
+
169
+ it 'raises when describing a route parameter that is not included in the path' do
170
+ expect {
171
+ Class.new(Sinatra::Base) do
172
+ extend Apiculture
173
+ route_param :thing_id, "The ID of the thing"
174
+ api_method(:get, '/thing/:id') { raise "Should never be called" }
175
+ end
176
+ }.to raise_error('Parameter :thing_id not present in path "/thing/:id"')
177
+ end
178
+
179
+ it 'applies a symbol typecast by calling a method on the parameter value' do
180
+ @app_class = Class.new(Sinatra::Base) do
181
+ settings.show_exceptions = false
182
+ settings.raise_errors = true
183
+ extend Apiculture
184
+
185
+ required_param :number, "Number of the thing", Integer, :cast => :to_i
186
+ api_method :post, '/thing' do
187
+ raise "Not cast" unless params[:number] == 123
188
+ 'Total success'
189
+ end
190
+ end
191
+ post '/thing', {number: '123'}
192
+ expect(last_response.body).to eq('Total success')
193
+ end
194
+
195
+ it 'applies a Proc typecast by calling the proc (for example - for ISO8601 time)' do
196
+ @app_class = Class.new(Sinatra::Base) do
197
+ settings.show_exceptions = false
198
+ settings.raise_errors = true
199
+ extend Apiculture
200
+
201
+ required_param :when, "When it happened", Time, cast: ->(v){ Time.parse(v) }
202
+ api_method :post, '/occurrence' do
203
+ raise "Not cast" unless params[:when].year == 2015
204
+ raise "Not cast" unless params[:when].month == 7
205
+ 'Total success'
206
+ end
207
+ end
208
+ post '/occurrence', {when: '2015-07-05T22:16:18Z'}
209
+ expect(last_response.body).to eq('Total success')
210
+ end
211
+ end
212
+
213
+ context 'Sinatra instance method extensions' do
214
+ it 'adds support for json_response' do
215
+ @app_class = Class.new(Sinatra::Base) do
216
+ extend Apiculture
217
+ settings.show_exceptions = false
218
+ settings.raise_errors = true
219
+ api_method :get, '/some-json' do
220
+ json_response({foo: 'bar'})
221
+ end
222
+ end
223
+
224
+ get '/some-json'
225
+ expect(last_response).to be_ok
226
+ expect(last_response['Content-Type']).to include('application/json')
227
+ parsed_body = JSON.load(last_response.body)
228
+ expect(parsed_body['foo']).to eq('bar')
229
+ end
230
+
231
+ it 'adds support for json_halt' do
232
+ @app_class = Class.new(Sinatra::Base) do
233
+ extend Apiculture
234
+ settings.show_exceptions = false
235
+ settings.raise_errors = true
236
+ api_method :get, '/simple-halt' do
237
+ json_halt "Nein."
238
+ raise "This should never be called"
239
+ end
240
+ api_method :get, '/halt-with-custom-status' do
241
+ json_halt 'Nein.', status: 503
242
+ raise "This should never be called"
243
+ end
244
+ api_method :get, '/halt-with-error-payload' do
245
+ json_halt 'Nein.', teapot: true
246
+ raise "This should never be called"
247
+ end
248
+ end
249
+
250
+ get '/simple-halt'
251
+ expect(last_response.status).to eq(400)
252
+ expect(last_response['Content-Type']).to include('application/json')
253
+ parsed_body = JSON.load(last_response.body)
254
+ expect(parsed_body).to eq({"error"=>"Nein."})
255
+
256
+ get '/halt-with-error-payload'
257
+ expect(last_response.status).to eq(400)
258
+ expect(last_response['Content-Type']).to include('application/json')
259
+ parsed_body = JSON.load(last_response.body)
260
+ expect(parsed_body).to eq({"error"=>"Nein.", "teapot"=>true})
261
+ end
262
+ end
263
+ end