launchdarkly-server-sdk-3.0.0: src/LaunchDarkly/Server/Integrations/TestData/FlagBuilder.hs
module LaunchDarkly.Server.Integrations.TestData.FlagBuilder
( FlagBuilder(..)
, UserKey
, VariationIndex
, newFlagBuilder
, booleanFlag
, on
, fallthroughVariation
, offVariation
, variationForAllUsers
, valueForAllUsers
, variationForUser
, variations
, buildFlag
, UserAttribute
, ifMatch
, ifNotMatch
, FlagRuleBuilder
, andMatch
, andNotMatch
, thenReturn
, Variation
)
where
import qualified Data.Aeson as Aeson
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Natural (Natural)
import qualified LaunchDarkly.Server.Features as F
import qualified LaunchDarkly.Server.Operators as Op
import Data.Function ((&))
type UserKey = Text
type VariationIndex = Integer
trueVariationForBoolean, falseVariationForBoolean :: VariationIndex
trueVariationForBoolean = 0
falseVariationForBoolean = 1
variationForBoolean :: Bool -> VariationIndex
variationForBoolean True = trueVariationForBoolean
variationForBoolean False = falseVariationForBoolean
-- |
-- A builder for feature flag configurations to be used with "LaunchDarkly.Server.Integrations.TestData".
--
-- see 'LaunchDarkly.Server.Integrations.TestData.flag' and
-- 'LaunchDarkly.Server.Integrations.TestData.update'
data FlagBuilder = FlagBuilder
{ fbKey :: Text
, fbOffVariation :: Maybe VariationIndex
, fbOn :: Bool
, fbFallthroughVariation :: Maybe VariationIndex
, fbVariations :: [Aeson.Value]
, fbTargetMap :: Map UserKey VariationIndex
, fbRules :: [FlagRule]
} deriving (Show)
fbTargets :: FlagBuilder -> [F.Target]
fbTargets flagBuilder =
Map.elems $
Map.mapWithKey (flip F.Target) $
Map.foldrWithKey go mempty (fbTargetMap flagBuilder)
where
go userKey variation =
Map.insertWith (<>) variation [userKey]
buildFlag :: Natural -> FlagBuilder -> F.Flag
buildFlag version flagBuilder =
F.Flag
{ F.key = fbKey flagBuilder
, F.version = version
, F.on = fbOn flagBuilder
, F.trackEvents = False
, F.trackEventsFallthrough = False
, F.deleted = False
, F.prerequisites = []
, F.salt = "salt"
, F.targets = fbTargets flagBuilder
, F.rules = mapWithIndex convertFlagRule (fbRules flagBuilder)
, F.fallthrough = F.VariationOrRollout (fbFallthroughVariation flagBuilder) Nothing
, F.offVariation = fbOffVariation flagBuilder
, F.variations = fbVariations flagBuilder
, F.debugEventsUntilDate = Nothing
, F.clientSideAvailability = F.ClientSideAvailability False False False
}
mapWithIndex :: Integral num => (num -> a -> b) -> [a] -> [b]
mapWithIndex f l =
fmap (uncurry f) (zip [0..] l)
newFlagBuilder :: Text -> FlagBuilder
newFlagBuilder key =
FlagBuilder
{ fbKey = key
, fbOffVariation = Nothing
, fbOn = True
, fbFallthroughVariation = Nothing
, fbVariations = mempty
, fbTargetMap = mempty
, fbRules = mempty
}
booleanFlagVariations :: [Aeson.Value]
booleanFlagVariations = [Aeson.Bool True, Aeson.Bool False]
isBooleanFlag :: FlagBuilder -> Bool
isBooleanFlag flagBuilder
| booleanFlagVariations == fbVariations flagBuilder = True
| otherwise = False
-- |
-- A shortcut for setting the flag to use the standard boolean configuration.
--
-- This is the default for all new flags created with 'LaunchDarkly.Server.Integrations.TestData.flag'. The flag
-- will have two variations, @True@ and @False@ (in that order); it will return
-- @False@ whenever targeting is off, and @True@ when targeting is on if no other
-- settings specify otherwise.
booleanFlag :: FlagBuilder -> FlagBuilder
booleanFlag flagBuilder
| isBooleanFlag flagBuilder =
flagBuilder
| otherwise =
flagBuilder
& variations booleanFlagVariations
& fallthroughVariation trueVariationForBoolean
& offVariation falseVariationForBoolean
-- |
-- Sets targeting to be on or off for this flag.
--
-- The effect of this depends on the rest of the flag configuration, just as it does on the
-- real LaunchDarkly dashboard. In the default configuration that you get from calling
-- 'LaunchDarkly.Server.Integrations.TestData.flag' with a new flag key, the flag will return @False@
-- whenever targeting is off, and @True@ when targeting is on.
on :: Bool -- ^ isOn @True@ if targeting should be on
-> FlagBuilder
-> FlagBuilder
on isOn fb =
fb{ fbOn = isOn }
-- |
-- Removes any existing rules from the flag.
-- This undoes the effect of methods like 'ifMatch' or 'ifNotMatch'
clearRules :: FlagBuilder -> FlagBuilder
clearRules fb =
fb{ fbRules = mempty }
-- |
-- Removes any existing user targets from the flag.
-- This undoes the effect of methods like 'variationForUser'
clearUserTargets :: FlagBuilder -> FlagBuilder
clearUserTargets fb =
fb{ fbTargetMap = mempty }
-- |
-- Sets the flag to always return the specified variation value for all users.
--
-- The value may be of any type that implements 'Aeson.ToJSON'. This method changes the
-- flag to have only a single variation, which is this value, and to return the same
-- variation regardless of whether targeting is on or off. Any existing targets or rules
-- are removed.
valueForAllUsers :: Aeson.ToJSON value
=> value -- the desired value to be returned for all users
-> FlagBuilder
-> FlagBuilder
valueForAllUsers val fb =
fb & variations [Aeson.toJSON val]
& variationForAllUsers (0 :: VariationIndex)
-- |
-- Changes the allowable variation values for the flag.
--
-- The value may be of any JSON type, as defined by 'Aeson.Value'. For instance, a boolean flag
-- normally has [toJSON True, toJSON False]; a string-valued flag might have
-- [toJSON "red", toJSON "green"]; etc.
variations :: [Aeson.Value] -- ^ the desired variations
-> FlagBuilder
-> FlagBuilder
variations values fb =
fb{ fbVariations = values }
-- Should this actually use overloaded function names?
class Variation val where
-- |
-- Specifies the fallthrough variation. The fallthrough is the value
-- that is returned if targeting is on and the user was not matched by a more specific
-- target or rule.
--
-- If the flag was previously configured with other variations and the variation specified is a boolean,
-- this also changes it to a boolean flag.
fallthroughVariation :: val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
-> FlagBuilder
-> FlagBuilder
-- |
-- Specifies the off variation for a flag. This is the variation that is returned
-- whenever targeting is off.
--
-- If the flag was previously configured with other variations and the variation specified is a boolean,
-- this also changes it to a boolean flag.
offVariation :: val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
-> FlagBuilder
-> FlagBuilder
-- |
-- Sets the flag to always return the specified variation for all users.
--
-- The variation is specified, Targeting is switched on, and any existing targets or rules are removed.
-- The fallthrough variation is set to the specified value. The off variation is left unchanged.
--
-- If the flag was previously configured with other variations and the variation specified is a boolean,
-- this also changes it to a boolean flag.
variationForAllUsers :: val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
-> FlagBuilder
-> FlagBuilder
-- |
-- Sets the flag to return the specified variation for a specific user key when targeting
-- is on.
--
-- This has no effect when targeting is turned off for the flag.
--
-- If the flag was previously configured with other variations and the variation specified is a boolean,
-- this also changes it to a boolean flag.
variationForUser :: UserKey -- ^ a user key to target
-> val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
-> FlagBuilder
-> FlagBuilder
-- |
-- Finishes defining the rule, specifying the result as either a boolean
-- or a variation index.
--
-- If the flag was previously configured with other variations and the variation specified is a boolean,
-- this also changes it to a boolean flag.
thenReturn :: val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
-> FlagRuleBuilder
-> FlagBuilder
instance Variation Integer where
fallthroughVariation variationIndex fb =
fb{ fbFallthroughVariation = Just variationIndex }
offVariation variationIndex fb =
fb{ fbOffVariation = Just variationIndex }
variationForAllUsers variationIndex fb =
fb & on True
& clearRules
& clearUserTargets
& fallthroughVariation variationIndex
variationForUser userKey variationIndex fb =
fb{ fbTargetMap = Map.insert userKey variationIndex (fbTargetMap fb) }
thenReturn variationIndex ruleBuilder =
let fb = frbBaseBuilder ruleBuilder
in fb{ fbRules = FlagRule (frbClauses ruleBuilder) variationIndex : fbRules fb }
instance Variation Bool where
fallthroughVariation value fb =
fb & booleanFlag
& fallthroughVariation (variationForBoolean value)
offVariation value fb =
fb & booleanFlag
& offVariation (variationForBoolean value)
variationForAllUsers value fb =
fb & booleanFlag
& variationForAllUsers (variationForBoolean value)
variationForUser userKey value fb =
fb & booleanFlag
& variationForUser userKey (variationForBoolean value)
thenReturn value ruleBuilder =
ruleBuilder { frbBaseBuilder = booleanFlag $ frbBaseBuilder ruleBuilder }
& thenReturn (variationForBoolean value)
type UserAttribute = Text
-- |
-- Starts defining a flag rule, using the "is one of" operator.
--
-- For example, this creates a rule that returns @True@ if the name is \"Patsy\" or \"Edina\":
--
-- @
-- testData
-- & flag "flag"
-- & ifMatch "name" [toJSON \"Patsy\", toJSON \"Edina\"]
-- & thenReturn True
-- @
ifMatch :: UserAttribute -- ^ attribute the user attribute to match against
-> [Aeson.Value] -- ^ values to compare to
-> FlagBuilder
-> FlagRuleBuilder -- ^ call 'thenReturn' to finish the rule, or add more tests with 'andMatch' or 'andNotMatch'
ifMatch userAttribute values fb =
newFlagRuleBuilder fb
& andMatch userAttribute values
-- |
-- Starts defining a flag rule, using the "is not one of" operator.
--
-- For example, this creates a rule that returns @True@ if the name is neither \"Saffron\" nor \"Bubble\"
--
-- @
-- testData
-- & flag "flag"
-- & ifNotMatch "name" [toJSON \"Saffron\", toJSON \"Bubble\"]
-- & thenReturn True
-- @
ifNotMatch :: UserAttribute -- ^ attribute the user attribute to match against
-> [Aeson.Value] -- ^ values to compare to
-> FlagBuilder
-> FlagRuleBuilder -- ^ call 'thenReturn' to finish the rule, or add more tests with 'andMatch' or 'andNotMatch'
ifNotMatch userAttribute values fb =
newFlagRuleBuilder fb
& andNotMatch userAttribute values
data Clause = Clause
{ clauseAttribute :: UserAttribute
, clauseValues :: [Aeson.Value]
, clauseNegate :: Bool
} deriving (Show)
data FlagRule = FlagRule
{ frClauses :: [Clause]
, frVariation :: VariationIndex
} deriving (Show)
convertFlagRule :: Integer -> FlagRule -> F.Rule
convertFlagRule idx flagRule =
F.Rule
{ F.id = T.pack $ "rule" <> show idx
, F.variationOrRollout = F.VariationOrRollout (Just $ frVariation flagRule) Nothing
, F.clauses = fmap convertClause (frClauses flagRule)
, F.trackEvents = False
}
convertClause :: Clause -> F.Clause
convertClause clause =
F.Clause
{ F.attribute = clauseAttribute clause
, F.negate = clauseNegate clause
, F.values = clauseValues clause
, F.op = Op.OpIn
}
-- |
-- A builder for feature flag rules to be used with 'FlagBuilder'.
--
-- In the LaunchDarkly model, a flag can have any number of rules, and a rule can have any number of
-- clauses. A clause is an individual test such as \"name is \'X\'\". A rule matches a user if all of the
-- rule's clauses match the user.
--
-- To start defining a rule, use one of the matching functions such as 'ifMatch' or 'ifNotMatch'.
-- This defines the first clause for the rule.
-- Optionally, you may add more clauses with the rule builder functions such as 'andMatch' and 'andNotMatch'.
-- Finally, call 'thenReturn' to finish defining the rule.
data FlagRuleBuilder = FlagRuleBuilder
{ frbClauses :: [Clause]
, frbBaseBuilder :: FlagBuilder
} deriving (Show)
newFlagRuleBuilder :: FlagBuilder -> FlagRuleBuilder
newFlagRuleBuilder baseBuilder =
FlagRuleBuilder
{ frbClauses = mempty
, frbBaseBuilder = baseBuilder
}
-- |
-- Adds another clause, using the "is one of" operator.
--
-- For example, this creates a rule that returns @True@ if the name is \"Patsy\" and the
-- country is \"gb\":
--
-- @
-- testData
-- & flag "flag"
-- & ifMatch "name" [toJSON \"Patsy\"]
-- & andMatch "country" [toJSON \"gb\"]
-- & thenReturn True
-- @
andMatch :: UserAttribute -- ^ the user attribute to match against
-> [Aeson.Value] -- ^ values to compare to
-> FlagRuleBuilder
-> FlagRuleBuilder
andMatch userAttribute values ruleBuilder =
ruleBuilder{ frbClauses = Clause userAttribute values False : frbClauses ruleBuilder }
-- |
-- Adds another clause, using the "is not one of" operator.
--
-- For example, this creates a rule that returns @True@ if the name is \"Patsy\" and the
-- country is not \"gb\":
--
-- @
-- testData
-- & flag "flag"
-- & ifMatch "name" [toJSON \"Patsy\"]
-- & andNotMatch "country" [toJSON \"gb\"]
-- & thenReturn True
-- @
andNotMatch :: UserAttribute -- ^ the user attribute to match against
-> [Aeson.Value] -- ^ values to compare to
-> FlagRuleBuilder
-> FlagRuleBuilder
andNotMatch userAttribute values ruleBuilder =
ruleBuilder{ frbClauses = Clause userAttribute values True : frbClauses ruleBuilder }