Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

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

Merged
merged 22 commits into from

4 participants

@mtparet

Description

Instead of manually redefine the route for the action, retrieve it from the routes of the application.
This enable also to have only place where to maintain the routing.

Example:

Instead of defining the name, HTTP verb, and route of the action.

api :POST, "/users", "Create user"

You could now just define the name of the action.

api! 'Create user'

HTTP verb and route will be retrieved from the routes of the application.

@iNecas
Owner

Hi, You are my hero!

Having multiple routes to one method is the case quite often. What about having also

api_routes desc

that would add all the routes for the method? This way, the devs have option if one route is
enough, all they want all of them. Thoughts?

@iNecas
Owner

Also, maybe we could do this

api "Create user"

Since the method and path were not defined, we would load them from the routes. What about that?

@Pajk
Owner

Great feature, thanks for contribution :+1:. We have been thinking about this since the beginning.

@mtparet

Thank you so much guys!

@iNecas I'm trying to add all the routes for one action, but I'm not sure how should I do, any thoughts ? https://github.com/Pajk/apipie-rails/pull/187/files#diff-3794160d9a3652a5e62136ca6ebc4f91R326

Do you see an inconvenient to add all routes for an action by default ?

@mtparet

@iNecas for now, I removed the work in progress to support multiple routes for one action. I prefer to do it in another PR once this one will be merged (and used by people ?).

@mtparet

Tests failing for Rails 3.0.0, do we really want to support a deprecated Rails version ?

@iNecas
Owner

We can at least try figure out what's won't be working. So we can put into the tests condition on the rails version, something like:

unless Rails::VERSION::STRING < '3.2.0'

end

So we would be able to say at least, that specific features doesn't work with old rails. I have not hard requirement on supporting rails 3.0, but since so far apipie was working on it, some folks might consider it as advantage.

@iNecas
Owner

in ideal case, we would go with deprecation warning with the next release (I would say it's time for 0.1.0 version finally :) and remove the support next version (I don't have issues with release 0.2.0 quite soon, just to drop the support.

@mtparet

So i'll go for Rails::VERSION::STRING < '3.2.0' :)

@iNecas
Owner

I've checked the PR, hit some issues with Sprockets routes (fixed), also looked into possibility of loading all matching routes and using api keyword when not specifying the vert/path.

What do you think about #194 ?

@mtparet

Thanks, I'm checking this now.

@mtparet

#194 looks good for me :+1:

@mtparet

@iNecas what do you think about the state of this feature ? (ie: is it mergeable?)

@iNecas
Owner

Hi, I have maybe a bit more flexible way how to define the customization in loading the routes for the "end user", I hope to get that in code sometimes this week. Sry for delay!

@mtparet

@iNecas sorry to bother you, what are your last thoughts about this PR? I'm willing to help on this side so let me know :)

lib/apipie/application.rb
((22 lines not shown))
+ # 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
+ end
+
+ def routes_for_action(controller, method)
+ routes = apipie_routes.select do |route|
+ controller == route_app_controller(route.app, route) &&
+ method.to_s == route.defaults[:action]
+ end
+
+ routes.map do |route|
@iNecas Owner
iNecas added a note

I wonder if the routes_path_formatter (or routes_formatter) shouldn't be a class/object, that would take the array of routes and return the array of the {path: path, verb: '...'}. The reason is the case, whe you would like for some reason for example change the order of api keywords.

What about the default formater class similar to this:

class RoutesFormater
  def format_paths(paths)
     paths.map { |path| format_path(path) } 
  end

  def format_path(path)
     { path: "", verb: "" }
  end
end

It would allow to:

  1. customize the order of the routes if needed
  2. skip some routes from getting to the documentation
  3. add things like automatic description of the api, if needed

What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@iNecas
Owner

@mtparet I really appreciate your patience. Thanks for reviving this PR. One final thought about the interface for the custom formatter.

@iNecas
Owner

@mtparet I hope I didn't put you off just yet…

@mtparet

@iNecas nope, I was on vacation these last weeks and has to catch up some works this week. I hope to have time next week to finalize this one :)

@mtparet

Just rebased and introduced routes formatter class.
(see comment about one questioning)

