Skip to content

Commit

Permalink
Create Flipper#Api
Browse files Browse the repository at this point in the history
* api gemspec
* api middleware
* api boolean_gate
* api middleware
* boolean gate spec
  • Loading branch information
AlexWheeler committed Apr 22, 2016
1 parent e98f9cc commit 4992aa9
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 21 deletions.
23 changes: 23 additions & 0 deletions flipper-api.gemspec
@@ -0,0 +1,23 @@
# -*- encoding: utf-8 -*-
require File.expand_path('../lib/flipper/version', __FILE__)

flipper_api_files = lambda { |file|
file =~ /(flipper)[\/-]api/
}

Gem::Specification.new do |gem|
gem.authors = ["John Nunemaker"]
gem.email = ["nunemaker@gmail.com"]
gem.summary = "API for the Flipper gem"
gem.description = "Rack middleware that provides an API for the flipper gem."
gem.license = "MIT"
gem.homepage = "https://github.com/jnunemaker/flipper"
gem.files = `git ls-files`.split("\n").select(&flipper_api_files) + ["lib/flipper/version.rb"]
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").select(&flipper_api_files)
gem.name = "flipper-api"
gem.require_paths = ["lib"]
gem.version = Flipper::VERSION

gem.add_dependency 'rack', '>= 1.4', '< 3'
gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
end
1 change: 1 addition & 0 deletions lib/flipper-api.rb
@@ -0,0 +1 @@
require 'flipper/api'
18 changes: 18 additions & 0 deletions lib/flipper/action_collection.rb
@@ -0,0 +1,18 @@
module Flipper
# Internal: Used to detect the action that should be used in the middleware.
class ActionCollection
def initialize
@action_classes = []
end

def add(action_class)
@action_classes << action_class
end

def action_for_request(request)
@action_classes.detect { |action_class|
request.path_info =~ action_class.regex
}
end
end
end
40 changes: 40 additions & 0 deletions lib/flipper/api.rb
@@ -0,0 +1,40 @@
require 'rack'
require 'flipper'
require 'flipper/api/middleware'

module Flipper
module Api
def self.app(flipper)
app = App.new(200,{'Content-Type' => 'application/json'}, [''])
builder = Rack::Builder.new
yield builder if block_given?
builder.use Flipper::Api::Middleware, flipper
builder.run app
builder
end

class App
# Public: HTTP response code
# Use this method to update status code before responding
attr_writer :status

def initialize(status, headers, body)
@status = status
@headers = headers
@body = body
end

# Public : Rack expects object that responds to call
# env - environment hash
def call(env)
response
end

private

def response
[@status, @headers, @body]
end
end
end
end
108 changes: 108 additions & 0 deletions lib/flipper/api/action.rb
@@ -0,0 +1,108 @@
module Flipper
module Api
class Action
extend Forwardable

# Public: Call this in subclasses so the action knows its route.
#
# regex - The Regexp that this action should run for.
#
# Returns nothing.
def self.route(regex)
@regex = regex
end

# Internal: Initializes and runs an action for a given request.
#
# flipper - The Flipper::DSL instance.
# request - The Rack::Request that was sent.
#
# Returns result of Action#run.
def self.run(flipper, request)
new(flipper, request).run
end

# Internal: The regex that matches which routes this action will work for.
def self.regex
@regex || raise("#{name}.route is not set")
end

# Public: The instance of the Flipper::DSL the middleware was
# initialized with.
attr_reader :flipper

# Public: The Rack::Request to provide a response for.
attr_reader :request

# Public: The params for the request.
def_delegator :@request, :params

def initialize(flipper, request)
@flipper, @request = flipper, request
@code = 200
@headers = {"Content-Type" => "application/json"}
end

# Public: Runs the request method for the provided request.
#
# Returns whatever the request method returns in the action.
def run
if respond_to?(request_method_name)
catch(:halt) { send(request_method_name) }
else
raise Api::RequestMethodNotSupported, "#{self.class} does not support request method #{request_method_name.inspect}"
end
end

# Public: Runs another action from within the request method of a
# different action.
#
# action_class - The class of the other action to run.
#
# Examples
#
# run_other_action Home
# # => result of running Home action
#
# Returns result of other action.
def run_other_action(action_class)
action_class.new(flipper, request).run
end

# Public: Call this with a response to immediately stop the current action
# and respond however you want.
#
# response - The response you would like to return.
def halt(response)
throw :halt, response
end

def json_response(object)
header 'Content-Type', 'application/json'
status(200)
body = JSON.dump(object)
halt [@code, @headers, [body]]
end

# Public: Set the status code for the response.
#
# code - The Integer code you would like the response to return.
def status(code)
@code = code.to_i
end

# Public: Set a header.
#
# name - The String name of the header.
# value - The value of the header.
def header(name, value)
@headers[name] = value
end

