require 'set' require 'flipper' require 'mongo' module Flipper module Adapters class Mongo include ::Flipper::Adapter # Public: The name of the collection storing the feature data. attr_reader :collection def initialize(collection) @collection = collection @features_key = :flipper_features end # Public: The set of known features. def features read_feature_keys end # Public: Adds a feature to the set of known features. def add(feature) update @features_key, '$addToSet' => { 'features' => feature.key } true end # Public: Removes a feature from the set of known features. def remove(feature) update @features_key, '$pull' => { 'features' => feature.key } clear feature true end # Public: Clears all the gate values for a feature. def clear(feature) delete 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) doc = find(feature.key) result_for_feature(feature, doc) end def get_multi(features) read_many_features(features) end def get_all features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) } read_many_features(features) end # Public: Enables a gate for a given thing. # # feature - The Flipper::Feature for the gate. # gate - The Flipper::Gate to enable. # thing - The Flipper::Type being enabled for the gate. # # Returns true. def enable(feature, gate, thing) case gate.data_type when :boolean clear(feature) update feature.key, '$set' => { gate.key.to_s => thing.value.to_s, } when :integer update feature.key, '$set' => { gate.key.to_s => thing.value.to_s, } when :set update feature.key, '$addToSet' => { gate.key.to_s => thing.value.to_s, } when :json update feature.key, '$set' => { gate.key.to_s => Typecast.to_json(thing.value), } 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 delete feature.key when :integer update feature.key, '$set' => { gate.key.to_s => thing.value.to_s, } when :set update feature.key, '$pull' => { gate.key.to_s => thing.value.to_s, } when :json update feature.key, '$set' => { gate.key.to_s => nil, } else unsupported_data_type gate.data_type end true end private def read_feature_keys find(@features_key).fetch('features') { Set.new }.to_set end def read_many_features(features) docs = find_many(features.map(&:key)) result = {} features.each do |feature| result[feature.key] = result_for_feature(feature, docs[feature.key]) end result end # Private def unsupported_data_type(data_type) raise "#{data_type} is not supported by this adapter" end # Private def find(key) @collection.find(_id: key.to_s).limit(1).first || {} end def find_many(keys) docs = @collection.find(_id: { '$in' => keys }).to_a result = Hash.new { |hash, key| hash[key] = {} } docs.each do |doc| result[doc['_id']] = doc end result end # Private def update(key, updates) options = { upsert: true } @collection.find(_id: key.to_s).update_one(updates, options) end # Private def delete(key) @collection.find(_id: key.to_s).delete_one end def result_for_feature(feature, doc) result = {} feature.gates.each do |gate| result[gate.key] = case gate.data_type when :boolean, :integer doc[gate.key.to_s] when :set doc.fetch(gate.key.to_s) { Set.new }.to_set when :json value = doc[gate.key.to_s] Typecast.from_json(value) else unsupported_data_type gate.data_type end end result end end end end Flipper.configure do |config| config.adapter do url = ENV["FLIPPER_MONGO_URL"] || ENV["MONGO_URL"] collection = ENV["FLIPPER_MONGO_COLLECTION"] || "flipper" unless url raise ArgumentError, "The MONGO_URL environment variable must be set. For example: mongodb://127.0.0.1:27017/flipper" end Flipper::Adapters::Mongo.new(Mongo::Client.new(url)[collection]) end end