-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Allow multiple mounts of the same API class #570
Comments
Quick and dirty fix is to load and evaluate same code in the different places eval(IO.read(Rails.root.join('app', 'api', 'v1', 'mixins', 'assets.rb'))) I'm new to ruby, so maybe my solution is ugly... best i could think of... |
I'd like to be able to allow multiple |
That would be nice! |
We added a small bounty for this issue on bountysource. |
How would this be different than Modules? Can you provide a working example? |
The Modules section describes how you can mount multiple APIs in the same/different paths. The problem is that if you mount the same API (e.g. Twitter::APIv2) inside different APIs (e.g Twitter::API and AllAPIs::API) the paths etc. of the first time it is mounted will be overwritten which will result in the API being mounted twice at the same location. |
@hobofan I think you could write a failing spec for this. Would be greatly appreciated. |
The issue arises within My workaround would be: module APILogic
def self.included(base)
base.instance_eval do
desc "Return a personal timeline."
get :home_timeline do
authenticate!
current_user.statuses.limit(20)
end
end
end
end
class API1 < Grape::API
include APILogic
end
class API2 < Grape::API
include APILogic
end
class Twitter::API < Grape::API
mount API1 => '/mounted'
mount API2 => '/double'
end
|
The behavior achieved by @dspaeth-faber's example is the expected behavior, and should be default. I can't see an easy fix for this, but two possible fixes could be:
|
@lasseebert cloning is not trivial. What count's in the end and what all the issues are about are the Endpoints. When you clone the API-Class you have to clone the Endpoints too. The Endpoints carry the routing informations. But when you clone the Endpoints and you change the original class after warts and add some routes then the cloned class will not have all the routes the original one has.
grape needs at the moment the routing information within the Endpoint to generate a good documentation and uses it directly so that Endoints can register them selfs within a But like you recognized major refactoring and major breaking change. |
I have made a neat superclass if this is something used a lot in a project: class MountableAPI
def self.anonymous_class
Class.new(Grape::API).tap do |klass|
klass.instance_eval(&@proc)
end
end
def self.mounted(&block)
@proc = block
end
end And use it like this: class FooAPI < MountableAPI
mounted do
get '/foo' do
# ...
end
end
end And mount it like this: # Inside another Grape app
mount FooAPI.anonymous_class => '/' |
Yeah, and with this you bring up a painful subject. grape uses Classes where it should use objects. mount Grape::API.new => '/' |
Agreed! 👍 |
Intreasting!I want to make a PR,but it looks not easy for me. |
Just encountered the same issue, made a spec to show the issue (can be used in a PR): require 'spec_helper'
describe Grape::API do
subject do
class Stats < Grape::API
get :stats do
"stats"
end
end
class Main < Grape::API
resource :one do
mount Stats
end
resource :two do
mount Stats
end
end
Main
end
def app
subject
end
it 'can mount api in multiple resources' do
get '/one/stats'
puts one: last_response.body # => Not Found
get '/two/stats'
puts two: last_response.body # => stats
get '/one/stats'
expect(last_response.body).to eq('stats')
get '/two/stats'
expect(last_response.body).to eq('stats')
end
end |
@lasseebert's workaround works: require 'spec_helper'
class MountableAPI
def self.anonymous_class
Class.new(Grape::API).tap do |klass|
klass.instance_eval(&@proc)
end
end
def self.mounted(&block)
@proc = block
end
end
describe Grape::API do
subject do
class Stats < MountableAPI
mounted do
get :stats do
"stats"
end
end
end
class Main < Grape::API
resource :one do
mount Stats.anonymous_class
end
resource :two do
mount Stats.anonymous_class
end
end
Main
end
def app
subject
end
it 'can mount api in multiple resources' do
get '/one/stats'
puts one: last_response.body
get '/two/stats'
puts two: last_response.body
get '/one/stats'
expect(last_response.body).to eq('stats')
get '/two/stats'
expect(last_response.body).to eq('stats')
end
end |
@elado This hack is interesting, do you think this is something we could roll into Grape in some way? Maybe we could be mounting an anonymous class that inherits from the API class instead of an eval-ed one? |
Another tip for anyone interested: The way I use the I have handled it by just setting an instance variable on the class and fetch it from a helper method: class MountableAPI
def self.anonymous_class(options = {})
Class.new(BaseAPI).tap do |klass|
klass.instance_eval(&@proc)
klass.instance_variable_set(:@mount_options, options.dup)
klass.helpers do
def mount_options
options[:for].instance_variable_get(:@mount_options)
end
end
end
end
def self.mounted(&block)
@proc = block
end
end Usage: class FooAPI < MountableAPI
mounted do
get '/foo' do
something = mount_options[:something]
...
end
end
end
# Inside a Grape app:
mount FooAPI.anonymous_class(something: "Hello world!")
# Inside another Grape app or in another namespace:
mount FooAPI.anonymous_class(something: "Waynes world!") |
@dblock I'd love to see it in Grape. Yes, it'd be better to have a global solution that doesn't need eval and "just works" with |
I am not sure @elado, if you want to try a PR we can discuss it over that. |
So, I spent a couple nights looking into this and found that (with much hackery) it is possible to have the But because the act of mounting an API requires modifying its class-level configuration, I don't see a way to preserve both behaviors. If we wanted to allow an API to be modified (e.g. by mounting another API inside of it) after it's mounted, it'd be hard to make it also be able to be mounted twice (a single API class can't maintain two sets of configuration, and a copied class would need some way to 'refresh' its configuration from the original class). Personally, I think it makes sense to expect mounting to be nested inside-out (define your smallest units, mount them to create a bigger API). But that doesn't match existing behavior. To allow both, a re-architecture of how endpoints are defined and compiled into routes might be a better idea... though a significant amount of work. |
Thanks @rnubel. I feel like we should be mounting instances of API classes, not static versions of those classes. Does this help in terms of direction? |
I've just met this problem. anyone still working on this ? |
@salimane I doubt it. |
Ran into this issue as well while working on GitLab, they have quite an extensive API and I saw two workarounds for this so far: |
the only way i found to allow for the same API path to be mounted under different header versions was |
I know this is a pretty old ticket that's been mostly dead for a while, but I'm wondering if there's any progress here or anyone knows how to make this work? I tried @lasseebert's |
@dvandersluis not sure if you found a solution but I'll add mine since I came across this ticket. I just used a "meta-programming" type approach module API
module Client
class Comments < Grape::API
[Post, Profile, Story].each do |model|
namespace model.table_name do
route_param :ref, type: String do
namespace :comments do
desc "List comments for #{model.name.downcase}"
params do
optional :per_page, type: Integer, default: 10
optional :page, type: Integer, default: 1
end
get do
commentable = find(model, ref: params[:ref])
present :comments, paginate(commentable.comments)
end
desc "Create comment in #{model.name.downcase}"
params do
requires :body, type: String
end
post do
commentable = find(model, ref: params[:ref])
comment = commentable.comments.create!(author_id: params[:_user_id], body: params[:body])
MentionProcessingWorker.perform_async(comment.id, comment.base_class_name, :body)
present :comment, comment
end
end
end
end
end
namespace :comments do
route_param :id, type: Integer do
desc 'Edit comment'
params do
requires :body, type: String
end
put do
comment = find(Comment, id: params[:id])
forbidden! unless comment.author_id == params[:_user_id]
comment.update!(body: params[:body], edited: true)
MentionProcessingWorker.perform_async(comment.id, comment.base_class_name, :body)
present :comment, comment
end
end
end
end
end
end This creates the following routes when mounted
|
I played around with a bunch of different metaprogramming but never got a solution that worked 100%. I really hope grape could support this properly and encourage code reuse. |
This approach worked for me, if anyone is still looking how to do it.
And I get
|
So.. I'm very confused as to why this is still open.. From reading it you are simply wanting to re-use a set API endpoint beneath multiple other api endpoints which is supported in Grape already. I've been building new APIs recently and we did this to handle re-using endpoints (and so I can have each model in their endpoint class).
class Resources::Organizations < Grape::API
resource :organizations do
# get all orgs
get do
end
params do
requires :organization_id, type: Integer
end
resource ':organization_id' do
# get a single org
get do
end
mount Resources::Projects, with: {nested: :organization}
mount Resources::Tasks, with: {nested: :organization}
end
end
end
class Resources::Projects < Grape::API
resource :projects do
nested = configuration[:nested]
if nested
# get list of projects
get do
end
end
if nested.nil?
# shallow routes
params do
requires :project_id, type: Integer
end
resource ':project_id' do
# get a single project
get do
end
mount Resources::Tasks, with: {nested: :project}
end
end
end
end
class Resources::Tasks < Grape::API
resource :tasks do
nested = configuration[:nested]
if nested
# get list of tasks
get do
end
end
if nested.nil?
# shallow routes
params do
requires :task_id, type: Integer
end
resource ':task_id' do
# get a single task
get do
end
end
end
end
end Inside the task or project nested endpoints we can do this to determine what param we should fetch and how we should filter. if options[:for].configuration[:nested] == :organization
# filter tasks by org (params[:organization_id)
elsif options[:for].configuration[:nested] == :project
# filter tasks by project (params[:project_id)
else
raise ArgumentError, 'Endpoint mounted incorrectly'
end |
So i have Assets and have many other models that has many Assets.
How do i expose this in API using grape?
For now i'm duplicating Assest class that is exposing Assets resource... But it's not DRY...
I have many relations... and my current project starts to look like copy-paste shit.
I want to have one class that exposes Assets resource no matter on wich level it is (as top level resource or nested). But when i mount that class twice, only last mounted resource works.
Thank you.
The text was updated successfully, but these errors were encountered: