Skip to content

Commit ae879cb

Browse files
committed
Remountable APIs, allows to re-mount APIs that inherit from this
Creates a new module that can replace a Grape::API in most cases which allows APIs to be re-mounted
1 parent b88a8df commit ae879cb

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

lib/grape.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module Grape
2929

3030
eager_autoload do
3131
autoload :API
32+
autoload :RemountableAPI
3233
autoload :Endpoint
3334

3435
autoload :Namespace

lib/grape/dsl/routing.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@ def do_not_route_options!
7777
namespace_inheritable(:do_not_route_options, true)
7878
end
7979

80-
def mount(mounts)
80+
def mount(mounts, with: {})
8181
mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
8282
mounts.each_pair do |app, path|
83+
if app.is_a?(Class) && app.ancestors.include?(Grape::RemountableAPI)
84+
return mount(app.new_instance(configuration: with) => path)
85+
end
8386
in_setting = inheritable_setting
8487

8588
if app.respond_to?(:inheritable_setting, true)

lib/grape/remountable_api.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module Grape
2+
# The RemountableAPI class can replace most API classes, except for the base one that is to be mounted in rack.
3+
# should subclass this class in order to build an API.
4+
class RemountableAPI
5+
class << self
6+
# When inherited, will create a list of all instances (times the API was mounted)
7+
# It will listen to the setup required to mount that endpoint, and replicate it on any new instance
8+
def inherited(remountable_class)
9+
remountable_class.instance_variable_set(:@instances, [])
10+
remountable_class.instance_variable_set(:@setup, [])
11+
12+
base_instance = Class.new(Grape::API)
13+
base_instance.define_singleton_method(:configuration) { {} }
14+
15+
remountable_class.instance_variable_set(:@base_instance, base_instance)
16+
base_instance.constants.each do |constant_name|
17+
remountable_class.const_set(constant_name, base_instance.const_get(constant_name))
18+
end
19+
end
20+
21+
# The remountable class can have a configuration hash to provide some dynamic class-level variables.
22+
# For instance, a descripcion could be done using: `desc configuration[:description]` if it may vary
23+
# depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
24+
# too much, you may actually want to provide a new API rather than remount it.
25+
def new_instance(configuration: {})
26+
instance = Class.new(Grape::API)
27+
instance.instance_variable_set(:@configuration, configuration)
28+
instance.define_singleton_method(:configuration) { @configuration }
29+
replay_setup_on(instance)
30+
@instances << instance
31+
instance
32+
end
33+
34+
# Replays the set up to produce an API as defined in this class, can be called
35+
# on classes that inherit from Grape::API
36+
def replay_setup_on(instance)
37+
@setup.each do |setup_stage|
38+
instance.send(setup_stage[:method], *setup_stage[:args], &setup_stage[:block])
39+
end
40+
end
41+
42+
private
43+
44+
# Adds a new stage to the set up require to get a Grape::API up and running
45+
def add_setup(method, *args, &block)
46+
setup_stage = { method: method, args: args, block: block }
47+
@setup << setup_stage
48+
@base_instance.send(setup_stage[:method], *setup_stage[:args], &setup_stage[:block])
49+
end
50+
51+
def method_missing(method, *args, &block)
52+
if respond_to_missing?(method, true)
53+
add_setup(method, *args, &block)
54+
else
55+
super
56+
end
57+
end
58+
59+
def respond_to_missing?(name, include_private = false)
60+
@base_instance.respond_to?(name, include_private)
61+
end
62+
end
63+
end
64+
end

spec/grape/remountable_api_spec.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
require 'spec_helper'
2+
require 'shared/versioning_examples'
3+
4+
describe Grape::RemountableAPI do
5+
subject(:a_remountable_api) { Class.new(Grape::RemountableAPI) }
6+
let(:root_api) { Class.new(Grape::API) }
7+
8+
def app
9+
root_api
10+
end
11+
12+
describe 'mounted RemountableAPI' do
13+
context 'with a defined route' do
14+
before do
15+
a_remountable_api.get '/votes' do
16+
'10 votes'
17+
end
18+
end
19+
20+
context 'when mounting one instance' do
21+
before do
22+
root_api.mount a_remountable_api
23+
end
24+
25+
it 'can access the endpoint' do
26+
get '/votes'
27+
expect(last_response.body).to eql '10 votes'
28+
end
29+
end
30+
31+
context 'when mounting twice' do
32+
before do
33+
root_api.mount a_remountable_api => '/posts'
34+
root_api.mount a_remountable_api => '/comments'
35+
end
36+
37+
it 'can access the votes in both places' do
38+
get '/posts/votes'
39+
expect(last_response.body).to eql '10 votes'
40+
get '/comments/votes'
41+
expect(last_response.body).to eql '10 votes'
42+
end
43+
end
44+
45+
context 'when mounting on namespace' do
46+
before do
47+
stub_const('StaticRefToAPI', a_remountable_api)
48+
root_api.namespace 'posts' do
49+
mount StaticRefToAPI
50+
end
51+
52+
root_api.namespace 'comments' do
53+
mount StaticRefToAPI
54+
end
55+
end
56+
57+
it 'can access the votes in both places' do
58+
get '/posts/votes'
59+
expect(last_response.body).to eql '10 votes'
60+
get '/comments/votes'
61+
expect(last_response.body).to eql '10 votes'
62+
end
63+
end
64+
end
65+
66+
context 'with a dynamically configured route' do
67+
before do
68+
a_remountable_api.namespace 'api' do
69+
get "/#{configuration[:path]}" do
70+
'10 votes'
71+
end
72+
end
73+
root_api.mount a_remountable_api, with: { path: 'votes' }
74+
root_api.mount a_remountable_api, with: { path: 'scores' }
75+
end
76+
77+
it 'will use the dynamic configuration on all routes' do
78+
get 'api/votes'
79+
expect(last_response.body).to eql '10 votes'
80+
get 'api/scores'
81+
expect(last_response.body).to eql '10 votes'
82+
end
83+
end
84+
end
85+
end

0 commit comments

Comments
 (0)