Skip to content

Commit

Permalink
Initial pass at active record adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
jnunemaker committed Dec 20, 2015
1 parent 8840e61 commit 73752f1
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 0 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ gem 'statsd-ruby', '~> 1.2.1'
gem 'rspec', '~> 3.0'
gem 'rack-test', '~> 0.6.3'
gem 'activesupport', '~> 4.2.0'
gem 'sqlite3', '~> 1.3.11'

group(:guard) do
gem 'guard', '~> 2.12.5'
Expand Down
24 changes: 24 additions & 0 deletions flipper-active_record.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- encoding: utf-8 -*-
require File.expand_path('../lib/flipper/version', __FILE__)

flipper_mongo_files = lambda { |file|
file =~ /active_record/
}

Gem::Specification.new do |gem|
gem.authors = ["John Nunemaker"]
gem.email = ["[email protected]"]
gem.summary = "ActiveRecord adapter for Flipper"
gem.description = "ActiveRecord adapter for Flipper"
gem.license = "MIT"
gem.homepage = "https://github.com/jnunemaker/flipper"

gem.files = `git ls-files`.split("\n").select(&flipper_mongo_files) + ["lib/flipper/version.rb"]
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").select(&flipper_mongo_files)
gem.name = "flipper-active_record"
gem.require_paths = ["lib"]
gem.version = Flipper::VERSION

gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
gem.add_dependency 'activerecord', '~> 4.2.0'
end
1 change: 1 addition & 0 deletions lib/flipper-active_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'flipper/adapters/active_record'
142 changes: 142 additions & 0 deletions lib/flipper/adapters/active_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
require 'set'
require 'flipper'
require 'active_record'

module Flipper
module Adapters
class ActiveRecord
include Flipper::Adapter

class Feature < ::ActiveRecord::Base
self.table_name = "flipper_features"
end

class Gate < ::ActiveRecord::Base
self.table_name = "flipper_gates"
end

# Public: The name of the adapter.
attr_reader :name

def initialize
@name = :active_record
end

# Public: The set of known features.
def features
Feature.select(:key).map(&:key).to_set
end

# Public: Adds a feature to the set of known features.
def add(feature)
attributes = {key: feature.key}
# race condition, but add is only used by enable/disable which happen
# super rarely, so it shouldn't matter in practice
Feature.where(attributes).first || Feature.create!(attributes)
true
end

# Public: Removes a feature from the set of known features.
def remove(feature)
Feature.transaction do
Feature.delete_all(key: feature.key)
clear(feature)
end
true
end

# Public: Clears the gate values for a feature.
def clear(feature)
Gate.delete_all(feature_key: feature.key)
true
end

# Public: Gets the values for all gates for a given feature.
#
# Returns a Hash of Flipper::Gate#key => value.
def get(feature)
result = {}

db_gates = Gate.where(feature_key: feature.key)

feature.gates.each do |gate|
result[gate.key] = case gate.data_type
when :boolean
if db_gate = db_gates.detect { |db_gate| db_gate.key == gate.key.to_s }
db_gate.value
end
when :integer
if db_gate = db_gates.detect { |db_gate| db_gate.key == gate.key.to_s }
db_gate.value
end
when :set
db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map(&:value).to_set
else
unsupported_data_type gate.data_type
end
end

result
end

# Public: Enables a gate for a given thing.
#
# feature - The Flipper::Feature for the gate.
# gate - The Flipper::Gate to disable.
# thing - The Flipper::Type being disabled for the gate.
#
# Returns true.
def enable(feature, gate, thing)
case gate.data_type
when :boolean, :integer
Gate.create!({
feature_key: feature.key,
key: gate.key,
value: thing.value.to_s,
})
when :set
Gate.create!({
feature_key: feature.key,
key: gate.key,
value: thing.value.to_s,
})
else
unsupported_data_type gate.data_type
end

true
end

# Public: Disables a gate for a given thing.
#
# feature - The Flipper::Feature for the gate.
# gate - The Flipper::Gate to disable.
# thing - The Flipper::Type being disabled for the gate.
#
# Returns true.
def disable(feature, gate, thing)
case gate.data_type
when :boolean
clear(feature)
when :integer
Gate.create!({
feature_key: feature.key,
key: gate.key,
value: thing.value.to_s,
})
when :set
Gate.delete_all(feature_key: feature.key, key: gate.key, value: thing.value)
else
unsupported_data_type gate.data_type
end

true
end

# Private
def unsupported_data_type(data_type)
raise "#{data_type} is not supported by this adapter"
end
end
end
end
50 changes: 50 additions & 0 deletions spec/flipper/adapters/active_record_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require 'helper'
require 'flipper/adapters/active_record'
require 'flipper/spec/shared_adapter_specs'

# Turn off migration logging for specs
ActiveRecord::Migration.verbose = false

class CreateFlipperTables < ActiveRecord::Migration
def self.up
create_table :flipper_features do |t|
t.string :key, null: false
t.timestamps null: false
end
add_index :flipper_features, :key, unique: true

create_table :flipper_gates do |t|
t.string :feature_key, null: false
t.string :key, null: false
t.string :value
t.timestamps null: false
end
add_index :flipper_gates, [:feature_key, :key, :value], unique: true
end

def self.down
drop_table :flipper_gates
drop_table :flipper_features
end
end

RSpec.describe Flipper::Adapters::ActiveRecord do
subject { described_class.new }

before(:all) do
ActiveRecord::Base.establish_connection({
adapter: "sqlite3",
database: ":memory:",
})
end

before(:each) do
CreateFlipperTables.up
end

after(:each) do
CreateFlipperTables.down
end

it_should_behave_like 'a flipper adapter'
end

0 comments on commit 73752f1

Please sign in to comment.