# Private: Returns the request method converted to an action method.
def request_method_name
@request_method_name ||= @request.request_method.downcase
end
end
end
end
10 changes: 10 additions & 0 deletions lib/flipper/api/error.rb
@@ -0,0 +1,10 @@
module Flipper
module Api
# All flipper api errors inherit from this.
Error = Class.new(StandardError)

# Raised when a request method (get, post, etc.) is called for an action
# that does not know how to handle it.
RequestMethodNotSupported = Class.new(Error)
end
end
61 changes: 61 additions & 0 deletions lib/flipper/api/middleware.rb
@@ -0,0 +1,61 @@
require 'rack'
require 'flipper/action_collection'

# Require all V1 actions automatically.
Pathname(__FILE__).dirname.join('v1/actions').each_child(false) do |name|
require "flipper/api/v1/actions/#{name}"
end

module Flipper
module Api
class Middleware
# Public: Initializes an instance of the API middleware.
#
# app - The app this middleware is included in.
# flipper_or_block - The Flipper::DSL instance or a block that yields a
# Flipper::DSL instance to use for all operations.
#
# Examples
#
# flipper = Flipper.new(...)
#
# # using with a normal flipper instance
# use Flipper::Api::Middleware, flipper
#
# # using with a block that yields a flipper instance
# use Flipper::Api::Middleware, lambda { Flipper.new(...) }
#
def initialize(app, flipper_or_block)
@app = app

if flipper_or_block.respond_to?(:call)
@flipper_block = flipper_or_block
else
@flipper = flipper_or_block
end

@action_collection = ActionCollection.new
@action_collection.add Api::V1::Actions::BooleanGate
end

def flipper
@flipper ||= @flipper_block.call
end

def call(env)
dup.call!(env)
end

def call!(env)
request = Rack::Request.new(env)
action_class = @action_collection.action_for_request(request)
if action_class.nil?
@app.status = 404
@app.call(env)
else
action_class.run(flipper, request)
end
end
end
end
end
27 changes: 27 additions & 0 deletions lib/flipper/api/v1/actions/boolean_gate.rb
@@ -0,0 +1,27 @@
require 'flipper/api/action'

module Flipper
module Api
module V1
module Actions
class BooleanGate < Api::Action
route %r{api/v1/features/[^/]*/(enable|disable)/?\Z}

def put
feature_name = Rack::Utils.unescape(route_parts[-2])
feature = flipper[feature_name.to_sym]
action = Rack::Utils.unescape(route_parts.last)
feature.send(action)
json_response({feature: feature})
end

private

def route_parts
request.path.split("/")
end
end
end
end
end
end
20 changes: 0 additions & 20 deletions lib/flipper/ui/action_collection.rb

This file was deleted.

2 changes: 1 addition & 1 deletion lib/flipper/ui/middleware.rb
@@ -1,5 +1,5 @@
require 'rack'
require 'flipper/ui/action_collection'
require 'flipper/action_collection'

# Require all actions automatically.
Pathname(__FILE__).dirname.join('actions').each_child(false) do |name|
Expand Down
39 changes: 39 additions & 0 deletions spec/flipper/api/v1/actions/boolean_gate_spec.rb
@@ -0,0 +1,39 @@
require 'helper'

RSpec.describe Flipper::Api::V1::Actions::BooleanGate do
let(:app) { build_api(flipper) }

describe 'enable' do
before do
flipper[:my_feature].disable
put '/api/v1/features/my_feature/enable'
end

it 'enables feature' do
expect(last_response.status).to eq(200)
expect(flipper[:my_feature].on?).to be_truthy
end
end

describe 'disable' do
before do
flipper[:my_feature].enable
put '/api/v1/features/my_feature/disable'
end

it 'disables feature' do
expect(last_response.status).to eq(200)
expect(flipper[:my_feature].off?).to be_truthy
end
end

describe 'invalid paremeter' do
before do
put '/api/v1/features/my_feature/invalid_param'
end

it 'responds with 404 when not sent enable or disable parameter' do
expect(last_response.status).to eq(404)
end
end
end
1 change: 1 addition & 0 deletions spec/helper.rb
Expand Up @@ -10,6 +10,7 @@

require 'flipper'
require 'flipper-ui'
require 'flipper-api'

Dir[FlipperRoot.join("spec/support/**/*.rb")].each { |f| require f }

Expand Down
4 changes: 4 additions & 0 deletions spec/support/spec_helpers.rb
Expand Up @@ -14,6 +14,10 @@ def build_app(flipper)
}
end

def build_api(flipper)
Flipper::Api.app(flipper)
end

def build_flipper(adapter = build_memory_adapter)
Flipper.new(adapter)
end
Expand Down

0 comments on commit 4992aa9

Please sign in to comment.