Skip to content

Instantly share code, notes, and snippets.

@justinko
Last active August 29, 2015 13:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save justinko/8871303 to your computer and use it in GitHub Desktop.
Save justinko/8871303 to your computer and use it in GitHub Desktop.
Inheritance based API versioning in Rails
class ApiVersioning
class Middleware
def initialize(app)
@app = app
end
def call(env)
if version_number = extract_version_number(env)
ApiVersioning.current_version_number = version_number.to_i
Rails.application.eager_load! unless Rails.application.config.cache_classes
ClassDescendantsBuilder.build ApiBeta::ApplicationController,
ApiVersioning.max_version_number
end
@app.call env
end
private
def extract_version_number(env)
env['HTTP_ACCEPT'] && env['HTTP_ACCEPT'][/v([0-9\.]+)\+json/, 1]
end
end
class_attribute :current_version_number
def self.instance
@instance || raise('call `.setup` first')
end
def self.setup(context, &block)
@instance = new(context, &block)
end
def self.min_version_number
instance.min_version_number
end
def self.max_version_number
instance.max_version_number
end
def initialize(context, &block)
@context, @definitions = context, {}
instance_eval &block
end
def version(number, &block)
@definitions[number] = block || Proc.new {}
@context.scope module: "v#{number}", constraints: ->(*) { ApiVersioning.current_version_number == number } do
number.downto(min_version_number) do |i|
@context.instance_eval &@definitions[i]
end
end
end
def min_version_number
@definitions.keys.min
end
def max_version_number
@definitions.keys.max
end
end
module MyApp
class Application < Rails::Application
config.middleware.use 'ApiVersioning::Middleware'
end
end
class ApplicationController
respond_to :json
def self.remove_action(*action_names)
action_names.each do |action_name|
define_method(action_name) { head :not_found }
end
end
end
class ClassDescendantsBuilder
LEVEL_REGEX = /\d+/
def self.build(base_class, level)
base_class.descendants.each do |descendant|
new(descendant, level).build
end
end
def initialize(descendant, level)
@descendant, @level = descendant, level
end
def build
initial_level.upto(@level - 1) do |level|
build_descendant(level) unless descendant_defined?(level)
end
end
private
def descendant_defined?(level)
!!swap_level(level.next).safe_constantize
end
def build_descendant(level)
namespace(level.next).constantize.const_set(
swap_level(level.next).demodulize,
Class.new(swap_level(level).constantize)
)
end
def namespace(level)
swap_level(level).deconstantize.presence || 'Object'
end
def swap_level(level)
@descendant.name.sub LEVEL_REGEX, level.to_s
end
def initial_level
@descendant.name[LEVEL_REGEX].to_i
end
end
MyApp::Application.routes.draw do
ApiVersioning.setup(self) do
version 1 do
resources :accounts
end
version 2 do
# will inherit `resources :accounts`
resources :users
end
version 3 # will inherit routes in versions 1 & 2
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment