Skip to content

Commit

Permalink
Merge pull request #187 from ifeelgoods/generate_api_route
Browse files Browse the repository at this point in the history
Instead of manually redefine the route for the action, retrieve it from the routes of the application.
  • Loading branch information
iNecas committed Dec 18, 2014
2 parents 395237b + e494e62 commit 4627d69
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 31 deletions.
54 changes: 53 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ api
You can use this +api+ method more than once for one method. It could
be useful when there are more routes mapped to it.

When providing just one argument (description) or not argument at all,
the paths will be loaded from routes.rb file.

api!
Provide short description and additional option.
The last parameter is methods short description.
The paths will be loaded from routes.rb file. See
`Rails Routes Integration`_ for more details.

api_versions (also api_version)
What version(s) does the action belong to. (See `Versioning`_ for details.)

Expand Down Expand Up @@ -220,6 +229,12 @@ Example:

.. code:: ruby
# The simplest case: just load the paths from routes.rb
api!
def index
end
# More complex example
api :GET, "/users/:id", "Show user profile"
error :code => 401, :desc => "Unauthorized"
error :code => 404, :desc => "Not Found", :meta => {:anything => "you can think of"}
Expand Down Expand Up @@ -528,7 +543,13 @@ api_controllers_matcher
For reloading to work properly you need to specify where your API controllers are. Can be an array if multiple paths are needed

api_routes
Set if your application uses custom API router, different from Rails default
Set if your application uses custom API router, different from Rails
default

routes_formatter
An object providing the translation from the Rails routes to the
format usable in the documentation when using the `api!` keyword. By
default, the ``Apipie::RoutesFormatter`` is used.

markup
You can choose markup language for descriptions of your application,
Expand Down Expand Up @@ -613,6 +634,37 @@ checksum_path
update_checksum
If set to true, the checksum is recalculated with every documentation_reload call

========================
Rails Routes Integration
========================

Apipie is able to load the information about the paths based on the
routes defined in the Rails application, by using the `api!` keyword
in the DSL.

It should be usable out of box, however, one might want
to do some customization (such as omitting some implicit parameters in
the path etc.). For this kind of customizations one can create a new
formatter and pass as the ``Apipie.configuration.routes_formatter``
option, like this:

.. code:: ruby
class MyFormatter < Apipie::RailsFormatter
def format_path(route)
super.gsub(/\(.*?\)/, '').gsub('//','') # hide all implicit parameters
end
end
Apipie.configure do |config|
...
config.routes_formatter = MyFormatter.new
...
end
The similar way can be influenced things like order or a description
of the loaded APIs, even omitting some paths if needed.

============
Processing
============
Expand Down
45 changes: 44 additions & 1 deletion lib/apipie/application.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
require 'apipie/static_dispatcher'
require 'apipie/routes_formatter'
require 'yaml'
require 'digest/md5'
require 'json'

module Apipie

class Application

# we need engine just for serving static assets
class Engine < Rails::Engine
initializer "static assets" do |app|
Expand All @@ -29,6 +29,49 @@ def set_resource_id(controller, resource_id)
@controller_to_resource_id[controller] = resource_id
end

def rails_routes(route_set = nil)
if route_set.nil? && @rails_routes
return @rails_routes
end
route_set ||= Rails.application.routes
# ensure routes are loaded
Rails.application.reload_routes! unless Rails.application.routes.routes.any?

flatten_routes = []

route_set.routes.each do |route|
if route.app.respond_to?(:routes) && route.app.routes.is_a?(ActionDispatch::Routing::RouteSet)
# recursively go though the moutned engines
flatten_routes.concat(rails_routes(route.app.routes))
else
flatten_routes << route
end
end

@rails_routes = flatten_routes
end

# the app might be nested when using contraints, namespaces etc.
# this method does in depth search for the route controller
def route_app_controller(app, route)
if app.respond_to?(:controller)
return app.controller(route.defaults)
elsif app.respond_to?(:app)
return route_app_controller(app.app, route)
end
rescue ActionController::RoutingError
# some errors in the routes will not stop us here: just ignoring
end

