From 4819b5994c63c47fb1229d653c89417b9246c228 Mon Sep 17 00:00:00 2001 From: dblock Date: Thu, 28 Jul 2011 17:57:10 -0400 Subject: [PATCH 1/4] Initial cut for structure layout. --- README.markdown | 33 +++++++++++++++ lib/grape/api.rb | 46 +++++++++++++++++++-- spec/grape/api_spec.rb | 91 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index 4fdbe412f..3c457ad05 100644 --- a/README.markdown +++ b/README.markdown @@ -83,6 +83,39 @@ you simply use the `rescue_from` method inside your API declaration: rescue_from ArgumentError, NotImplementedError # :all for all errors end +## API Structure + +Grape exposes the API structure as a hash. The hash keys are API versions (`:default` when version is omitted). When namespaces are present, +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 +containing two keys, `:method` which holds a string containing the path's HTTP request type, and `:path` which holds a string representing the +path. The structure is retrieved via the `structure` method. + + 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 + +Yields the following `TwitterAPI::structure`. + + { + "v1" => [ + { :method=>"GET", :path=>"version(.:format)"} ], + "v2"=> { + "ns"=>[ { :method=>"GET", :path=>"version(.:format)" }] + } + } + ## Note on Patches/Pull Requests * Fork the project. diff --git a/lib/grape/api.rb b/lib/grape/api.rb index bbe1d3528..9d80d520d 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -10,6 +10,7 @@ module Grape class API class << self attr_reader :route_set + attr_reader :structure def logger @logger ||= Logger.new($STDOUT) @@ -159,7 +160,7 @@ def http_basic(options = {}, &block) def http_digest(options = {}, &block) options[:realm] ||= "API Authorization" - options[:opaque] ||= "secret" + options[:opaque] ||= "secret" auth :http_digest, options, &block end @@ -192,6 +193,39 @@ def route(methods, paths, &block) ) end end + + # create API structure + (version || [ :default ]).each { |v| + + ms = [] + methods.each { |m| + paths.each { |p| + ms << { + :method => m, + :path => p.to_s + } + } + } + + if namespace == '/' + if structure[v].is_a?(Hash) + structure[v][:default] ||= (structure[v][:default] || {}).merge(structure[v]) + if (structure[v][:default].is_a?(Hash)) + structure[v][:default] = [ structure[v][:default], ms ] + else + structure[v][:default] |= ms + end + else + structure[v] ||= [] + structure[v] |= ms + end + else + structure[v] = { :default => structure[v] } if structure[v].is_a?(Array) + structure[v] ||= {} + structure[v][namespace[1..namespace.length - 1].to_s] = ms + end + } + end def get(*paths, &block); route('GET', paths, &block) end @@ -238,6 +272,12 @@ def middleware settings_stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a} end + # API structure contains a hash of API versions to API methods. + # If the API is not versioned, the hash contains a single :current key. + def structure + @structure ||= {} + end + protected # Execute first the provided block, then each of the @@ -264,7 +304,7 @@ def build_endpoint(&block) :format => settings[:error_format] || :txt, :rescue_options => settings[:rescue_options] b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic - b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest + b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version b.use Grape::Middleware::Formatter, :default_format => default_format || :json @@ -284,7 +324,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/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index e2e7b6a03..39754bb33 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -617,4 +617,95 @@ def two last_response.status.should eql 403 end end + + describe "simple version structure" do + before(:each) do + subject.get :hello do + "No version" + end + end + it "versions api" do + get '/hello' + last_response.body.should eql "No version" + end + it "returns an array of versions" do + subject.structure.should == + { + :default => [ { :method => "GET", :path => "hello" } ], + } + end + end + + describe "twitter versions" do + 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 + it "should return a single structure with both versions" do + TwitterAPI::structure.should == { + "v1"=>[{:method=>"GET", :path=>"version(.:format)"}], + "v2"=>{ + "ns"=>[{:method=>"GET", :path=>"version(.:format)"}] + }} + end + end + + describe "nested versions" do + before(:each) do + subject.get :hello do + "No version" + end + subject.version 'v1' + subject.get :hello do + "Version: #{request.env['api.version']}" + end + subject.version 'v2' + subject.get :hello do + "Version: #{request.env['api.version']} w/o namespace" + end + subject.namespace :ns1 do + get :hello do + "Version: #{request.env['api.version']}" + end + end + subject.namespace :ns2 do + get :hello do + "Version: #{request.env['api.version']}" + end + end + subject.post :goodbye do + "Version: #{request.env['api.version']} w/o namespace" + end + end + it "versions api" do + get '/hello' + last_response.body.should eql "No version" + get '/v1/hello' + last_response.body.should eql "Version: v1" + get '/v2/ns1/hello' + last_response.body.should eql "Version: v2" + get '/v2/ns2/hello' + last_response.body.should eql "Version: v2" + end + it "returns an array of versions" do + subject.structure.should == + { + :default => [ { :method => "GET", :path => "hello" } ], + "v1" => [ { :method => "GET", :path => "hello" } ], + "v2" => { + :default => [ { :method => "GET", :path => "hello" }, { :method => "POST", :path => "goodbye" } ], + "ns1" => [ { :method => "GET", :path => "hello" } ], + "ns2" => [ { :method => "GET", :path => "hello" } ] + } + } + end + end end From b544913d1ec712e6dec7c18b76f0e87c8f308eed Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 29 Jul 2011 09:20:43 -0400 Subject: [PATCH 2/4] Refactored api structure madness into api route collection. --- README.markdown | 22 ++----- lib/grape.rb | 1 + lib/grape/api.rb | 70 +++++++++-------------- lib/grape/route.rb | 25 ++++++++ spec/grape/api_spec.rb | 127 ++++++++++++++++------------------------- 5 files changed, 107 insertions(+), 138 deletions(-) create mode 100644 lib/grape/route.rb diff --git a/README.markdown b/README.markdown index 3c457ad05..ba33078b0 100644 --- a/README.markdown +++ b/README.markdown @@ -83,12 +83,9 @@ you simply use the `rescue_from` method inside your API declaration: rescue_from ArgumentError, NotImplementedError # :all for all errors end -## API Structure +## Inspecting an API -Grape exposes the API structure as a hash. The hash keys are API versions (`:default` when version is omitted). When namespaces are present, -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 -containing two keys, `:method` which holds a string containing the path's HTTP request type, and `:path` which holds a string representing the -path. The structure is retrieved via the `structure` method. +Grape exposes arrays of API versions and compiled routes. Each route contains a prefix, version, namespace, method and path. class TwitterAPI < Grape::API @@ -96,7 +93,7 @@ path. The structure is retrieved via the `structure` method. get "version" do api.version end - + version 'v2' namespace "ns" do get "version" do @@ -106,22 +103,15 @@ path. The structure is retrieved via the `structure` method. end -Yields the following `TwitterAPI::structure`. - - { - "v1" => [ - { :method=>"GET", :path=>"version(.:format)"} ], - "v2"=> { - "ns"=>[ { :method=>"GET", :path=>"version(.:format)" }] - } - } + TwitterAPI::versions # yields [ 'v1', 'v2' ] + TwitterAPI::routes # yields an array of Grape::Route objects ## 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 9d80d520d..d42cd708f 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -10,7 +10,8 @@ module Grape class API class << self attr_reader :route_set - attr_reader :structure + attr_reader :versions + attr_reader :routes def logger @logger ||= Logger.new($STDOUT) @@ -71,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 @@ -180,52 +186,27 @@ def route(methods, paths, &block) methods = Array(methods) paths = ['/'] if paths == [] paths = Array(paths) - endpoint = build_endpoint(&block) + endpoint = build_endpoint(&block) options = {} + options[:version] = /#{version.join('|')}/ if version 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, options, %w( / . ? ), true) + request_method = (method.to_s.upcase unless method == :any) + routes << Route.new(prefix, + version ? version.join('|') : nil, + namespace, + request_method, + compiled_path) route_set.add_route(endpoint, :path_info => path, - :request_method => (method.to_s.upcase unless method == :any) + :request_method => request_method ) end end - - # create API structure - (version || [ :default ]).each { |v| - - ms = [] - methods.each { |m| - paths.each { |p| - ms << { - :method => m, - :path => p.to_s - } - } - } - - if namespace == '/' - if structure[v].is_a?(Hash) - structure[v][:default] ||= (structure[v][:default] || {}).merge(structure[v]) - if (structure[v][:default].is_a?(Hash)) - structure[v][:default] = [ structure[v][:default], ms ] - else - structure[v][:default] |= ms - end - else - structure[v] ||= [] - structure[v] |= ms - end - else - structure[v] = { :default => structure[v] } if structure[v].is_a?(Array) - structure[v] ||= {} - structure[v][namespace[1..namespace.length - 1].to_s] = ms - end - } - end def get(*paths, &block); route('GET', paths, &block) end @@ -272,10 +253,13 @@ def middleware settings_stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a} end - # API structure contains a hash of API versions to API methods. - # If the API is not versioned, the hash contains a single :current key. - def structure - @structure ||= {} + # An array of API routes. + def routes + @routes ||= [] + end + + def versions + @versions ||= [] end protected @@ -305,7 +289,7 @@ def build_endpoint(&block) :rescue_options => settings[:rescue_options] b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest - b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix + b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version b.use Grape::Middleware::Formatter, :default_format => default_format || :json middleware.each{|m| b.use *m } diff --git a/lib/grape/route.rb b/lib/grape/route.rb new file mode 100644 index 000000000..5b939f04c --- /dev/null +++ b/lib/grape/route.rb @@ -0,0 +1,25 @@ +module Grape + + # A compiled route for inspection. + class Route + + attr_reader :prefix + attr_reader :version + attr_reader :namespace + attr_reader :method + attr_reader :path + + def initialize(prefix, version, namespace, method, path) + @prefix = prefix + @version = version + @namespace = namespace + @method = method + @path = path + end + + def to_s + "#{method} #{path}" + end + + end +end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 39754bb33..b9702ba73 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -618,94 +618,63 @@ def two end end - describe "simple version structure" do - before(:each) do - subject.get :hello do - "No version" + 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 - end - it "versions api" do - get '/hello' - last_response.body.should eql "No version" - end - it "returns an array of versions" do - subject.structure.should == - { - :default => [ { :method => "GET", :path => "hello" } ], - } - end - end - - describe "twitter versions" do - class TwitterAPI < Grape::API - version 'v1' - get "version" do - api.version - end - version 'v2' - namespace "ns" do - get "version" do + it "returns one route" do + subject.routes.size.should == 1 + route = subject.routes[0] + route.version.should be_nil + route.path.should == "/ping(.:format)" + 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 - end - it "should return a single structure with both versions" do - TwitterAPI::structure.should == { - "v1"=>[{:method=>"GET", :path=>"version(.:format)"}], - "v2"=>{ - "ns"=>[{:method=>"GET", :path=>"version(.:format)"}] - }} - end - end - - describe "nested versions" do - before(:each) do - subject.get :hello do - "No version" - end - subject.version 'v1' - subject.get :hello do - "Version: #{request.env['api.version']}" + it "should return versions" do + TwitterAPI::versions.should == [ 'v1', 'v2' ] end - subject.version 'v2' - subject.get :hello do - "Version: #{request.env['api.version']} w/o namespace" + it "should set route paths" do + TwitterAPI::routes.size.should == 2 + TwitterAPI::routes[0].path.should == "/:version/version(.:format)" + TwitterAPI::routes[1].path.should == "/p/:version/n1/n2/version(.:format)" end - subject.namespace :ns1 do - get :hello do - "Version: #{request.env['api.version']}" - end + it "should set route versions" do + TwitterAPI::routes[0].version.should == 'v1' + TwitterAPI::routes[1].version.should == 'v2' end - subject.namespace :ns2 do - get :hello do - "Version: #{request.env['api.version']}" - end + it "should set a nested namespace" do + TwitterAPI::routes[1].namespace.should == "/n1/n2" end - subject.post :goodbye do - "Version: #{request.env['api.version']} w/o namespace" + it "should set prefix" do + TwitterAPI::routes[1].prefix.should == 'p' end end - it "versions api" do - get '/hello' - last_response.body.should eql "No version" - get '/v1/hello' - last_response.body.should eql "Version: v1" - get '/v2/ns1/hello' - last_response.body.should eql "Version: v2" - get '/v2/ns2/hello' - last_response.body.should eql "Version: v2" - end - it "returns an array of versions" do - subject.structure.should == - { - :default => [ { :method => "GET", :path => "hello" } ], - "v1" => [ { :method => "GET", :path => "hello" } ], - "v2" => { - :default => [ { :method => "GET", :path => "hello" }, { :method => "POST", :path => "goodbye" } ], - "ns1" => [ { :method => "GET", :path => "hello" } ], - "ns2" => [ { :method => "GET", :path => "hello" } ] - } - } - end end + end From 51fb5c47c4cfd71e2ebce35c660e5b756ffd3050 Mon Sep 17 00:00:00 2001 From: dblock Date: Tue, 2 Aug 2011 15:53:20 -0400 Subject: [PATCH 3/4] Allowing arbitrary option hashes along with the api declaration. --- README.markdown | 12 ++++++++++++ lib/grape/api.rb | 37 ++++++++++++++++++++++--------------- lib/grape/route.rb | 22 ++++++++++------------ spec/grape/api_spec.rb | 38 +++++++++++++++++++++++++------------- 4 files changed, 69 insertions(+), 40 deletions(-) diff --git a/README.markdown b/README.markdown index ba33078b0..f22a8e813 100644 --- a/README.markdown +++ b/README.markdown @@ -105,6 +105,18 @@ Grape exposes arrays of API versions and compiled routes. Each route contains a 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 options 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 values are accessible via `route_[name]`. + + class StringAPI < Grape::API + get :split, { :params => [ :string, :token ] } do + params[:string].split(params[:token]) + end + end + + StringAPI::routes[0].route_params # yields an array [ :string, :token ] ## Note on Patches/Pull Requests diff --git a/lib/grape/api.rb b/lib/grape/api.rb index d42cd708f..072d442c7 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -182,25 +182,32 @@ 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_options = {} + endpoint_options[:version] = /#{version.join('|')}/ if version methods.each do |method| paths.each do |path| + compiled_path = compile_path(path) - path = Rack::Mount::Strexp.compile(compiled_path, options, %w( / . ? ), true) + path = Rack::Mount::Strexp.compile(compiled_path, endpoint_options, %w( / . ? ), true) request_method = (method.to_s.upcase unless method == :any) - routes << Route.new(prefix, - version ? version.join('|') : nil, - namespace, - request_method, - compiled_path) + + routes << Route.new({ + :prefix => prefix, + :version => version ? version.join('|') : nil, + :namespace => namespace, + :method => request_method, + :path => compiled_path} + .merge(route_options || {})) + route_set.add_route(endpoint, :path_info => path, :request_method => request_method @@ -209,11 +216,11 @@ def route(methods, paths, &block) 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? diff --git a/lib/grape/route.rb b/lib/grape/route.rb index 5b939f04c..07ae31cbb 100644 --- a/lib/grape/route.rb +++ b/lib/grape/route.rb @@ -3,22 +3,20 @@ module Grape # A compiled route for inspection. class Route - attr_reader :prefix - attr_reader :version - attr_reader :namespace - attr_reader :method - attr_reader :path + def initialize(options = {}) + @options = options || {} + end - def initialize(prefix, version, namespace, method, path) - @prefix = prefix - @version = version - @namespace = namespace - @method = method - @path = path + 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 - "#{method} #{path}" + "#{route_method} #{route_path}" end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index b9702ba73..4d2e60403 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' @@ -633,9 +630,9 @@ def two it "returns one route" do subject.routes.size.should == 1 route = subject.routes[0] - route.version.should be_nil - route.path.should == "/ping(.:format)" - route.method.should == "GET" + 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 @@ -661,18 +658,33 @@ class TwitterAPI < Grape::API end it "should set route paths" do TwitterAPI::routes.size.should == 2 - TwitterAPI::routes[0].path.should == "/:version/version(.:format)" - TwitterAPI::routes[1].path.should == "/p/:version/n1/n2/version(.:format)" + 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].version.should == 'v1' - TwitterAPI::routes[1].version.should == 'v2' + 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].namespace.should == "/n1/n2" + TwitterAPI::routes[1].route_namespace.should == "/n1/n2" end it "should set prefix" do - TwitterAPI::routes[1].prefix.should == 'p' + TwitterAPI::routes[1].route_prefix.should == 'p' + end + end + describe "api structure with additional parameters" do + before(:each) do + subject.get :split, { :params => [ :string, :token ] } do + params[:string].split(params[:token]) + end + end + it "should split a string" do + get "/split", :string => "a,b,c", :token => ',' + 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 ] end end end From 933c72483059b589992657591d1849da784d8895 Mon Sep 17 00:00:00 2001 From: dblock Date: Tue, 2 Aug 2011 16:17:12 -0400 Subject: [PATCH 4/4] Storing additional information. --- README.markdown | 14 +++++++------- lib/grape/api.rb | 10 +++++++--- spec/grape/api_spec.rb | 13 +++++++++---- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/README.markdown b/README.markdown index f22a8e813..3109c52e0 100644 --- a/README.markdown +++ b/README.markdown @@ -85,7 +85,7 @@ you simply use the `rescue_from` method inside your API declaration: ## Inspecting an API -Grape exposes arrays of API versions and compiled routes. Each route contains a prefix, version, namespace, method and path. +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 @@ -107,16 +107,16 @@ Grape exposes arrays of API versions and compiled routes. Each route contains a TwitterAPI::routes # yields an array of Grape::Route objects TwitterAPI::routes[0].route_version # yields 'v1' -Grape also supports storing additional options 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 values are accessible via `route_[name]`. +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, { :params => [ :string, :token ] } do - params[:string].split(params[:token]) + 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_params # yields an array [ "string", "token" ] + StringAPI::routes[0].route_optional_params # yields an array [ "limit" ] ## Note on Patches/Pull Requests diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 072d442c7..591e1efd1 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -193,20 +193,24 @@ def route(methods, paths = ['/'], route_options = {}, &block) endpoint_options = {} endpoint_options[:version] = /#{version.join('|')}/ if version + route_options ||= {} + methods.each do |method| paths.each do |path| 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({ + routes << Route.new(route_options.merge({ :prefix => prefix, :version => version ? version.join('|') : nil, :namespace => namespace, :method => request_method, - :path => compiled_path} - .merge(route_options || {})) + :path => compiled_path, + :params => path_params})) route_set.add_route(endpoint, :path_info => path, diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 4d2e60403..3675bf5b2 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -674,17 +674,22 @@ class TwitterAPI < Grape::API end describe "api structure with additional parameters" do before(:each) do - subject.get :split, { :params => [ :string, :token ] } do - params[:string].split(params[:token]) + 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", :string => "a,b,c", :token => ',' + 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_params.should == [ "string", "token" ] + subject.routes[0].route_optional_params.should == [ "limit" ] end end end