{-# 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