feature-flipper 0.2.0.1 → 0.2.1.2
raw patch · 6 files changed
+356/−76 lines, 6 filesdep +bytestringdep +digestdep ~basePVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: bytestring, digest
Dependency ranges changed: base
API changes (from Hackage documentation)
- Control.Flipper.Types: instance GHC.Classes.Eq Control.Flipper.Types.Features
+ Control.Flipper: enableFor :: (ModifiesFeatureFlags m, HasActorId a) => FeatureName -> a -> m ()
+ Control.Flipper: enableForPercentage :: (ModifiesFeatureFlags m) => FeatureName -> Percentage -> m ()
+ Control.Flipper: enabledFor :: (HasFeatureFlags m, HasActorId a) => FeatureName -> a -> m Bool
+ Control.Flipper: whenEnabledFor :: (HasFeatureFlags m, HasActorId a) => FeatureName -> a -> m () -> m ()
+ Control.Flipper.Types: ActorId :: ByteString -> ActorId
+ Control.Flipper.Types: Feature :: FeatureName -> Bool -> Set ActorId -> Percentage -> Feature
+ Control.Flipper.Types: Percentage :: Int -> Percentage
+ Control.Flipper.Types: [enabledActors] :: Feature -> Set ActorId
+ Control.Flipper.Types: [enabledPercentage] :: Feature -> Percentage
+ Control.Flipper.Types: [featureName] :: Feature -> FeatureName
+ Control.Flipper.Types: [isEnabled] :: Feature -> Bool
+ Control.Flipper.Types: actorId :: HasActorId a => a -> ActorId
+ Control.Flipper.Types: class HasActorId a
+ Control.Flipper.Types: data Feature
+ Control.Flipper.Types: instance Data.Digest.CRC32.CRC32 Control.Flipper.Types.ActorId
+ Control.Flipper.Types: instance GHC.Classes.Eq Control.Flipper.Types.ActorId
+ Control.Flipper.Types: instance GHC.Classes.Eq Control.Flipper.Types.Feature
+ Control.Flipper.Types: instance GHC.Classes.Eq Control.Flipper.Types.Percentage
+ Control.Flipper.Types: instance GHC.Classes.Ord Control.Flipper.Types.ActorId
+ Control.Flipper.Types: instance GHC.Classes.Ord Control.Flipper.Types.Percentage
+ Control.Flipper.Types: instance GHC.Enum.Bounded Control.Flipper.Types.Percentage
+ Control.Flipper.Types: instance GHC.Num.Num Control.Flipper.Types.Percentage
+ Control.Flipper.Types: instance GHC.Read.Read Control.Flipper.Types.Percentage
+ Control.Flipper.Types: instance GHC.Show.Show Control.Flipper.Types.ActorId
+ Control.Flipper.Types: instance GHC.Show.Show Control.Flipper.Types.Feature
+ Control.Flipper.Types: instance GHC.Show.Show Control.Flipper.Types.Percentage
+ Control.Flipper.Types: isEnabledFor :: (HasActorId a) => Feature -> a -> Bool
+ Control.Flipper.Types: mkFeature :: FeatureName -> Feature
+ Control.Flipper.Types: newtype ActorId
+ Control.Flipper.Types: newtype Percentage
+ Control.Flipper.Types: upsert :: ModifiesFeatureFlags m => Feature -> m ()
- Control.Flipper.Types: Features :: Map FeatureName Bool -> Features
+ Control.Flipper.Types: Features :: Map FeatureName Feature -> Features
- Control.Flipper.Types: [unFeatures] :: Features -> Map FeatureName Bool
+ Control.Flipper.Types: [unFeatures] :: Features -> Map FeatureName Feature
- Control.Flipper.Types: class HasFeatureFlags m => ModifiesFeatureFlags m where updateFeatures = lift . updateFeatures updateFeature fName isEnabled = lift $ updateFeature fName isEnabled
+ Control.Flipper.Types: class HasFeatureFlags m => ModifiesFeatureFlags m where updateFeatures = lift . updateFeatures updateFeature fName feature = lift $ updateFeature fName feature
- Control.Flipper.Types: getFeature :: (HasFeatureFlags m, MonadTrans t, HasFeatureFlags m1, m ~ t m1) => FeatureName -> m (Maybe Bool)
+ Control.Flipper.Types: getFeature :: (HasFeatureFlags m, MonadTrans t, HasFeatureFlags m1, m ~ t m1) => FeatureName -> m (Maybe Feature)
- Control.Flipper.Types: mkFeatures :: Map FeatureName Bool -> Features
+ Control.Flipper.Types: mkFeatures :: Features
- Control.Flipper.Types: update :: ModifiesFeatureFlags m => FeatureName -> (Maybe Bool -> Maybe Bool) -> m ()
+ Control.Flipper.Types: update :: ModifiesFeatureFlags m => FeatureName -> (Maybe Feature -> Maybe Feature) -> m ()
- Control.Flipper.Types: updateFeature :: (ModifiesFeatureFlags m, MonadTrans t, ModifiesFeatureFlags m1, m ~ t m1) => FeatureName -> Bool -> m ()
+ Control.Flipper.Types: updateFeature :: (ModifiesFeatureFlags m, MonadTrans t, ModifiesFeatureFlags m1, m ~ t m1) => FeatureName -> Feature -> m ()
Files
- README.md +5/−5
- feature-flipper.cabal +9/−4
- src/Control/Flipper.hs +92/−20
- src/Control/Flipper/Adapters/Memory.hs +3/−4
- src/Control/Flipper/Types.hs +131/−20
- test/Control/FlipperSpec.hs +116/−23
README.md view
@@ -1,5 +1,7 @@ # flipper +[](https://circleci.com/gh/toddmohney/flipper/tree/master)+ A light-weight library providing an interface for minimally obtrusive feature toggles. @@ -21,9 +23,7 @@ - [Configuring feature flags from environment variables](https://github.com/toddmohney/flipper/tree/master/examples/environment-config) -## Roadmap--### Add storage adapters+### Persistent storage adapters -- TODO: Create Postgres adapter-- TODO: Create Redis adapter+If persisting your feature flags in Postgres is more your cup of tea, check+out [Flipper Postgres](https://github.com/toddmohney/flipper-postgres)
feature-flipper.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack name: feature-flipper-version: 0.2.0.1+version: 0.2.1.2 synopsis: A minimally obtrusive feature flag library description: A minimally obtrusive feature flag library homepage: https://github.com/toddmohney/flipper#readme@@ -28,7 +28,7 @@ hs-source-dirs: src default-extensions: OverloadedStrings- ghc-options: -Wall -fwarn-unused-matches -fwarn-unused-binds -fwarn-unused-imports+ ghc-options: -Wall -freverse-errors -fwarn-unused-binds -fwarn-unused-imports -fwarn-unused-matches exposed-modules: Control.Flipper Control.Flipper.Types@@ -37,7 +37,9 @@ Paths_feature_flipper build-depends: base >=4.8 && <5+ , bytestring , containers+ , digest , mtl , text default-language: Haskell2010@@ -49,11 +51,14 @@ default-extensions: OverloadedStrings main-is: Spec.hs build-depends:- base+ base >=4.8 && <5+ , bytestring , containers+ , digest+ , mtl+ , text , feature-flipper , hspec- , mtl other-modules: Control.FlipperSpec ghc-options: -threaded -rtsopts -with-rtsopts=-N
src/Control/Flipper.hs view
@@ -4,14 +4,20 @@ -} module Control.Flipper ( enabled+ , enabledFor , enable+ , enableFor+ , enableForPercentage , disable , toggle , whenEnabled+ , whenEnabledFor , module Control.Flipper.Types ) where import Control.Monad (when)+import Data.Monoid ((<>))+import qualified Data.Set as S import Control.Flipper.Types @@ -25,51 +31,103 @@ whenEnabled :: (HasFeatureFlags m) => FeatureName -> m () -> m () whenEnabled fName f = do- isEnabled <- enabled fName- when isEnabled f+ featureEnabled <- enabled fName+ when featureEnabled f {- |+The 'whenEnabledFor' function calls the supplied function, 'm ()', when the given+'FeatureName' is enabled for the given actor.++When the feature specified by 'FeatureName' is disabled for the given actor,+'m ()' is not evaluated.+-}+whenEnabledFor :: (HasFeatureFlags m, HasActorId a)+ => FeatureName -> a -> m () -> m ()+whenEnabledFor fName actor f = do+ featureEnabled <- enabledFor fName actor+ when featureEnabled f++{- | The 'enabled' function returns a Bool indicating if the queried feature is active. -When the queried FeatureName exists, the active state is returned.- When the queried FeatureName does not exists, 'enabled' returns False. -} enabled :: HasFeatureFlags m => FeatureName -> m Bool enabled fName = do- feature <- getFeature fName- if feature == Just True- then return True- else return False+ mFeature <- getFeature fName+ case mFeature of+ (Just feature) -> return $ isEnabled feature+ Nothing -> return False {- |-The 'enable' function activates a feature.+The 'enabledFor' function returns a Bool indicating if the queried feature is+active for the given enitty. -When the FeatureName exists in the store, it is set to active.+If the queried FeatureName does not exists, 'enabledFor' returns False.+-}+enabledFor :: (HasFeatureFlags m, HasActorId a)+ => FeatureName -> a -> m Bool+enabledFor fName actor = do+ mFeature <- getFeature fName+ case mFeature of+ Nothing -> return False+ (Just feature) -> return $ isEnabledFor feature actor +{- |+The 'enable' function activates a feature globally.+ When the FeatureName does not exist, it is created and set to active. -} enable :: ModifiesFeatureFlags m => FeatureName -> m ()-enable fName = updateFeature fName True+enable fName = upsertFeature fName True {- |-The 'disable' function deactivates a feature.+The 'enableFor' function activates a feature for a single actor. -When the FeatureName exists in the store, it is set to inactive.+If the FeatureName does not exist in the store, it is created and set to active+only for the given actor.+-}+enableFor :: (ModifiesFeatureFlags m, HasActorId a)+ => FeatureName -> a -> m ()+enableFor fName actor = update fName (enableFor' actor fName) +enableFor' :: HasActorId a => a -> FeatureName -> Maybe Feature -> Maybe Feature+enableFor' actor _ (Just feature) = Just $ feature { enabledActors = S.insert (actorId actor) (enabledActors feature) }+enableFor' actor fname Nothing = Just $ (mkFeature fname) { enabledActors = S.singleton (actorId actor) }++{- |+The 'enableForPercentage' function activates a feature for a percentage of actors.++If the FeatureName does not exist in the store, it is created and set to active+only for the specified percentage.+-}+enableForPercentage :: (ModifiesFeatureFlags m)+ => FeatureName -> Percentage -> m ()+enableForPercentage fName pct+ | pct < 0 = raiseOutOfRangeError+ | pct > 100 = raiseOutOfRangeError+ | otherwise = update fName (enableForPercentage' pct fName)+ where+ raiseOutOfRangeError = error ("Invalid percentage: " <> show pct <> " Expected a value between 0 - 100")++enableForPercentage' :: Percentage -> FeatureName -> Maybe Feature -> Maybe Feature+enableForPercentage' pct _ (Just feature) = Just $ feature { enabledPercentage = pct }+enableForPercentage' pct fname Nothing = Just (mkFeature fname) { enabledPercentage = pct }++{- |+The 'disable' function deactivates a feature globally.+ When the FeatureName does not exist, it is created and set to inactive. -} disable :: ModifiesFeatureFlags m => FeatureName -> m ()-disable fName = updateFeature fName False+disable fName = upsertFeature fName False {- |-The 'toggle' function flips the current state of a feature.--When the FeatureName exists in the store, it flips the feature state.+The 'toggle' function flips the current state of a feature globally. When the FeatureName does not exist, it is created and set to True. -}@@ -77,6 +135,20 @@ => FeatureName -> m () toggle fName = update fName flipIt' where- flipIt' :: Maybe Bool -> Maybe Bool- flipIt' (Just a) = Just (not a)- flipIt' Nothing = Just True+ flipIt' :: Maybe Feature -> Maybe Feature+ flipIt' (Just feature) = Just (feature { isEnabled = not (isEnabled feature) })+ flipIt' Nothing = Just $ (mkFeature fName) { isEnabled = True }++{- |+When the FeatureName exists in the store, it is set to the specified `isEnabled` state.++When the FeatureName does not exist, it is created and set to the specified `isEnabled` state.+-}+upsertFeature :: ModifiesFeatureFlags m+ => FeatureName -> Bool -> m ()+upsertFeature fName featureEnabled =+ update fName upsertFeature'+ where+ upsertFeature' :: (Maybe Feature -> Maybe Feature)+ upsertFeature' Nothing = Just $ (mkFeature fName) { isEnabled = featureEnabled }+ upsertFeature' (Just feature) = Just (feature { isEnabled = featureEnabled })
src/Control/Flipper/Adapters/Memory.hs view
@@ -27,15 +27,14 @@ instance (Monad m) => HasFeatureFlags (FlipperT m) where getFeatures = get - getFeature featureName = do+ getFeature fname = do features <- unFeatures <$> getFeatures- return (Map.lookup featureName features)+ return (Map.lookup fname features) instance (Monad m) => ModifiesFeatureFlags (FlipperT m) where updateFeatures = put - updateFeature featureName True = update featureName (\_ -> Just True)- updateFeature featureName False = update featureName (\_ -> Just False)+ updateFeature fname feature = update fname (\_ -> Just feature) {- | Evaluates a feature-switched computation, returning the final value and
src/Control/Flipper/Types.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TypeFamilies #-} {-|@@ -7,23 +8,35 @@ Description : Datatype and Typeclass definitions -} module Control.Flipper.Types- ( Features(..)+ ( ActorId(..)+ , Feature(..)+ , Features(..) , FeatureName(..)+ , HasActorId(..) , HasFeatureFlags(..) , ModifiesFeatureFlags(..)+ , Percentage(..) , update+ , upsert+ , isEnabledFor+ , mkFeature , mkFeatures ) where -import Control.Monad (void)+import Control.Monad (void) import Control.Monad.Reader import Control.Monad.State-import Data.Map.Strict (Map)-import qualified Data.Map.Strict as Map+import Data.ByteString (ByteString)+import qualified Data.Digest.CRC32 as D+import qualified Data.List as L+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map import Data.Monoid-import Data.String (IsString (..))-import Data.Text (Text)-import qualified Data.Text as T+import Data.Set (Set)+import qualified Data.Set as S+import Data.String (IsString (..))+import Data.Text (Text)+import qualified Data.Text as T {- | The 'HasFeatureFlags' typeclass describes how to access the Features store@@ -33,13 +46,15 @@ -- | 'getFeatures' access the Features store within the current monad getFeatures :: m Features + -- | default implementation provided to reduce boilerplate default getFeatures :: (MonadTrans t, HasFeatureFlags m1, m ~ t m1) => m Features getFeatures = lift getFeatures -- | 'getFeature' access a single Feature within the current monad- getFeature :: FeatureName -> m (Maybe Bool)+ getFeature :: FeatureName -> m (Maybe Feature) - default getFeature :: (MonadTrans t, HasFeatureFlags m1, m ~ t m1) => FeatureName -> m (Maybe Bool)+ -- | default implementation provided to reduce boilerplate+ default getFeature :: (MonadTrans t, HasFeatureFlags m1, m ~ t m1) => FeatureName -> m (Maybe Feature) getFeature = lift . getFeature instance (MonadIO m, HasFeatureFlags m) => HasFeatureFlags (StateT s m)@@ -53,28 +68,104 @@ -- | 'updateFeatures' modifies the Features store within the current monad updateFeatures :: Features -> m () + -- | default implementation provided to reduce boilerplate default updateFeatures :: (MonadTrans t, ModifiesFeatureFlags m1, m ~ t m1) => Features -> m () updateFeatures = lift . updateFeatures -- | 'updateFeature' modifies a single Feature within the current monad- updateFeature :: FeatureName -> Bool -> m ()+ updateFeature :: FeatureName -> Feature -> m () - default updateFeature :: (MonadTrans t, ModifiesFeatureFlags m1, m ~ t m1) => FeatureName -> Bool -> m ()- updateFeature fName isEnabled = lift $ updateFeature fName isEnabled+ -- | default implementation provided to reduce boilerplate+ default updateFeature :: (MonadTrans t, ModifiesFeatureFlags m1, m ~ t m1) => FeatureName -> Feature -> m ()+ updateFeature fName feature = lift $ updateFeature fName feature instance (MonadIO m, ModifiesFeatureFlags m) => ModifiesFeatureFlags (StateT s m) instance (MonadIO m, ModifiesFeatureFlags m) => ModifiesFeatureFlags (ReaderT s m) {- |+A standardization of feature-enableable IDs.+-}+newtype ActorId = ActorId ByteString+ deriving (Show, Eq, Ord, D.CRC32)++{- |+Typeclass describing how to derive an ActorId from a datatype.++The resulting ActorId produced must be unique within the set of actors+permitted to a given feature.++To clarify, let's say the actor is a User and User is defined as+@+data User = User { id :: Int }+@+It's sufficient to use the `id` alone if it is unique among Users and User types+are the only actor type using a given Feature. However, if a Feature is used by+both `User` types and `data Admin = Admin { id :: Int }` types where IDs are _not_+unique between types, it is recommended to avoid collisions by combining the+type name and the ID unique to that type.++For example, the implementation for `User` and Admin could be+@+instance HasActorId User where+ actorId user = "User:" <> show (user id)++instance HasActorId Admin where+ actorId user = "Admin:" <> show (user id)+@+-}+class HasActorId a where+ actorId :: a -> ActorId++{- |+A type describing an access-controlled feature.+-}+data Feature = Feature+ {+ -- | the name of the feature+ featureName :: FeatureName++ -- | flag indicating if the Feautre is globally enabled+ , isEnabled :: Bool++ -- | a list of ActorIDs for which to enable the Feature.+ , enabledActors :: Set ActorId++ -- | the percentage of total actors for which to enable the Feature.+ -- | 0 <= enabledPercentage <= 100+ , enabledPercentage :: Percentage+ } deriving (Show, Eq)++{- |+Smart constructor+-}+mkFeature :: FeatureName -> Feature+mkFeature fname = Feature+ { featureName = fname+ , isEnabled = False+ , enabledActors = S.empty+ , enabledPercentage = Percentage 0+ }++newtype Percentage = Percentage Int+ deriving (Show, Read, Eq, Ord, Num)++instance Bounded Percentage where+ minBound = Percentage 0+ maxBound = Percentage 100++{- | An abstraction representing the current state of the features store. -}-newtype Features = Features { unFeatures :: Map FeatureName Bool }- deriving (Show, Eq)+newtype Features = Features { unFeatures :: Map FeatureName Feature }+ deriving (Show) instance Monoid Features where mempty = Features mempty mappend a b = Features (unFeatures a <> unFeatures b) +mkFeatures :: Features+mkFeatures = Features Map.empty+ {- | The main identifier of a feature -}@@ -84,17 +175,37 @@ instance IsString FeatureName where fromString s = FeatureName (T.pack s) -{- |-Convienience constructor--}-mkFeatures :: Map FeatureName Bool -> Features-mkFeatures = Features+upsert :: ModifiesFeatureFlags m+ => Feature -> m ()+upsert f = do+ features <- unFeatures <$> getFeatures+ void . updateFeatures . Features $ Map.insertWith mergeFeatures (featureName f) f features +mergeFeatures :: Feature -> Feature -> Feature+mergeFeatures new old = Feature+ { featureName = featureName new+ , isEnabled = isEnabled new+ , enabledActors = enabledActors old <> enabledActors new+ , enabledPercentage = enabledPercentage new+ }+ {- | Updates a single Feature within the current monad -} update :: ModifiesFeatureFlags m- => FeatureName -> (Maybe Bool -> Maybe Bool) -> m ()+ => FeatureName -> (Maybe Feature -> Maybe Feature) -> m () update fName updateFn = do features <- unFeatures <$> getFeatures void . updateFeatures . Features $ Map.alter updateFn fName features++isEnabledFor :: (HasActorId a) => Feature -> a -> Bool+isEnabledFor (Feature _ globallyEnabled actors (Percentage pct)) actor =+ globallyEnabled+ || inActivePercentageGroup+ || inActiveActorsGroup+ where+ inActivePercentageGroup = mod (actorHash actor) 100 < pct+ inActiveActorsGroup = actorId actor `L.elem` actors++actorHash :: HasActorId a => a -> Int+actorHash a = fromIntegral . D.crc32 $ actorId a
test/Control/FlipperSpec.hs view
@@ -1,10 +1,18 @@ module Control.FlipperSpec (main, spec) where +import qualified Data.ByteString.Char8 as C8 import qualified Data.Map.Strict as M+import qualified Data.Set as S import Test.Hspec -import Control.Flipper-import Control.Flipper.Adapters.Memory+import Control.Flipper+ ( ActorId(..)+ , Features(..)+ , Feature(..)+ , HasActorId(..)+ )+import qualified Control.Flipper as F+import qualified Control.Flipper.Adapters.Memory as FM main :: IO () main = hspec spec@@ -14,43 +22,111 @@ describe "whenEnabled" $ do context "the feature is enabled" $ it "evaluates the given monad" $ do- let featureState = mkFeatures $ M.insert "ADD_ANOTHER_FEATURE" True mempty- store <- execFlipperT featureState (whenEnabled "ADD_ANOTHER_FEATURE" (enable "ANOTHER_FEATURE"))- M.lookup "ANOTHER_FEATURE" (unFeatures store) `shouldBe` Just True+ let feature = (F.mkFeature "ADD_ANOTHER_FEATURE") { isEnabled = True }+ let featureState = Features $ M.singleton (featureName feature) feature+ store <- FM.execFlipperT featureState (F.whenEnabled "ADD_ANOTHER_FEATURE" (F.enable "ANOTHER_FEATURE"))+ M.lookup "ANOTHER_FEATURE" (unFeatures store) `shouldBe` Just (F.mkFeature "ANOTHER_FEATURE") { isEnabled = True } context "the feature is disabled" $ it "does not evaluate the given monad" $ do- let featureState = mkFeatures $ M.insert "ADD_ANOTHER_FEATURE" False mempty- store <- execFlipperT featureState (whenEnabled "ADD_ANOTHER_FEATURE" (enable "ANOTHER_FEATURE"))+ let feature = (F.mkFeature "ADD_ANOTHER_FEATURE") { isEnabled = False }+ let featureState = Features $ M.singleton (featureName feature) feature+ store <- FM.execFlipperT featureState (F.whenEnabled "ADD_ANOTHER_FEATURE" (F.enable "ANOTHER_FEATURE")) M.lookup "ANOTHER_FEATURE" (unFeatures store) `shouldBe` Nothing + describe "whenEnabledFor" $ do+ context "the feature is enabled globally" $+ it "evaluates the given monad" $ do+ let feature = (F.mkFeature "ADD_ANOTHER_FEATURE") { isEnabled = True }+ let featureState = Features $ M.singleton (featureName feature) feature+ store <- FM.execFlipperT featureState (F.whenEnabledFor "ADD_ANOTHER_FEATURE" myActor (F.enable "ANOTHER_FEATURE"))+ M.lookup "ANOTHER_FEATURE" (unFeatures store) `shouldBe` Just (F.mkFeature "ANOTHER_FEATURE") { isEnabled = True }++ context "the feature is enabled for the given actor" $+ it "evaluates the given monad" $ do+ let feature = (F.mkFeature "ADD_ANOTHER_FEATURE") { enabledActors = S.singleton (actorId myActor), isEnabled = False }+ let featureState = Features $ M.singleton (featureName feature) feature+ store <- FM.execFlipperT featureState (F.whenEnabledFor "ADD_ANOTHER_FEATURE" myActor (F.enable "ANOTHER_FEATURE"))+ M.lookup "ANOTHER_FEATURE" (unFeatures store) `shouldBe` Just (F.mkFeature "ANOTHER_FEATURE") { isEnabled = True }++ context "the feature is not enabled for the given actor" $+ it "does not evaluate the given monad" $ do+ let feature = F.mkFeature "ADD_ANOTHER_FEATURE"+ let featureState = Features $ M.singleton (featureName feature) feature+ store <- FM.execFlipperT featureState (F.whenEnabledFor "ADD_ANOTHER_FEATURE" myActor (F.enable "ANOTHER_FEATURE"))+ M.lookup "ANOTHER_FEATURE" (unFeatures store) `shouldBe` Nothing+ describe "enable" $ do it "enables a new feature key" $ let featureState = mempty :: Features- result = evalFlipperT featureState (enable "NEW_FEATURE" >> enabled "NEW_FEATURE")+ result = FM.evalFlipperT featureState (F.enable "NEW_FEATURE" >> F.enabled "NEW_FEATURE") in result `shouldReturn` True it "enables a existing feature key" $ let- featureState = mkFeatures $ M.insert "FEATURE" False mempty- result = evalFlipperT featureState (enable "FEATURE" >> enabled "FEATURE")+ feature = (F.mkFeature "FEATURE") { isEnabled = False }+ featureState = Features $ M.singleton (featureName feature) feature+ result = FM.evalFlipperT featureState (F.enable "FEATURE" >> F.enabled "FEATURE") in result `shouldReturn` True + describe "enableFor" $ do+ describe "with an existing feature" $ do+ it "enables the feature only for the given actor" $ do+ let feature = F.mkFeature "FEATURE"+ let featureState = Features $ M.singleton (featureName feature) feature+ let result1 = FM.evalFlipperT featureState (F.enableFor "FEATURE" myActor >> F.enabledFor "FEATURE" myActor)+ let result2 = FM.evalFlipperT featureState (F.enableFor "FEATURE" myActor >> F.enabledFor "FEATURE" myOtherActor)++ result1 `shouldReturn` True+ result2 `shouldReturn` False++ describe "with a non-existant feature" $ do+ it "enables the feature only for the given actor" $ do+ let featureState = mempty :: Features+ let result1 = FM.evalFlipperT featureState (F.enableFor "FEATURE" myActor >> F.enabledFor "FEATURE" myActor)+ let result2 = FM.evalFlipperT featureState (F.enableFor "FEATURE" myActor >> F.enabledFor "FEATURE" myOtherActor)++ result1 `shouldReturn` True+ result2 `shouldReturn` False++ describe "enabledFor" $ do+ it "returns True if the feature is enabled globally" $ do+ let feature = (F.mkFeature "FEATURE") { isEnabled = True }+ let featureState = Features $ M.singleton (featureName feature) feature+ let result = FM.evalFlipperT featureState (F.enabledFor "FEATURE" myActor)++ result `shouldReturn` True++ it "returns True if the feature is enabled for the actor" $ do+ let feature = (F.mkFeature "FEATURE") { enabledActors = S.singleton (actorId myActor), isEnabled = False }+ let featureState = Features $ M.singleton (featureName feature) feature+ let result = FM.evalFlipperT featureState (F.enabledFor "FEATURE" myActor)++ result `shouldReturn` True++ it "returns False if the feature is not enabled for this actor" $ do+ let feature = F.mkFeature "FEATURE"+ let featureState = Features $ M.singleton (featureName feature) feature+ let result = FM.evalFlipperT featureState (F.enabledFor "FEATURE" myActor)++ result `shouldReturn` False+ describe "disable" $ do it "disables a new feature key" $ let featureState = mempty :: Features- result = evalFlipperT featureState (disable "NEW_FEATURE" >> enabled "NEW_FEATURE")+ result = FM.evalFlipperT featureState (F.disable "NEW_FEATURE" >> F.enabled "NEW_FEATURE") in result `shouldReturn` False it "disables a existing feature key" $ let- featureState = mkFeatures $ M.insert "FEATURE" True mempty- result = evalFlipperT featureState (disable "FEATURE" >> enabled "FEATURE")+ feature = (F.mkFeature "FEATURE") { isEnabled = True }+ featureState = Features $ M.singleton (featureName feature) feature+ result = FM.evalFlipperT featureState (F.disable "FEATURE" >> F.enabled "FEATURE") in result `shouldReturn` False @@ -58,42 +134,59 @@ it "enables a new feature key" $ let featureState = mempty :: Features- result = evalFlipperT featureState (toggle "NEW_FEATURE" >> enabled "NEW_FEATURE")+ result = FM.evalFlipperT featureState (F.toggle "NEW_FEATURE" >> F.enabled "NEW_FEATURE") in result `shouldReturn` True it "enables a disabled feature key" $ let- featureState = mkFeatures $ M.insert "FEATURE" False mempty- result = evalFlipperT featureState (toggle "FEATURE" >> enabled "FEATURE")+ feature = (F.mkFeature "FEATURE") { isEnabled = False }+ featureState = Features $ M.singleton (featureName feature) feature+ result = FM.evalFlipperT featureState (F.toggle "FEATURE" >> F.enabled "FEATURE") in result `shouldReturn` True it "disables a enabled feature key" $ let- featureState = mkFeatures $ M.insert "FEATURE" True mempty- result = evalFlipperT featureState (toggle "FEATURE" >> enabled "FEATURE")+ feature = (F.mkFeature "FEATURE") { isEnabled = True }+ featureState = Features $ M.singleton (featureName feature) feature+ result = FM.evalFlipperT featureState (F.toggle "FEATURE" >> F.enabled "FEATURE") in result `shouldReturn` False describe "enabled" $ do it "returns True when the feature is enabled" $ let- featureState = mkFeatures $ M.insert "ENABLED_FEATURE" True mempty- result = evalFlipperT featureState (enabled "ENABLED_FEATURE")+ feature = (F.mkFeature "ENABLED_FEATURE") { isEnabled = True }+ featureState = Features $ M.singleton (featureName feature) feature+ result = FM.evalFlipperT featureState (F.enabled "ENABLED_FEATURE") in result `shouldReturn` True it "returns False when the feature is not enabled" $ let- featureState = mkFeatures $ M.insert "DISABLED_FEATURE" False mempty- result = evalFlipperT featureState (enabled "DISABLED_FEATURE")+ feature = (F.mkFeature "DISABLED_FEATURE") { isEnabled = False }+ featureState = Features $ M.singleton (featureName feature) feature+ result = FM.evalFlipperT featureState (F.enabled "DISABLED_FEATURE") in result `shouldReturn` False it "returns False when the feature key is not found" $ let featureState = mempty :: Features- result = evalFlipperT featureState (enabled "NON_EXISTANT_FEATURE")+ result = FM.evalFlipperT featureState (F.enabled "NON_EXISTANT_FEATURE") in result `shouldReturn` False++newtype TestActor = TestActor+ { myId :: Int+ } deriving (Show, Eq)++instance HasActorId TestActor where+ actorId actor = ActorId . C8.pack . show $ myId actor++myActor :: TestActor+myActor = TestActor 1++myOtherActor :: TestActor+myOtherActor = TestActor 2