Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instead of manually redefine the route for the action, retrieve it from the routes of the application. #187

Merged
merged 22 commits into from
Dec 18, 2014
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
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]
resources :twitter_example do
collection do
Expand Down