lib/apipie/dsl_definition.rb
((15 lines not shown))
return unless Apipie.active_dsl?
- _apipie_dsl_data[:api_args] << [method, path, desc, options]
+ _apipie_dsl_data[:api] = true
+ case args.size
+ when 0..1
@mtparet
mtparet added a note

Since we introduced another argument options, if we want to allow pass it here, we should check size between 0 and 2 but this overlaps with the below check between 2 and 4.
So it means we should use another way to distinguish the two possibility to define routes.

@iNecas Owner
iNecas added a note

I wonder, if api! would not make that difference easier: so that we would not be limited on the number of arguments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/apipie/configuration.rb
@@ -146,6 +152,14 @@ def initialize
@default_locale = 'en'
@locale = lambda { |locale| @default_locale }
@translate = lambda { |str, locale| str }
+ @routes_path_formatter = lambda do |path|
@iNecas Owner
iNecas added a note

I was thinking the routes_path_formattter would be a subclass of the RoutesFormatter: so to customize the paths, one would override the methods as needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/apipie/routes_formatter.rb
@@ -0,0 +1,24 @@
+class RoutesFormater
+ API_METHODS = %w{GET POST PUT PATCH OPTIONS DELETE}
+
+ def self.format_paths(rails_paths)
@iNecas Owner
iNecas added a note

using instance methods would make it easier to create "parametrized formatters", using some parameter at initializer, so that some slight modifications to the behavior would be possible without need to interit from it every time: let's say I would wike to have a slightly different fromatter for devel (showing all routs) and production (omitting some of them). From my experience, I usually refactor these kind of "static" classes later to use the instance methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@iNecas
Owner

It's shaping really nicely, some comments inline

@mtparet

Just did the modification about api! vs api and about the formatter. Perhaps needs few documentation updates.

spec/dummy/config/initializers/apipie.rb
@@ -73,6 +73,18 @@
# config.link_extension = ""
end
+# define formatter
+class RoutesFormater::Path
+ def format(rails_path_spec)
@mtparet
mtparet added a note

Hum, this should be the default formatter, no ? @iNecas
(Enable to get the /users/create_route format)

@iNecas Owner
iNecas added a note

Yop

@mtparet
mtparet added a note

done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mtparet

just rebased against master

lib/apipie/routes_formatter.rb
@@ -0,0 +1,39 @@
+class RoutesFormater
@iNecas Owner
iNecas added a note

Should be namespaced under Apipie module

@iNecas Owner
iNecas added a note

@mtparet no action needed: I will fix that with some other updates I will probably have when testing against my apps

@mtparet
mtparet added a note

Good catch, I forgot to encapsulate it under the module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/apipie/application.rb
@@ -29,6 +29,38 @@ def set_resource_id(controller, resource_id)
@controller_to_resource_id[controller] = resource_id
end
+ def apipie_routes
+ unless @apipie_api_routes
+ # ensure routes are loaded
+ Rails.application.reload_routes! unless Rails.application.routes.routes.any?
+
+ regex = Regexp.new("\\A#{Apipie.configuration.api_base_url.values.join('|')}")
@iNecas Owner
iNecas added a note

What's the reason for filtering the routes based on the api_base_url? Just optimization or there are other reasons? I causes me some troubles when I want to use this to document some engines that add their specific API

@mtparet
mtparet added a note

Indeed the value added is not enough in comparison of problems it may cause.
(I implemented just because I tried to follow current driven behaviors by api_base_url)

@iNecas Owner
iNecas added a note

I will remove that then, we can add that back later in some cusomizable fashion later, if the need occurs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
iNecas added some commits
@iNecas iNecas More customizable RoutesFormatter
Ability to specify the formatter thought the configuration and
influence the other +api+ paramters (such as desc) form the formatter.
48bfe6c
@iNecas iNecas Support for loading routes from engines
Adding a recursion for getting routes form mounted engines.

Also checking if we need to add the api_base_url to the path instead
of removing the api_base_url when loading the path from routes:
doesn't play well with inheritance of api_base_url.
d35e0a1
@iNecas iNecas Distinguish between api from routes and the others 170454d
@iNecas
Owner

