packages feed

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 view
@@ -1,5 +1,7 @@ # flipper +[![CircleCI](https://circleci.com/gh/toddmohney/flipper/tree/master.svg?style=svg)](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