diff --git a/README.markdown b/README.markdown index e12b1986e..da519ff0d 100644 --- a/README.markdown +++ b/README.markdown @@ -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 diff --git a/lib/grape.rb b/lib/grape.rb index 2a7d82d27..02daf88b9 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -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' diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 5bf2ca495..00d450f39 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -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) @@ -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 @@ -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? @@ -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 @@ -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 diff --git a/lib/grape/route.rb b/lib/grape/route.rb new file mode 100644 index 000000000..07ae31cbb --- /dev/null +++ b/lib/grape/route.rb @@ -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_(?[_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 diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index a15d4c284..07efd1b6c 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -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 @@ -223,11 +223,9 @@ 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" @@ -235,7 +233,6 @@ def app; subject end subject.route([:get, :post], '/:id/first/second') do "second" end - get '/1' last_response.body.should eql 'ola' @@ -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 @@ -666,5 +742,4 @@ class CommunicationError < RuntimeError; end end end - end