Skip to content

Commit 4819b59

Browse files
committed
Initial cut for structure layout.
1 parent 316d863 commit 4819b59

File tree

3 files changed

+167
-3
lines changed

3 files changed

+167
-3
lines changed

README.markdown

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,39 @@ you simply use the `rescue_from` method inside your API declaration:
8383
rescue_from ArgumentError, NotImplementedError # :all for all errors
8484
end
8585

86+
## API Structure
87+
88+
Grape exposes the API structure as a hash. The hash keys are API versions (`:default` when version is omitted). When namespaces are present,
89+
each value is a hash of namespaces with paths as values. When namespaces are not present, each value is an array of paths. Each path is a hash
90+
containing two keys, `:method` which holds a string containing the path's HTTP request type, and `:path` which holds a string representing the
91+
path. The structure is retrieved via the `structure` method.
92+
93+
class TwitterAPI < Grape::API
94+
95+
version 'v1'
96+
get "version" do
97+
api.version
98+
end
99+
100+
version 'v2'
101+
namespace "ns" do
102+
get "version" do
103+
api.version
104+
end
105+
end
106+
107+
end
108+
109+
Yields the following `TwitterAPI::structure`.
110+
111+
{
112+
"v1" => [
113+
{ :method=>"GET", :path=>"version(.:format)"} ],
114+
"v2"=> {
115+
"ns"=>[ { :method=>"GET", :path=>"version(.:format)" }]
116+
}
117+
}
118+
86119
## Note on Patches/Pull Requests
87120

88121
* Fork the project.

lib/grape/api.rb

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Grape
1010
class API
1111
class << self
1212
attr_reader :route_set
13+
attr_reader :structure
1314

1415
def logger
1516
@logger ||= Logger.new($STDOUT)
@@ -159,7 +160,7 @@ def http_basic(options = {}, &block)
159160

160161
def http_digest(options = {}, &block)
161162
options[:realm] ||= "API Authorization"
162-
options[:opaque] ||= "secret"
163+
options[:opaque] ||= "secret"
163164
auth :http_digest, options, &block
164165
end
165166

@@ -192,6 +193,39 @@ def route(methods, paths, &block)
192193
)
193194
end
194195
end
196+
197+
# create API structure
198+
(version || [ :default ]).each { |v|
199+
200+
ms = []
201+
methods.each { |m|
202+
paths.each { |p|
203+
ms << {
204+
:method => m,
205+
:path => p.to_s
206+
}
207+
}
208+
}
209+
210+
if namespace == '/'
211+
if structure[v].is_a?(Hash)
212+
structure[v][:default] ||= (structure[v][:default] || {}).merge(structure[v])
213+
if (structure[v][:default].is_a?(Hash))
214+
structure[v][:default] = [ structure[v][:default], ms ]
215+
else
216+
structure[v][:default] |= ms
217+
end
218+
else
219+
structure[v] ||= []
220+
structure[v] |= ms
221+
end
222+
else
223+
structure[v] = { :default => structure[v] } if structure[v].is_a?(Array)
224+
structure[v] ||= {}
225+
structure[v][namespace[1..namespace.length - 1].to_s] = ms
226+
end
227+
}
228+
195229
end
196230

197231
def get(*paths, &block); route('GET', paths, &block) end
@@ -238,6 +272,12 @@ def middleware
238272
settings_stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a}
239273
end
240274

275+
# API structure contains a hash of API versions to API methods.
276+
# If the API is not versioned, the hash contains a single :current key.
277+
def structure
278+
@structure ||= {}
279+
end
280+
241281
protected
242282

243283
# Execute first the provided block, then each of the
@@ -264,7 +304,7 @@ def build_endpoint(&block)
264304
:format => settings[:error_format] || :txt,
265305
:rescue_options => settings[:rescue_options]
266306
b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
267-
b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
307+
b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
268308
b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
269309
b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version
270310
b.use Grape::Middleware::Formatter, :default_format => default_format || :json
@@ -284,7 +324,7 @@ def inherited(subclass)
284324
def route_set
285325
@route_set ||= Rack::Mount::RouteSet.new
286326
end
287-
327+
288328
def compile_path(path)
289329
parts = []
290330
parts << prefix if prefix

spec/grape/api_spec.rb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,4 +617,95 @@ def two
617617
last_response.status.should eql 403
618618
end
619619
end
620+
621+
describe "simple version structure" do
622+
before(:each) do
623+
subject.get :hello do
624+
"No version"
625+
end
626+
end
627+
it "versions api" do
628+
get '/hello'
629+
last_response.body.should eql "No version"
630+
end
631+
it "returns an array of versions" do
632+
subject.structure.should ==
633+
{
634+
:default => [ { :method => "GET", :path => "hello" } ],
635+
}
636+
end
637+
end
638+
639+
describe "twitter versions" do
640+
class TwitterAPI < Grape::API
641+
version 'v1'
642+
get "version" do
643+
api.version
644+
end
645+
version 'v2'
646+
namespace "ns" do
647+
get "version" do
648+
api.version
649+
end
650+
end
651+
end
652+
it "should return a single structure with both versions" do
653+
TwitterAPI::structure.should == {
654+
"v1"=>[{:method=>"GET", :path=>"version(.:format)"}],
655+
"v2"=>{
656+
"ns"=>[{:method=>"GET", :path=>"version(.:format)"}]
657+
}}
658+
end
659+
end
660+
661+
describe "nested versions" do
662+
before(:each) do
663+
subject.get :hello do
664+
"No version"
665+
end
666+
subject.version 'v1'
667+
subject.get :hello do
668+
"Version: #{request.env['api.version']}"
669+
end
670+
subject.version 'v2'
671+
subject.get :hello do
672+
"Version: #{request.env['api.version']} w/o namespace"
673+
end
674+
subject.namespace :ns1 do
675+
get :hello do
676+
"Version: #{request.env['api.version']}"
677+
end
678+
end
679+
subject.namespace :ns2 do
680+
get :hello do
681+
"Version: #{request.env['api.version']}"
682+
end
683+
end
684+
subject.post :goodbye do
685+
"Version: #{request.env['api.version']} w/o namespace"
686+
end
687+
end
688+
it "versions api" do
689+
get '/hello'
690+
last_response.body.should eql "No version"
691+
get '/v1/hello'
692+
last_response.body.should eql "Version: v1"
693+
get '/v2/ns1/hello'
694+
last_response.body.should eql "Version: v2"
695+
get '/v2/ns2/hello'
696+
last_response.body.should eql "Version: v2"
697+
end
698+
it "returns an array of versions" do
699+
subject.structure.should ==
700+
{
701+
:default => [ { :method => "GET", :path => "hello" } ],
702+
"v1" => [ { :method => "GET", :path => "hello" } ],
703+
"v2" => {
704+
:default => [ { :method => "GET", :path => "hello" }, { :method => "POST", :path => "goodbye" } ],
705+
"ns1" => [ { :method => "GET", :path => "hello" } ],
706+
"ns2" => [ { :method => "GET", :path => "hello" } ]
707+
}
708+
}
709+
end
710+
end
620711
end

0 commit comments

Comments
 (0)