Skip to content

Commit fd8b850

Browse files
author
Michael Bleigh
committed
Merge pull request ruby-grape#94 from dblock/frontier-desc-api
frontier: added support for desc blocks
2 parents 915c2e2 + accf045 commit fd8b850

File tree

5 files changed

+136
-37
lines changed

5 files changed

+136
-37
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ tmtags
1111
\#*
1212
.\#*
1313

14+
## REDCAR
15+
.redcar
16+
1417
## VIM
1518
*.swp
1619

@@ -23,7 +26,5 @@ pkg
2326
.yardoc/*
2427
dist
2528
Gemfile.lock
26-
*.orig
27-
*.rej
2829

2930
## PROJECT::SPECIFIC

README.markdown

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,32 +248,40 @@ describe Twitter::API do
248248
end
249249
```
250250

251-
## Inspecting an API
251+
## Describing and Inspecting an API
252252

253-
Grape exposes arrays of API versions and compiled routes. Each route contains a `route_prefix`, `route_version`, `route_namespace`, `route_method`, `route_path` and `route_params`.
253+
Grape lets you add a description to an API along with any other optional elements that can also be inspected at runtime.
254+
This can be useful for generating documentation.
254255

255256
```ruby
256257
class TwitterAPI < Grape::API
257258

258259
version 'v1'
260+
261+
desc "Retrieves the API version number."
259262
get "version" do
260263
api.version
261264
end
262265

263-
version 'v2'
264-
namespace "ns" do
265-
get "version" do
266-
api.version
267-
end
266+
desc "Reverses a string.", { :params =>
267+
{ "s" => { :desc => "string to reverse", :type => "string" }}
268+
}
269+
get "reverse" do
270+
params[:s].reverse
268271
end
269272
end
273+
```
274+
275+
Grape then exposes arrays of API versions and compiled routes. Each route contains a `route_prefix`, `route_version`, `route_namespace`, `route_method`, `route_path` and `route_params`. The description and the optional hash that follows the API path may contain any number of keys and its values are also accessible via dynamically-generated `route_[name]` functions.
270276

277+
```ruby
271278
TwitterAPI::versions # yields [ 'v1', 'v2' ]
272279
TwitterAPI::routes # yields an array of Grape::Route objects
273280
TwitterAPI::routes[0].route_version # yields 'v1'
281+
TwitterAPI::routes[0].route_description # yields [ { "s" => { :desc => "string to reverse", :type => "string" }} ]
274282
```
275283

276-
Grape also supports storing additional parameters with the route information. This can be useful for generating documentation. The optional hash that follows the API path may contain any number of keys and its values are also accessible via a dynamically-generated `route_[name]` function.
284+
Parameters can also be tagged to the method declaration itself.
277285

278286
```ruby
279287
class StringAPI < Grape::API

lib/grape/api.rb

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def reset!
3131
@route_set = Rack::Mount::RouteSet.new
3232
@endpoints = []
3333
@mountings = []
34+
@routes = nil
3435
end
3536

3637
def compile
@@ -103,6 +104,11 @@ def version(*args, &block)
103104
end
104105
end
105106
end
107+
108+
# Add a description to the next namespace or function.
109+
def desc(description, options = {})
110+
@last_description = options.merge({description: description})
111+
end
106112

107113
# Specify the default format for the API's
108114
# serializers. Currently only `:json` is
@@ -260,11 +266,13 @@ def mount(mounts)
260266
# end
261267
# end
262268
def route(methods, paths = ['/'], route_options = {}, &block)
263-
endpoints << Grape::Endpoint.new(settings.clone, {
269+
endpoint_options = {
264270
:method => methods,
265271
:path => paths,
266-
:route_options => (route_options || {})
267-
}, &block)
272+
:route_options => (route_options || {}).merge(@last_description || {})
273+
}
274+
endpoints << Grape::Endpoint.new(settings.clone, endpoint_options, &block)
275+
@last_description = nil
268276
end
269277

270278
def before(&block)
@@ -326,11 +334,11 @@ def middleware
326334
def routes
327335
@routes ||= prepare_routes
328336
end
329-
337+
330338
def versions
331339
@versions ||= []
332340
end
333-
341+
334342
protected
335343

336344
def prepare_routes

lib/grape/endpoint.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def initialize(settings, options = {}, &block)
2525

2626
options[:route_options] ||= {}
2727
end
28-
28+
2929
def routes
3030
@routes ||= prepare_routes
3131
end
@@ -50,8 +50,13 @@ def prepare_routes
5050
prepared_path = prepare_path(path)
5151
path = compile_path(prepared_path, !options[:app])
5252
regex = Rack::Mount::RegexpWithNamedGroups.new(path)
53-
path_params = regex.named_captures.map { |nc| nc[0] } - [ 'version', 'format' ]
54-
path_params |= (options[:route_options][:params] || [])
53+
path_params = {}
54+
# named parameters in the api path
55+
named_params = regex.named_captures.map { |nc| nc[0] } - [ 'version', 'format' ]
56+
named_params.each { |named_param| path_params[named_param] = "" }
57+
# route parameters declared via desc or appended to the api declaration
58+
route_params = (options[:route_options][:params] || {})
59+
path_params.merge!(route_params)
5560
request_method = (method.to_s.upcase unless method == :any)
5661
routes << Route.new(options[:route_options].clone.merge({
5762
:prefix => settings[:root_prefix],

spec/grape/api_spec.rb

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def app; subject end
5858
end
5959

6060
it 'should route if any media type is allowed' do
61-
61+
6262
end
6363
end
6464

@@ -123,7 +123,7 @@ def app; subject end
123123
get '/members/23'
124124
last_response.body.should == "23"
125125
end
126-
126+
127127
it 'should be callable with nil just to push onto the stack' do
128128
subject.namespace do
129129
version 'v2', :using => :path
@@ -136,7 +136,7 @@ def app; subject end
136136
get '/hello'
137137
last_response.body.should == "outer"
138138
end
139-
139+
140140
%w(group resource resources segment).each do |als|
141141
it "`.#{als}` should be an alias" do
142142
subject.send(als, :awesome) do
@@ -231,7 +231,7 @@ def app; subject end
231231
subject.route([:get, :post], '/:id/first') do
232232
"first"
233233
end
234-
234+
235235
subject.route([:get, :post], '/:id') do
236236
"ola"
237237
end
@@ -342,7 +342,7 @@ def app; subject end
342342
last_response.headers['Content-Type'].should eql 'application/json'
343343
end
344344
end
345-
345+
346346
context 'custom middleware' do
347347
class PhonyMiddleware
348348
def initialize(app, *args)
@@ -374,7 +374,7 @@ def call(env)
374374
{:middleware => [[PhonyMiddleware, 'foo']]}
375375
]
376376
subject.stub!(:settings).and_return(settings)
377-
377+
378378
subject.middleware.should eql [
379379
[PhonyMiddleware, 123],
380380
[PhonyMiddleware, 'abc'],
@@ -579,7 +579,7 @@ def hello
579579
subject.get '/def' do
580580
'def'
581581
end
582-
582+
583583
get '/new/abc'
584584
last_response.status.should eql 404
585585
get '/legacy/abc'
@@ -758,26 +758,103 @@ class TwitterAPI < Grape::API
758758
end
759759
describe "api structure with additional parameters" do
760760
before(:each) do
761-
subject.get 'split/:string', { :params => [ "token" ], :optional_params => [ "limit" ] } do
762-
params[:string].split(params[:token], (params[:limit] || 0).to_i)
761+
subject.get 'split/:string', { :params => { "token" => "a token" }, :optional_params => { "limit" => "the limit" } } do
762+
params[:string].split(params[:token], (params[:limit] || 0).to_i)
763763
end
764764
end
765765
it "should split a string" do
766-
get "/split/a,b,c.json", :token => ','
767-
last_response.body.should == '["a","b","c"]'
766+
get "/split/a,b,c.json", :token => ','
767+
last_response.body.should == '["a","b","c"]'
768768
end
769769
it "should split a string with limit" do
770-
get "/split/a,b,c.json", :token => ',', :limit => '2'
771-
last_response.body.should == '["a","b,c"]'
770+
get "/split/a,b,c.json", :token => ',', :limit => '2'
771+
last_response.body.should == '["a","b,c"]'
772772
end
773773
it "should set route_params" do
774-
subject.routes.size.should == 1
775-
subject.routes[0].route_params.should == [ "string", "token" ]
776-
subject.routes[0].route_optional_params.should == [ "limit" ]
774+
subject.routes.size.should == 1
775+
subject.routes[0].route_params.should == { "string" => "", "token" => "a token" }
776+
subject.routes[0].route_optional_params.should == { "limit" => "the limit" }
777777
end
778778
end
779779
end
780780

781+
context "desc" do
782+
describe "empty api structure" do
783+
it "returns an empty array of routes" do
784+
subject.desc "grape api"
785+
subject.routes.should == []
786+
end
787+
end
788+
describe "single method with a desc" do
789+
before(:each) do
790+
subject.desc "ping method"
791+
subject.get :ping do
792+
'pong'
793+
end
794+
end
795+
it "returns route description" do
796+
subject.routes[0].route_description.should == "ping method"
797+
end
798+
end
799+
describe "single method with a an array of params and a desc hash block" do
800+
before(:each) do
801+
subject.desc "ping method", { :params => { "x" => "y" } }
802+
subject.get "ping/:x" do
803+
'pong'
804+
end
805+
end
806+
it "returns route description" do
807+
subject.routes[0].route_description.should == "ping method"
808+
end
809+
end
810+
describe "api structure with multiple methods and descriptions" do
811+
before(:each) do
812+
class JitterAPI < Grape::API
813+
desc "first method"
814+
get "first" do; end
815+
get "second" do; end
816+
desc "third method"
817+
get "third" do; end
818+
end
819+
end
820+
it "should return a description for the first method" do
821+
JitterAPI::routes[0].route_description.should == "first method"
822+
JitterAPI::routes[1].route_description.should be_nil
823+
JitterAPI::routes[2].route_description.should == "third method"
824+
end
825+
end
826+
describe "api structure with multiple methods, namespaces, descriptions and options" do
827+
before(:each) do
828+
class LitterAPI < Grape::API
829+
desc "first method"
830+
get "first" do; end
831+
get "second" do; end
832+
namespace "ns" do
833+
desc "ns second", :foo => "bar"
834+
get "second" do; end
835+
end
836+
desc "third method", :details => "details of third method"
837+
get "third" do; end
838+
desc "Reverses a string.", { :params =>
839+
{ "s" => { :desc => "string to reverse", :type => "string" }}
840+
}
841+
get "reverse" do
842+
params[:s].reverse
843+
end
844+
end
845+
end
846+
it "should return a description for the first method" do
847+
LitterAPI::routes[0].route_description.should == "first method"
848+
LitterAPI::routes[1].route_description.should be_nil
849+
LitterAPI::routes[2].route_description.should == "ns second"
850+
LitterAPI::routes[2].route_foo.should == "bar"
851+
LitterAPI::routes[3].route_description.should == "third method"
852+
LitterAPI::routes[4].route_description.should == "Reverses a string."
853+
LitterAPI::routes[4].route_params.should == { "s" => { :desc => "string to reverse", :type => "string" }}
854+
end
855+
end
856+
end
857+
781858
describe ".rescue_from klass, block" do
782859
it 'should rescue Exception' do
783860
subject.rescue_from RuntimeError do |e|
@@ -850,12 +927,12 @@ class CommunicationError < RuntimeError; end
850927

851928
describe '.mount' do
852929
let(:mounted_app){ lambda{|env| [200, {}, ["MOUNTED"]]} }
853-
930+
854931
context 'with a bare rack app' do
855932
before do
856933
subject.mount mounted_app => '/mounty'
857934
end
858-
935+
859936
it 'should make a bare Rack app available at the endpoint' do
860937
get '/mounty'
861938
last_response.body.should == 'MOUNTED'
@@ -867,7 +944,7 @@ class CommunicationError < RuntimeError; end
867944
end
868945

869946
it 'should be able to cascade' do
870-
subject.mount lambda{ |env|
947+
subject.mount lambda{ |env|
871948
headers = {}
872949
headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo')
873950
[200, headers, ["Farfegnugen"]]

0 commit comments

Comments
 (0)