apiculture 0.0.12

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.
@@ -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