feature-flipper-0.2.1.2: src/Control/Flipper/Types.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-|
Module : Control.Flipper.Types
Description : Datatype and Typeclass definitions
-}
module Control.Flipper.Types
( ActorId(..)
, Feature(..)
, Features(..)
, FeatureName(..)
, HasActorId(..)
, HasFeatureFlags(..)
, ModifiesFeatureFlags(..)
, Percentage(..)
, update
, upsert
, isEnabledFor
, mkFeature
, mkFeatures
) where
import Control.Monad (void)
import Control.Monad.Reader
import Control.Monad.State
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.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
within the current monad.
-}
class Monad m => HasFeatureFlags m where
-- | '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 Feature)
-- | 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)
instance (MonadIO m, HasFeatureFlags m) => HasFeatureFlags (ReaderT s m)
{- |
The 'ModifiesFeatureFlags' typeclass describes how to modify the Features store
within the current monad.
-}
class HasFeatureFlags m => ModifiesFeatureFlags m where
-- | '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 -> Feature -> m ()
-- | 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 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
-}
newtype FeatureName = FeatureName { unFeatureName :: Text }
deriving (Show, Eq, Ord)
instance IsString FeatureName where
fromString s = FeatureName (T.pack s)
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 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