apical 0.1.4
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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +70 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +94 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/apical.gemspec +122 -0
- data/bin/apical +7 -0
- data/examples/taco_truck/Gemfile +7 -0
- data/examples/taco_truck/Gemfile.lock +50 -0
- data/examples/taco_truck/app.rb +13 -0
- data/examples/taco_truck/taco_truck.apical +18 -0
- data/lib/apical.rb +30 -0
- data/lib/apical/adapter.rb +21 -0
- data/lib/apical/adapters/http_adapter.rb +30 -0
- data/lib/apical/adapters/rack_adapter.rb +16 -0
- data/lib/apical/cli.rb +36 -0
- data/lib/apical/content_types.rb +62 -0
- data/lib/apical/resource.rb +138 -0
- data/lib/apical/resource_types.rb +36 -0
- data/lib/apical/runner.rb +104 -0
- data/lib/apical/writers/console_writer.rb +26 -0
- data/lib/apical/writers/html_writer.rb +37 -0
- data/spec/apical/adapters/http_adapter_spec.rb +24 -0
- data/spec/apical/cli_spec.rb +81 -0
- data/spec/apical/content_types_spec.rb +9 -0
- data/spec/apical/rack_adapter_spec.rb +12 -0
- data/spec/apical_spec.rb +360 -0
- data/spec/before_and_after_spec.rb +211 -0
- data/spec/fixtures/cli_example_1.rb +7 -0
- data/spec/fixtures/cli_example_2.rb +8 -0
- data/spec/fixtures/example_require.rb +3 -0
- data/spec/fixtures/load_paths_example.apical +1 -0
- data/spec/html_writer_spec.rb +117 -0
- data/spec/http_apical_spec.rb +23 -0
- data/spec/load_paths_spec.rb +12 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/test_apps.rb +34 -0
- data/templates/apical_helper.rb +11 -0
- data/templates/layout.mustache +180 -0
- data/templates/resource.mustache +14 -0
- 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,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
|
+
|
data/spec/apical_spec.rb
ADDED
@@ -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 "meat": "beef",\n "lettuce": 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 "meat": "beef",\n "lettuce": true\n}")
|
250
|
+
subject.should include(""body": "meat=beef&lettuce=true"")
|
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
|
+
|