def routes_for_action(controller, method, args)
routes = rails_routes.select do |route|
controller == route_app_controller(route.app, route) &&
method.to_s == route.defaults[:action]
end

Apipie.configuration.routes_formatter.format_routes(routes, args)
end

# create new method api description
def define_method_description(controller, method_name, dsl_data)
return if ignored?(controller, method_name)
Expand Down
7 changes: 7 additions & 0 deletions lib/apipie/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class Configuration
# Api::Engine.routes
attr_accessor :api_routes

# a object responsible for transforming the routes loaded from Rails to a form
# to be used in the documentation, when using the `api!` keyword. By default,
# it's Apipie::RoutesFormatter. To customize the behaviour, one can inherit from
# from this class and override the methods as needed.
attr_accessor :routes_formatter

def reload_controllers?
@reload_controllers = Rails.env.development? unless defined? @reload_controllers
return @reload_controllers && @api_controllers_matcher
Expand Down Expand Up @@ -158,6 +164,7 @@ def initialize
@locale = lambda { |locale| @default_locale }
@translate = lambda { |str, locale| str }
@persist_show_in_doc = false
@routes_formatter = RoutesFormatter.new
end
end
end
65 changes: 40 additions & 25 deletions lib/apipie/dsl_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ def _apipie_dsl_data_clear

