Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,47 @@ A simple RSpec API test makes a `get` request and parses the response.
end
end

## Inspecting an API

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`.

class TwitterAPI < Grape::API

version 'v1'
get "version" do
api.version
end

version 'v2'
namespace "ns" do
get "version" do
api.version
end
end

end

TwitterAPI::versions # yields [ 'v1', 'v2' ]
TwitterAPI::routes # yields an array of Grape::Route objects
TwitterAPI::routes[0].route_version # yields 'v1'

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.

class StringAPI < Grape::API
get "split/:string", { :params => [ "token" ], :optional_params => [ "limit" ] } do
params[:string].split(params[:token], (params[:limit] || 0))
end
end

StringAPI::routes[0].route_params # yields an array [ "string", "token" ]
StringAPI::routes[0].route_optional_params # yields an array [ "limit" ]

## Note on Patches/Pull Requests

* Fork the project.
* Make your feature addition or bug fix.
* Add tests for it. This is important so I don't break it in a future version unintentionally.
* 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)
* 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)
* Send me a pull request. Bonus points for topic branches.

## Copyright
Expand Down
1 change: 1 addition & 0 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Grape
autoload :Endpoint, 'grape/endpoint'
autoload :MiddlewareStack, 'grape/middleware_stack'
autoload :Client, 'grape/client'
autoload :Route, 'grape/route'

module Middleware
autoload :Base, 'grape/middleware/base'
Expand Down
63 changes: 49 additions & 14 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module Grape
class API
class << self
attr_reader :route_set
attr_reader :versions
attr_reader :routes

def logger
@logger ||= Logger.new($STDOUT)
Expand Down Expand Up @@ -70,7 +72,12 @@ def prefix(prefix = nil)
# end
#
def version(*new_versions, &block)
new_versions.any? ? nest(block){ set(:version, new_versions) } : settings[:version]
if new_versions.any?
@versions = versions | new_versions
nest(block) { set(:version, new_versions) }
else
settings[:version]
end
end

# Specify the default format for the API's
Expand Down Expand Up @@ -181,30 +188,49 @@ def http_digest(options = {}, &block)
# {:hello => 'world'}
# end
# end
def route(methods, paths, &block)
def route(methods, paths = ['/'], route_options = {}, &block)
methods = Array(methods)
paths = ['/'] if paths == []

paths = ['/'] if ! paths || paths == []
paths = Array(paths)
endpoint = build_endpoint(&block)
options = {}
options[:version] = /#{version.join('|')}/ if version

endpoint = build_endpoint(&block)

endpoint_options = {}
endpoint_options[:version] = /#{version.join('|')}/ if version

route_options ||= {}

methods.each do |method|
paths.each do |path|
path = Rack::Mount::Strexp.compile(compile_path(path), options, %w( / . ? ), true)

compiled_path = compile_path(path)
path = Rack::Mount::Strexp.compile(compiled_path, endpoint_options, %w( / . ? ), true)
path_params = path.named_captures.map { |nc| nc[0] } - [ 'version', 'format' ]
path_params |= (route_options[:params] || [])
request_method = (method.to_s.upcase unless method == :any)

routes << Route.new(route_options.merge({
:prefix => prefix,
:version => version ? version.join('|') : nil,
:namespace => namespace,
:method => request_method,
:path => compiled_path,
:params => path_params}))

route_set.add_route(endpoint,
:path_info => path,
:request_method => (method.to_s.upcase unless method == :any)
:request_method => request_method
)
end
end
end

def get(*paths, &block); route('GET', paths, &block) end
def post(*paths, &block); route('POST', paths, &block) end
def put(*paths, &block); route('PUT', paths, &block) end
def head(*paths, &block); route('HEAD', paths, &block) end
def delete(*paths, &block); route('DELETE', paths, &block) end
def get(paths = ['/'], options = {}, &block); route('GET', paths, options, &block) end
def post(paths = ['/'], options = {}, &block); route('POST', paths, options, &block) end
def put(paths = ['/'], options = {}, &block); route('PUT', paths, options, &block) end
def head(paths = ['/'], options = {}, &block); route('HEAD', paths, options, &block) end
def delete(paths = ['/'], options = {}, &block); route('DELETE', paths, options, &block) end

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

# An array of API routes.
def routes
@routes ||= []
end

def versions
@versions ||= []
end

protected

# Execute first the provided block, then each of the
Expand Down Expand Up @@ -291,7 +326,7 @@ def inherited(subclass)
def route_set
@route_set ||= Rack::Mount::RouteSet.new
end

def compile_path(path)
parts = []
parts << prefix if prefix
Expand Down
23 changes: 23 additions & 0 deletions lib/grape/route.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Grape

# A compiled route for inspection.
class Route

def initialize(options = {})
@options = options || {}
end

def method_missing(method_id, *arguments)
if match = /route_(?<name>[_a-zA-Z]\w*)/.match(method_id.to_s)
@options[match['name'].to_sym]
else
super
end
end

def to_s
"#{route_method} #{route_path}"
end

end
end
85 changes: 80 additions & 5 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def app; subject end
end

it 'should allow for multiple paths' do
subject.get("/abc", "/def") do
subject.get(["/abc", "/def"]) do
"foo"
end

Expand Down Expand Up @@ -223,19 +223,16 @@ def app; subject end

it 'should allow for multipart paths' do


subject.route([:get, :post], '/:id/first') do
"first"
end


subject.route([:get, :post], '/:id') do
"ola"
end
subject.route([:get, :post], '/:id/first/second') do
"second"
end


get '/1'
last_response.body.should eql 'ola'
Expand Down Expand Up @@ -617,6 +614,85 @@ def two
last_response.status.should eql 403
end
end

context "routes" do
describe "empty api structure" do
it "returns an empty array of routes" do
subject.routes.should == []
end
end
describe "single method api structure" do
before(:each) do
subject.get :ping do
'pong'
end
end
it "returns one route" do
subject.routes.size.should == 1
route = subject.routes[0]
route.route_version.should be_nil
route.route_path.should == "/ping(.:format)"
route.route_method.should == "GET"
end
end
describe "api structure with two versions and a namespace" do
class TwitterAPI < Grape::API
# version v1
version 'v1'
get "version" do
api.version
end
# version v2
version 'v2'
prefix 'p'
namespace "n1" do
namespace "n2" do
get "version" do
api.version
end
end
end
end
it "should return versions" do
TwitterAPI::versions.should == [ 'v1', 'v2' ]
end
it "should set route paths" do
TwitterAPI::routes.size.should == 2
TwitterAPI::routes[0].route_path.should == "/:version/version(.:format)"
TwitterAPI::routes[1].route_path.should == "/p/:version/n1/n2/version(.:format)"
end
it "should set route versions" do
TwitterAPI::routes[0].route_version.should == 'v1'
TwitterAPI::routes[1].route_version.should == 'v2'
end
it "should set a nested namespace" do
TwitterAPI::routes[1].route_namespace.should == "/n1/n2"
end
it "should set prefix" do
TwitterAPI::routes[1].route_prefix.should == 'p'
end
end
describe "api structure with additional parameters" do
before(:each) do
subject.get 'split/:string', { :params => [ "token" ], :optional_params => [ "limit" ] } do
params[:string].split(params[:token], (params[:limit] || 0).to_i)
end
end
it "should split a string" do
get "/split/a,b,c", :token => ','
last_response.body.should == '["a", "b", "c"]'
end
it "should split a string with limit" do
get "/split/a,b,c", :token => ',', :limit => '2'
last_response.body.should == '["a", "b,c"]'
end
it "should set route_params" do
subject.routes.size.should == 1
subject.routes[0].route_params.should == [ "string", "token" ]
subject.routes[0].route_optional_params.should == [ "limit" ]
end
end
end

describe ".rescue_from klass, block" do
it 'should rescue Exception' do
Expand Down Expand Up @@ -666,5 +742,4 @@ class CommunicationError < RuntimeError; end
end
end


end