apical 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +24 -0
  4. data/Gemfile.lock +70 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +94 -0
  7. data/Rakefile +55 -0
  8. data/VERSION +1 -0
  9. data/apical.gemspec +122 -0
  10. data/bin/apical +7 -0
  11. data/examples/taco_truck/Gemfile +7 -0
  12. data/examples/taco_truck/Gemfile.lock +50 -0
  13. data/examples/taco_truck/app.rb +13 -0
  14. data/examples/taco_truck/taco_truck.apical +18 -0
  15. data/lib/apical.rb +30 -0
  16. data/lib/apical/adapter.rb +21 -0
  17. data/lib/apical/adapters/http_adapter.rb +30 -0
  18. data/lib/apical/adapters/rack_adapter.rb +16 -0
  19. data/lib/apical/cli.rb +36 -0
  20. data/lib/apical/content_types.rb +62 -0
  21. data/lib/apical/resource.rb +138 -0
  22. data/lib/apical/resource_types.rb +36 -0
  23. data/lib/apical/runner.rb +104 -0
  24. data/lib/apical/writers/console_writer.rb +26 -0
  25. data/lib/apical/writers/html_writer.rb +37 -0
  26. data/spec/apical/adapters/http_adapter_spec.rb +24 -0
  27. data/spec/apical/cli_spec.rb +81 -0
  28. data/spec/apical/content_types_spec.rb +9 -0
  29. data/spec/apical/rack_adapter_spec.rb +12 -0
  30. data/spec/apical_spec.rb +360 -0
  31. data/spec/before_and_after_spec.rb +211 -0
  32. data/spec/fixtures/cli_example_1.rb +7 -0
  33. data/spec/fixtures/cli_example_2.rb +8 -0
  34. data/spec/fixtures/example_require.rb +3 -0
  35. data/spec/fixtures/load_paths_example.apical +1 -0
  36. data/spec/html_writer_spec.rb +117 -0
  37. data/spec/http_apical_spec.rb +23 -0
  38. data/spec/load_paths_spec.rb +12 -0
  39. data/spec/spec_helper.rb +19 -0
  40. data/spec/support/test_apps.rb +34 -0
  41. data/templates/apical_helper.rb +11 -0
  42. data/templates/layout.mustache +180 -0
  43. data/templates/resource.mustache +14 -0
  44. metadata +248 -0