def _apipie_dsl_data_init
@_apipie_dsl_data = {
:api => false,
:api_args => [],
:api_from_routes => nil,
:errors => [],
:params => [],
:resouce_id => nil,
Expand Down Expand Up @@ -72,16 +74,25 @@ def def_param_group(name, &block)
Apipie.add_param_group(self, name, &block)
end

# Declare an api.
#
# Example:
# api :GET, "/resource_route", "short description",
# # load paths from routes and don't provide description
# api
#
def api(method, path, desc = nil, options={}) #:doc:
return unless Apipie.active_dsl?
_apipie_dsl_data[:api] = true
_apipie_dsl_data[:api_args] << [method, path, desc, options]
end

# # load paths from routes
# api! "short description",
#
def api!(desc = nil, options={}) #:doc:
return unless Apipie.active_dsl?
_apipie_dsl_data[:api] = true
_apipie_dsl_data[:api_from_routes] = { :desc => desc, :options =>options }
end

# Reference other similar method
#
# api :PUT, '/articles/:id'
Expand Down Expand Up @@ -363,22 +374,32 @@ def apipie_concern?
# create method api and redefine newly added method
def method_added(method_name) #:doc:
super
return if !Apipie.active_dsl? || !_apipie_dsl_data[:api]

if ! Apipie.active_dsl? || _apipie_dsl_data[:api_args].blank?
_apipie_dsl_data_clear
return
end
if _apipie_dsl_data[:api_from_routes]
desc = _apipie_dsl_data[:api_from_routes][:desc]
options = _apipie_dsl_data[:api_from_routes][:options]

begin
# remove method description if exists and create new one
Apipie.remove_method_description(self, _apipie_dsl_data[:api_versions], method_name)
description = Apipie.define_method_description(self, method_name, _apipie_dsl_data)
ensure
_apipie_dsl_data_clear
api_from_routes = Apipie.routes_for_action(self, method_name, {:desc => desc, :options => options}).map do |route_info|
[route_info[:verb],
route_info[:path],
route_info[:desc],
(route_info[:options] || {}).merge(:from_routes => true)]
end
_apipie_dsl_data[:api_args].concat(api_from_routes)
end

return if _apipie_dsl_data[:api_args].blank?

# remove method description if exists and create new one
Apipie.remove_method_description(self, _apipie_dsl_data[:api_versions], method_name)
description = Apipie.define_method_description(self, method_name, _apipie_dsl_data)

_apipie_dsl_data_clear
_apipie_define_validators(description)
end # def method_added
ensure
_apipie_dsl_data_clear
end
end

module Concern
Expand Down Expand Up @@ -409,18 +430,12 @@ def apipie_concern?
def method_added(method_name) #:doc:
super

if ! Apipie.active_dsl? || _apipie_dsl_data[:api_args].blank?
_apipie_dsl_data_clear
return
end

begin
_apipie_concern_data << [method_name, _apipie_dsl_data.merge(:from_concern => true)]
ensure
_apipie_dsl_data_clear
end
return if ! Apipie.active_dsl? || !_apipie_dsl_data[:api]

end # def method_added
_apipie_concern_data << [method_name, _apipie_dsl_data.merge(:from_concern => true)]
ensure
_apipie_dsl_data_clear
end

end

Expand Down
8 changes: 6 additions & 2 deletions lib/apipie/method_description.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ class MethodDescription

class Api

attr_accessor :short_description, :path, :http_method, :options
attr_accessor :short_description, :path, :http_method, :from_routes, :options

def initialize(method, path, desc, options)
@http_method = method.to_s
@path = path
@short_description = desc
@from_routes = options[:from_routes]
@options = options
end

Expand Down Expand Up @@ -104,7 +105,10 @@ def doc_url
end

def create_api_url(api)
path = "#{@resource._api_base_url}#{api.path}"
path = api.path
unless api.from_routes
path = "#{@resource._api_base_url}#{path}"
end
path = path[0..-2] if path[-1..-1] == '/'
return path
end
Expand Down
33 changes: 33 additions & 0 deletions lib/apipie/routes_formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Apipie
class RoutesFormatter
API_METHODS = %w{GET POST PUT PATCH OPTIONS DELETE}

# The entry method called by Apipie to extract the array
# representing the api dsl from the routes definition.
def format_routes(rails_routes, args)
rails_routes.map { |rails_route| format_route(rails_route, args) }
end

def format_route(rails_route, args)
{ :path => format_path(rails_route),
:verb => format_verb(rails_route),
:desc => args[:desc],
:options => args[:options] }
end

def format_path(rails_route)
rails_route.path.spec.to_s.gsub('(.:format)', '')
end

def format_verb(rails_route)
verb = API_METHODS.select{|defined_verb| defined_verb =~ /\A#{rails_route.verb}\z/}
if verb.count != 1
verb = API_METHODS.select{|defined_verb| defined_verb == rails_route.constraints[:method]}
if verb.blank?
raise "Unknow verb #{rails_route.path.spec.to_s}"
end
end
verb.first
end
end
end
14 changes: 14 additions & 0 deletions spec/controllers/users_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def compare_hashes(h1, h2)
it "should contain all resource methods" do
methods = subject._methods
methods.keys.should include(:show)
methods.keys.should include(:create_route)
methods.keys.should include(:index)
methods.keys.should include(:create)
methods.keys.should include(:update)
Expand Down Expand Up @@ -382,6 +383,19 @@ def reload_controllers
b.full_description.length.should be > 400
end

context "Usign routes.rb" do
it "should contain basic info about method" do
a = Apipie[UsersController, :create_route]
a.apis.count.should == 1
a.formats.should eq(['json'])
api = a.apis.first
api.short_description.should eq("Create user")
api.path.should eq("/api/users/create_route")
api.from_routes.should be_true
api.http_method.should eq("POST")
end
end

context "contain :see option" do

context "the key is valid" do
Expand Down
10 changes: 10 additions & 0 deletions spec/dummy/app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,14 @@ def see_another
def desc_from_file
render :text => 'document from file action'
end

api! 'Create user'
param_group :user
param :user, Hash do
param :permalink, String
end
param :facts, Hash, :desc => "Additional optional facts about the user", :allow_nil => true
def create_route
end

end
1 change: 0 additions & 1 deletion spec/dummy/config/initializers/apipie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
# config.link_extension = ""
end


# integer validator
class Apipie::Validator::IntegerValidator < Apipie::Validator::BaseValidator

Expand Down
6 changes: 5 additions & 1 deletion spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
scope ENV['RAILS_RELATIVE_URL_ROOT'] || '/' do

scope '/api' do
resources :users
resources :users do
collection do
post :create_route
end
end
resources :concerns, :only => [:index, :show]
namespace :files do
get '/*file_path', to: :download, format: false
Expand Down

0 comments on commit 4627d69

Please sign in to comment.