Skip to content

Commit f263d54

Browse files
committed
Merge pull request ruby-grape#53 from dblock/api-structure-layout
Exposing the set of rack routes built by Grape
2 parents 587065f + 8f15c32 commit f263d54

File tree

5 files changed

+189
-20
lines changed

5 files changed

+189
-20
lines changed

README.markdown

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,47 @@ A simple RSpec API test makes a `get` request and parses the response.
133133
end
134134
end
135135

136+
## Inspecting an API
137+
138+
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`.
139+
140+
class TwitterAPI < Grape::API
141+
142+
version 'v1'
143+
get "version" do
144+
api.version
145+
end
146+
147+
version 'v2'
148+
namespace "ns" do
149+
get "version" do
150+
api.version
151+
end
152+
end
153+
154+
end
155+
156+
TwitterAPI::versions # yields [ 'v1', 'v2' ]
157+
TwitterAPI::routes # yields an array of Grape::Route objects
158+
TwitterAPI::routes[0].route_version # yields 'v1'
159+
160+
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.
161+
162+
class StringAPI < Grape::API
163+
get "split/:string", { :params => [ "token" ], :optional_params => [ "limit" ] } do
164+
params[:string].split(params[:token], (params[:limit] || 0))
165+
end
166+
end
167+
168+
StringAPI::routes[0].route_params # yields an array [ "string", "token" ]
169+
StringAPI::routes[0].route_optional_params # yields an array [ "limit" ]
170+
136171
## Note on Patches/Pull Requests
137172

138173
* Fork the project.
139174
* Make your feature addition or bug fix.
140175
* Add tests for it. This is important so I don't break it in a future version unintentionally.
141-
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
176+
* Commit, do not mess with Rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
142177
* Send me a pull request. Bonus points for topic branches.
143178

144179
## Copyright

lib/grape.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Grape
66
autoload :Endpoint, 'grape/endpoint'
77
autoload :MiddlewareStack, 'grape/middleware_stack'
88
autoload :Client, 'grape/client'
9+
autoload :Route, 'grape/route'
910

1011
module Middleware
1112
autoload :Base, 'grape/middleware/base'

lib/grape/api.rb

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ module Grape
1010
class API
1111
class << self
1212
attr_reader :route_set
13+
attr_reader :versions
14+
attr_reader :routes
1315

1416
def logger
1517
@logger ||= Logger.new($STDOUT)
@@ -70,7 +72,12 @@ def prefix(prefix = nil)
7072
# end
7173
#
7274
def version(*new_versions, &block)
73-
new_versions.any? ? nest(block){ set(:version, new_versions) } : settings[:version]
75+
if new_versions.any?
76+
@versions = versions | new_versions
77+
nest(block) { set(:version, new_versions) }
78+
else
79+
settings[:version]
80+
end
7481
end
7582

7683
# Specify the default format for the API's
@@ -181,30 +188,49 @@ def http_digest(options = {}, &block)
181188
# {:hello => 'world'}
182189
# end
183190
# end
184-
def route(methods, paths, &block)
191+
def route(methods, paths = ['/'], route_options = {}, &block)
185192
methods = Array(methods)
186-
paths = ['/'] if paths == []
193+
194+
paths = ['/'] if ! paths || paths == []
187195
paths = Array(paths)
188-
endpoint = build_endpoint(&block)
189-
options = {}
190-
options[:version] = /#{version.join('|')}/ if version
196+
197+
endpoint = build_endpoint(&block)
198+
199+
endpoint_options = {}
200+
endpoint_options[:version] = /#{version.join('|')}/ if version
201+
202+
route_options ||= {}
191203

192204
methods.each do |method|
193205
paths.each do |path|
194-
path = Rack::Mount::Strexp.compile(compile_path(path), options, %w( / . ? ), true)
206+
207+
compiled_path = compile_path(path)
208+
path = Rack::Mount::Strexp.compile(compiled_path, endpoint_options, %w( / . ? ), true)
209+
path_params = path.named_captures.map { |nc| nc[0] } - [ 'version', 'format' ]
210+
path_params |= (route_options[:params] || [])
211+
request_method = (method.to_s.upcase unless method == :any)
212+
213+
routes << Route.new(route_options.merge({
214+
:prefix => prefix,
215+
:version => version ? version.join('|') : nil,
216+
:namespace => namespace,
217+
:method => request_method,
218+
:path => compiled_path,
219+
:params => path_params}))
220+
195221
route_set.add_route(endpoint,
196222
:path_info => path,
197-
:request_method => (method.to_s.upcase unless method == :any)
223+
:request_method => request_method
198224
)
199225
end
200226
end
201227
end
202228

203-
def get(*paths, &block); route('GET', paths, &block) end
204-
def post(*paths, &block); route('POST', paths, &block) end
205-
def put(*paths, &block); route('PUT', paths, &block) end
206-
def head(*paths, &block); route('HEAD', paths, &block) end
207-
def delete(*paths, &block); route('DELETE', paths, &block) end
229+
def get(paths = ['/'], options = {}, &block); route('GET', paths, options, &block) end
230+
def post(paths = ['/'], options = {}, &block); route('POST', paths, options, &block) end
231+
def put(paths = ['/'], options = {}, &block); route('PUT', paths, options, &block) end
232+
def head(paths = ['/'], options = {}, &block); route('HEAD', paths, options, &block) end
233+
def delete(paths = ['/'], options = {}, &block); route('DELETE', paths, options, &block) end
208234

209235
def namespace(space = nil, &block)
210236
if space || block_given?
@@ -244,6 +270,15 @@ def middleware
244270
settings_stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a}
245271
end
246272

273+
# An array of API routes.
274+
def routes
275+
@routes ||= []
276+
end
277+
278+
def versions
279+
@versions ||= []
280+
end
281+
247282
protected
248283

249284
# Execute first the provided block, then each of the
@@ -291,7 +326,7 @@ def inherited(subclass)
291326
def route_set
292327
@route_set ||= Rack::Mount::RouteSet.new
293328
end
294-
329+
295330
def compile_path(path)
296331
parts = []
297332
parts << prefix if prefix

lib/grape/route.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module Grape
2+
3+
# A compiled route for inspection.
4+
class Route
5+
6+
def initialize(options = {})
7+
@options = options || {}
8+
end
9+
10+
def method_missing(method_id, *arguments)
11+
if match = /route_(?<name>[_a-zA-Z]\w*)/.match(method_id.to_s)
12+
@options[match['name'].to_sym]
13+
else
14+
super
15+
end
16+
end
17+
18+
def to_s
19+
"#{route_method} #{route_path}"
20+
end
21+
22+
end
23+
end

spec/grape/api_spec.rb

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def app; subject end
171171
end
172172

173173
it 'should allow for multiple paths' do
174-
subject.get("/abc", "/def") do
174+
subject.get(["/abc", "/def"]) do
175175
"foo"
176176
end
177177

@@ -223,19 +223,16 @@ def app; subject end
223223

224224
it 'should allow for multipart paths' do
225225

226-
227226
subject.route([:get, :post], '/:id/first') do
228227
"first"
229228
end
230-
231229

232230
subject.route([:get, :post], '/:id') do
233231
"ola"
234232
end
235233
subject.route([:get, :post], '/:id/first/second') do
236234
"second"
237235
end
238-
239236

240237
get '/1'
241238
last_response.body.should eql 'ola'
@@ -617,6 +614,85 @@ def two
617614
last_response.status.should eql 403
618615
end
619616
end
617+
618+
context "routes" do
619+
describe "empty api structure" do
620+
it "returns an empty array of routes" do
621+
subject.routes.should == []
622+
end
623+
end
624+
describe "single method api structure" do
625+
before(:each) do
626+
subject.get :ping do
627+
'pong'
628+
end
629+
end
630+
it "returns one route" do
631+
subject.routes.size.should == 1
632+
route = subject.routes[0]
633+
route.route_version.should be_nil
634+
route.route_path.should == "/ping(.:format)"
635+
route.route_method.should == "GET"
636+
end
637+
end
638+
describe "api structure with two versions and a namespace" do
639+
class TwitterAPI < Grape::API
640+
# version v1
641+
version 'v1'
642+
get "version" do
643+
api.version
644+
end
645+
# version v2
646+
version 'v2'
647+
prefix 'p'
648+
namespace "n1" do
649+
namespace "n2" do
650+
get "version" do
651+
api.version
652+
end
653+
end
654+
end
655+
end
656+
it "should return versions" do
657+
TwitterAPI::versions.should == [ 'v1', 'v2' ]
658+
end
659+
it "should set route paths" do
660+
TwitterAPI::routes.size.should == 2
661+
TwitterAPI::routes[0].route_path.should == "/:version/version(.:format)"
662+
TwitterAPI::routes[1].route_path.should == "/p/:version/n1/n2/version(.:format)"
663+
end
664+
it "should set route versions" do
665+
TwitterAPI::routes[0].route_version.should == 'v1'
666+
TwitterAPI::routes[1].route_version.should == 'v2'
667+
end
668+
it "should set a nested namespace" do
669+
TwitterAPI::routes[1].route_namespace.should == "/n1/n2"
670+
end
671+
it "should set prefix" do
672+
TwitterAPI::routes[1].route_prefix.should == 'p'
673+
end
674+
end
675+
describe "api structure with additional parameters" do
676+
before(:each) do
677+
subject.get 'split/:string', { :params => [ "token" ], :optional_params => [ "limit" ] } do
678+
params[:string].split(params[:token], (params[:limit] || 0).to_i)
679+
end
680+
end
681+
it "should split a string" do
682+
get "/split/a,b,c", :token => ','
683+
last_response.body.should == '["a", "b", "c"]'
684+
end
685+
it "should split a string with limit" do
686+
get "/split/a,b,c", :token => ',', :limit => '2'
687+
last_response.body.should == '["a", "b,c"]'
688+
end
689+
it "should set route_params" do
690+
subject.routes.size.should == 1
691+
subject.routes[0].route_params.should == [ "string", "token" ]
692+
subject.routes[0].route_optional_params.should == [ "limit" ]
693+
end
694+
end
695+
end
620696

621697
describe ".rescue_from klass, block" do
622698
it 'should rescue Exception' do
@@ -666,5 +742,4 @@ class CommunicationError < RuntimeError; end
666742
end
667743
end
668744

669-
670745
end

0 commit comments

Comments
 (0)