@@ -0,0 +1,26 @@
1
+ module Apical
2
+ class ConsoleWriter
3
+ def initialize(runner)
4
+ @runner = runner
5
+ end
6
+
7
+ def write(stream)
8
+ @runner.run
9
+
10
+ stream.write( @runner.resources.map{|r| resource_text(r) }.join )
11
+ stream.rewind if @runner.resources.any?
12
+ stream
13
+ end
14
+
15
+ def resource_text(resource)
16
+ <<-TXT
17
+ method: #{resource.method}
18
+ path: #{resource.path}
19
+ desc: #{resource.desc}
20
+ params: #{resource.formatted_params}
21
+ response: #{resource.formatted_response}
22
+
23
+ TXT
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ module Apical
2
+ class HtmlWriter
3
+ def initialize(runner)
4
+ @runner = runner
5
+ end
6
+
7
+ def markdown_to_html(str)
8
+ Kramdown::Document.new(str).to_html
9
+ end
10
+
11
+ def write(stream)
12
+ @runner.run
13
+
14
+ stream.write(
15
+ Mustache.to_html(File.read(File.dirname(__FILE__) + '/../../../templates/layout.mustache'), {
16
+ name: @runner.name,
17
+ desc: markdown_to_html(@runner.desc),
18
+ content: @runner.resources.map {|r| resource_html(r) }.join
19
+ })
20
+ )
21
+ stream.rewind
22
+ stream
23
+ end
24
+
25
+ def resource_html(resource)
26
+ Mustache.to_html(File.read(File.dirname(__FILE__) + '/../../../templates/resource.mustache'), {
27
+ method: resource.method,
28
+ path: resource.path,
29
+ desc: markdown_to_html(resource.desc),
30
+ accept: resource.accept,
31
+ params: resource.formatted_params,
32
+ content_type: resource.content_type,
33
+ response: resource.formatted_response
34
+ })
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Apical::HttpAdapter do
4
+ before do
5
+ stub_request(:get, "http://mocksite.com/tacos.json").
6
+ to_return(:status => 200, :body => "{some:'json'}", :headers => {})
7
+ stub_request(:post, "http://mocksite.com/tacos.json").
8
+ to_return(:status => 200, :body => "{made: 'a taco'}", :headers => {})
9
+ end
10
+
11
+ subject { Apical::HttpAdapter.new(base_uri: "http://mocksite.com") }
12
+ describe "when making a GET request" do
13
+ it "should return the raw response" do
14
+ subject.get('/tacos.json').body.should == "{some:'json'}"
15
+ end
16
+ end
17
+
18
+ describe "when making a POST request" do
19
+ it "should return the raw response" do
20
+ subject.post("/tacos.json").body.should == "{made: 'a taco'}"
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ require 'sinatra/base'
4
+ require 'fileutils'
5
+
6
+ describe Apical::CLI do
7
+
8
+ def capture(*streams)
9
+ streams.map! { |stream| stream.to_s }
10
+ begin
11
+ result = StringIO.new
12
+ streams.each { |stream| eval "$#{stream} = result" }
13
+ yield
14
+ ensure
15
+ streams.each { |stream| eval("$#{stream} = #{stream.upcase}") }
16
+ end
17
+ result.string
18
+ end
19
+
20
+ describe "when printing the version information" do
21
+ let(:output) { capture(:stdout) { subject.version } }
22
+ it "should print the version number to STDOUT" do
23
+ output.should == "#{Apical::VERSION}\n"
24
+ end
25
+ end
26
+
27
+ describe "when compiling" do
28
+ before do
29
+ class ExampleApp < Sinatra::Base
30
+ get '/example.json' do
31
+ JSON.generate({ status: 'example, mothafucka' })
32
+ end
33
+ end
34
+ end
35
+
36
+ describe "using an input file" do
37
+ before { subject.options = { app: "ExampleApp" } }
38
+ let(:output) { capture(:stdout) { subject.compile(ROOT.join(*%w{spec fixtures cli_example_1.rb})) } }
39
+
40
+ it "should print its output to the console by default" do
41
+ output.should include("example get resource")
42
+ end
43
+ end
44
+
45
+ describe "with the app specified in the API file itself" do
46
+ let(:output) { capture(:stdout) { subject.compile(ROOT.join(*%w{spec fixtures cli_example_2.rb})) } }
47
+
48
+ it "should print its output to the console by default" do
49
+ output.should include("example get resource")
50
+ end
51
+ end
52
+
53
+ describe "with HTML format specified" do
54
+ before { subject.options = { format: "html" } }
55
+ let(:output) { capture(:stdout) { subject.compile(ROOT.join(*%w{spec fixtures cli_example_1.rb})) } }
56
+
57
+ it "should print HTML output to STDOUT" do
58
+ output.should include("<html")
59
+ end
60
+ end
61
+
62
+ describe "with an output file specified" do
63
+ before do
64
+ subject.options = {
65
+ app: "ExampleApp",
66
+ output_path: ROOT.join(*%w{spec fixtures cli_example_1.html})
67
+ }
68
+ end
69
+
70
+ after do
71
+ FileUtils.rm subject.options[:output_path]
72
+ end
73
+
74
+ it "should write the output to the file instead of stdout" do
75
+ subject.compile(ROOT.join(*%w{spec fixtures cli_example_1.rb}))
76
+ File.read(subject.options[:output_path]).should include('example get resource')
77
+ end
78
+ end
79
+ end
80
+ end
81
+
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Apical::JsonContentType do
4
+ its(:to_s) { should == 'json' }
5
+ end
6
+
7
+ describe Apical::FormContentType do
8
+ its(:to_s) { should == 'form' }
9
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe Apical::RackAdapter do
4
+ subject { Apical::RackAdapter.new(app: TestApp) }
5
+ describe "when making a GET request" do
6
+ it "should return the raw response" do
7
+ subject.get('/tacos.json').body.should ==
8
+ JSON.generate([ { meat: 'beef' }, { meat: 'chicken' } ])
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,360 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Apical do
4
+ def stream_from writer, doc
5
+ StringIO.new.tap do |stream|
6
+ writer.new(doc).write(stream)
7
+ end.read
8
+ end
9
+
10
+ before do
11
+ @doc = Apical.new do
12
+ adapter :rack, app: TestApp
13
+ name "The Tacos API"
14
+ desc "An API for the making and consumption of delicious tacos"
15
+
16
+ get '/tacos.json' do
17
+ desc "Get all tacos"
18
+ end
19
+ end
20
+ end
21
+
22
+ subject { @doc }
23
+
24
+ its(:name) { should == "The Tacos API" }
25
+ its(:desc) { should == "An API for the making and consumption of delicious tacos" }
26
+
27
+ it "should have one resource" do
28
+ subject.resources.length.should == 1
29
+ end
30
+
31
+ it "returns itself after a run" do
32
+ @doc.run.should be(@doc)
33
+ end
34
+
35
+ describe "the /tacos.json resource" do
36
+ before do
37
+ @doc.run
38
+ end
39
+
40
+ subject { @doc.resources.first }
41
+ its(:method) { should == 'GET' }
42
+ its(:path) { should == '/tacos.json' }
43
+ its(:response_body) { should == JSON.generate([ { meat: 'beef' }, { meat: 'chicken' } ]) }
44
+ end
45
+
46
+ context "a Resource" do
47
+ describe "when setting the accept, content type and authorization headers" do
48
+ let(:doc) do
49
+ Apical.new do
50
+
51
+ adapter :rack, app: TestApp
52
+
53
+ get '/tacos.json' do
54
+ desc "Stuff"
55
+ accept :json
56
+ content_type :json
57
+ auth_header "One To Rule Them All"
58
+ end
59
+ end
60
+ end
61
+
62
+ it "sets the headers on the resource" do
63
+ doc.run.resources.first.tap do |res|
64
+ res.accept.should be_a(Apical::JsonContentType)
65
+ res.content_type.should be_a(Apical::JsonContentType)
66
+ res.custom_header('Authorization').should == 'One To Rule Them All'
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "when setting the default accept, content type and authorization headers" do
72
+ before do
73
+ @doc = Apical.new do
74
+ adapter :rack, app: TestApp
75
+ accept :json
76
+ content_type :json
77
+ auth_header "OAuth 123abc456"
78
+
79
+ post '/tacos.json' do
80
+ desc "Make a new delicious taco"
81
+ params do
82
+ { meat: 'beef', lettuce: true }
83
+ end
84
+ end
85
+ end
86
+
87
+ @doc.run
88
+ end
89
+
90
+ subject { @doc }
91
+
92
+ it "should propagate the accept type to its resources" do
93
+ @doc.resources.first.accept.should be_a(Apical::JsonContentType)
94
+ end
95
+
96
+ it "should propagate the content type to its resources" do
97
+ @doc.resources.first.content_type.should be_a(Apical::JsonContentType)
98
+ end
99
+
100
+ it "propagates the authorization header to its resources" do
101
+ @doc.resources.first.auth_header.should == "OAuth 123abc456"
102
+ end
103
+ end
104
+ end
105
+
106
+ describe "when setting a custom header" do
107
+ let(:doc) do
108
+ Apical.new do
109
+ adapter :rack, app: TestApp
110
+ get '/tacos.json' do
111
+ before do
112
+ @character = "Gandalf"
113
+ end
114
+
115
+ header "Custom-Header", "One To Rule Them All"
116
+ header "Custom-Header-By-Lambda", Proc.new { @character }
117
+ end
118
+ end
119
+ end
120
+
121
+ it "sets the header on the resource when specifying a string value" do
122
+ doc.run.resources.first.custom_header('Custom-Header').should == "One To Rule Them All"
123
+ end
124
+
125
+ it "calls the proc bound to the resource when setting a header with a proc value" do
126
+ doc.run.resources.first.custom_header('Custom-Header-By-Lambda').should == "Gandalf"
127
+ end
128
+ end
129
+
130
+ describe "when writing to the console" do
131
+ subject { stream_from Apical::ConsoleWriter, @doc }
132
+
133
+ it "should write the request method" do
134
+ subject.should include('GET')
135
+ end
136
+
137
+ it "should write the request path" do
138
+ subject.should include("/tacos.json")
139
+ end
140
+
141
+ it "should write the response body" do
142
+ subject.should include('meat')
143
+ end
144
+
145
+ describe "when accepting JSON as input" do
146
+ before do
147
+ @doc = Apical.new do
148
+ adapter :rack, app: TestApp
149
+ post '/tacos.json' do
150
+ desc "Make a new delicious taco"
151
+ accept :json
152
+ params do
153
+ { meat: 'beef', lettuce: true }
154
+ end
155
+ end
156
+ end
157
+
158
+ @doc.run
159
+ end
160
+
161
+ subject { stream_from Apical::ConsoleWriter, @doc }
162
+
163
+ it "should prett-print the JSON data" do
164
+ subject.should include(%Q|{\n \"meat\": \"beef\",\n \"lettuce\": true\n}\n|)
165
+ end
166
+ end
167
+
168
+ describe "when providing JSON as output" do
169
+ before do
170
+ @doc = Apical.new do
171
+ adapter :rack, app: TestApp
172
+ post '/tacos.json' do
173
+ desc "Make a new delicious taco"
174
+ content_type :json
175
+ params do
176
+ { meat: 'beef', lettuce: true }
177
+ end
178
+ end
179
+ end
180
+
181
+ @doc.run
182
+ end
183
+
184
+ subject { stream_from Apical::ConsoleWriter, @doc }
185
+
186
+ it "should pretty-print the response JSON data" do
187
+ subject.should include(%Q|"body": "meat=beef&lettuce=true"|)
188
+ end
189
+ end
190
+ end
191
+
192
+ describe "when writing to HTML" do
193
+ subject { stream_from Apical::HtmlWriter, @doc }
194
+
195
+ it "should write the request method" do
196
+ subject.should include('GET')
197
+ end
198
+
199
+ it "should write the request path" do
200
+ subject.should include("/tacos.json")
201
+ end
202
+
203
+ it "should write the response body" do
204
+ subject.should include('meat')
205
+ end
206
+
207
+ describe "when accepting JSON as input" do
208
+ before do
209
+ @doc = Apical.new do
210
+ adapter :rack, app: TestApp
211
+ post '/tacos.json' do
212
+ desc "Make a new delicious taco"
213
+ accept :json
214
+ params do
215
+ { meat: 'beef', lettuce: true }
216
+ end
217
+ end
218
+ end
219
+
220
+ @doc.run
221
+ end
222
+
223
+ subject { stream_from Apical::HtmlWriter, @doc }
224
+
225
+ it "should pretty-print the JSON data in the IN section" do
226
+ subject.should include("{\n &quot;meat&quot;: &quot;beef&quot;,\n &quot;lettuce&quot;: true\n}")
227
+ end
228
+ end
229
+
230
+ describe "when providing JSON as output" do
231
+ before do
232
+ @doc = Apical.new do
233
+ adapter :rack, app: TestApp
234
+ post '/tacos.json' do
235
+ desc "Make a new delicious taco"
236
+ content_type :json
237
+ params do
238
+ { meat: 'beef', lettuce: true }
239
+ end
240
+ end
241
+ end
242
+
243
+ @doc.run
244
+ end
245
+
246
+ subject { stream_from Apical::HtmlWriter, @doc }
247
+
248
+ it "should pretty-print the JSON data in the OUT section" do
249
+ # subject.should include("{\n &quot;meat&quot;: &quot;beef&quot;,\n &quot;lettuce&quot;: true\n}")
250
+ subject.should include("&quot;body&quot;: &quot;meat=beef&amp;lettuce=true&quot;")
251
+ end
252
+ end
253
+ end
254
+
255
+ describe "different request methods" do
256
+ before do
257
+ @doc = Apical.new do
258
+ adapter :rack, app: RequestMethodsApp
259
+ get '/get.json' do
260
+ desc "a get request"
261
+ end
262
+
263
+ post '/post.json' do
264
+ desc "a post request"
265
+ end
266
+
267
+ put '/put.json' do
268
+ desc "a put request"
269
+ end
270
+
271
+ delete '/delete.json' do
272
+ desc "a delete request"
273
+ end
274
+
275
+ options '/options.json' do
276
+ desc "an options request"
277
+ end
278
+ end
279
+ end
280
+
281
+ it "should have all five resources" do
282
+ @doc.resources.length.should == 5
283
+ end
284
+ end
285
+
286
+ describe "when specifying a request with parameters" do
287
+ before do
288
+ @doc = Apical.new do
289
+ adapter :rack, app: TestApp
290
+ post '/tacos.json' do
291
+ accept :json
292
+ content_type :json
293
+ desc "Make a new delicious taco"
294
+ params do
295
+ { meat: 'beef', lettuce: true }
296
+ end
297
+ end
298
+ end
299
+
300
+ @doc.run
301
+ end
302
+
303
+ subject { @doc.resources.first }
304
+
305
+ its(:params) { should == { meat: 'beef', lettuce: true } }
306
+ end
307
+
308
+ describe "when specifying a request with an interpolated path" do
309
+ before do
310
+ @doc = Apical.new do
311
+ adapter :rack, app: TestApp
312
+ get '/tacos/:id.json' do
313
+ accept :json
314
+ content_type :json
315
+ desc "Get a specific taco by its ID"
316
+ params do
317
+ { id: 123 }
318
+ end
319
+ end
320
+ end
321
+
322
+ @doc.run
323
+ end
324
+
325
+ subject { @doc.resources.first }
326
+
327
+ its(:path) { should == '/tacos/:id.json' }
328
+ its(:params) { should == { id: 123 } }
329
+ its(:response_body) { should == JSON.generate({ meat: 'beef', id: '123' }) }
330
+ end
331
+
332
+ describe "when specifying a resource with params that reference instance variables" do
333
+ before do
334
+ @doc = Apical.new do
335
+ adapter :rack, app: TestApp
336
+ get '/tacos/:id.json' do
337
+ accept :json
338
+ content_type :json
339
+ desc "Get a specific taco by its ID"
340
+
341
+ before do
342
+ @taco = { id: 123 }
343
+ end
344
+
345
+ params do
346
+ { id: @taco[:id] }
347
+ end
348
+ end
349
+ end
350
+
351
+ @doc.run
352
+ end
353
+
354
+ subject { @doc.resources.first }
355
+
356
+ its(:params) { should == { id: 123 } }
357
+ end
358
+
359
+ end
360
+