@mtparet I've opened a PR into your branch with my changes ifeelgoods#2, feel free to commend on them. After getting those changes in, I'm ok with merging this into master and releasing 0.3.0 next week.

@mtparet

Thanks a lot @iNecas, I'll take a look and merge it in my branch.

@iNecas
Owner

Merging now! Thanks @mtparet for all the work and patience. I expect to release 0.3.0 sometimes next week.

@iNecas iNecas merged commit 4627d69 into from
@mtparet

Nice!

@iNecas
Owner

I'm happy to announce that the new version 0.3.0 was released, including this change. Thanks for making that happen https://github.com/Apipie/apipie-rails/blob/master/CHANGELOG.md#v030

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 8, 2014
  1. @mtparet

    retrive route path and verb from routes.rb

    mtparet authored mtparet committed
  2. @mtparet

    add unit tests

    mtparet authored mtparet committed
  3. @mtparet

    ensure we get all routes

    mtparet authored mtparet committed
  4. @mtparet

    update README

    mtparet authored mtparet committed
  5. @iNecas @mtparet
  6. @mtparet

    fixes for rails 3.0

    mtparet authored mtparet committed
  7. @iNecas @mtparet

    In-depth search for the route controller

    iNecas authored mtparet committed
  8. @iNecas @mtparet

    Fix removing parenthesis

    iNecas authored mtparet committed
  9. @mtparet

    possiblity to define a custom route formatter

    mtparet authored mtparet committed
  10. @mtparet

    default option to empty hash

    mtparet authored
  11. @mtparet

    argument size can't anymore defined which way we are using

    mtparet authored
    (from route or not) so for now, we can't use options when
    using routes.rb definition
  12. @mtparet

    add routes formatter

    mtparet authored
  13. @mtparet
  14. @mtparet

    fix max number agument

    mtparet authored
  15. @mtparet
  16. @mtparet
  17. @mtparet

    Update README.rst

    mtparet authored mtparet committed
  18. @mtparet

    add default formatter

    mtparet authored
Commits on Dec 17, 2014
  1. @iNecas

    More customizable RoutesFormatter

    iNecas authored
    Ability to specify the formatter thought the configuration and
    influence the other +api+ paramters (such as desc) form the formatter.
  2. @iNecas

    Support for loading routes from engines

    iNecas authored
    Adding a recursion for getting routes form mounted engines.
    
    Also checking if we need to add the api_base_url to the path instead
    of removing the api_base_url when loading the path from routes:
    doesn't play well with inheritance of api_base_url.
  3. @iNecas
  4. @mtparet

    Merge pull request #2 from iNecas/ifeelgoods-generate_api_route

    mtparet authored
    Ifeelgoods generate api route
This page is out of date. Refresh to see the latest.
View
54 README.rst
@@ -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.)
@@ -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"}
@@ -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,
@@ -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
============
View
45 lib/apipie/application.rb
@@ -1,4 +1,5 @@
require 'apipie/static_dispatcher'
+require 'apipie/routes_formatter'
require 'yaml'
require 'digest/md5'
require 'json'
@@ -6,7 +7,6 @@
module Apipie
class Application
-
# we need engine just for serving static assets
class Engine < Rails::Engine
initializer "static assets" do |app|
@@ -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)
View
7 lib/apipie/configuration.rb
@@ -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
@@ -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
View
65 lib/apipie/dsl_definition.rb
@@ -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,
@@ -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'
@@ -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
@@ -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
View
8 lib/apipie/method_description.rb
@@ -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
@@ -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
View
33 lib/apipie/routes_formatter.rb
@@ -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
View
14 spec/controllers/users_controller_spec.rb
@@ -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)
@@ -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
View
10 spec/dummy/app/controllers/users_controller.rb
@@ -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
View
1  spec/dummy/config/initializers/apipie.rb
@@ -73,7 +73,6 @@
# config.link_extension = ""
end
-
# integer validator
class Apipie::Validator::IntegerValidator < Apipie::Validator::BaseValidator
View
6 spec/dummy/config/routes.rb
@@ -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
Something went wrong with that request. Please try again.