packages feed

launchdarkly-server-sdk-4.0.0: src/LaunchDarkly/Server/Features.hs

module LaunchDarkly.Server.Features where

import Control.Lens (element, (^?))
import Control.Monad (mzero)
import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, withObject, (.!=), (.:), (.:?), (.=))
import Data.Generics.Product (getField)
import Data.HashSet (HashSet)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import GHC.Generics (Generic)
import GHC.Natural (Natural)

import LaunchDarkly.Server.Details (EvaluationReason (..))
import qualified LaunchDarkly.Server.Details as D
import LaunchDarkly.Server.Operators (Op)
import LaunchDarkly.Server.Reference (Reference, makeLiteral, makeReference)

data Target = Target
    { values :: !(HashSet Text)
    , variation :: !Integer
    , contextKind :: Text
    }
    deriving (Generic, ToJSON, Show, Eq)

instance FromJSON Target where
    parseJSON = withObject "Target" $ \t ->
        Target
            <$> t .: "values"
            <*> t .: "variation"
            <*> t .:? "contextKind" .!= "user"

data Rule = Rule
    { id :: !Text
    , clauses :: ![Clause]
    , variationOrRollout :: !VariationOrRollout
    , trackEvents :: !Bool
    }
    deriving (Generic, Show, Eq)

instance FromJSON Rule where
    parseJSON = withObject "Rule" $ \o -> do
        id <- o .:? "id"
        clauses <- o .: "clauses"
        variation <- o .:? "variation"
        rollout <- o .:? "rollout"
        trackEvents <- o .: "trackEvents"
        pure
            Rule
                { id = fromMaybe "" id
                , clauses = clauses
                , variationOrRollout =
                    VariationOrRollout
                        { variation = variation
                        , rollout = rollout
                        }
                , trackEvents = trackEvents
                }

instance ToJSON Rule where
    toJSON rule =
        object
            [ "id" .= getField @"id" rule
            , "clauses" .= getField @"clauses" rule
            , "trackEvents" .= getField @"trackEvents" rule
            , "variation" .= getField @"variation" (getField @"variationOrRollout" rule)
            , "rollout" .= getField @"rollout" (getField @"variationOrRollout" rule)
            ]

data WeightedVariation = WeightedVariation
    { variation :: !Integer
    , weight :: !Float
    , untracked :: !Bool
    }
    deriving (Generic, ToJSON, Show, Eq)

instance FromJSON WeightedVariation where
    parseJSON = withObject "WeightedVariation" $ \o -> do
        variation <- o .: "variation"
        weight <- o .: "weight"
        untracked <- o .:? "untracked" .!= False
        pure WeightedVariation {..}

data RolloutKind = RolloutKindExperiment | RolloutKindRollout
    deriving (Eq, Show)

instance ToJSON RolloutKind where
    toJSON x = String $ case x of
        RolloutKindExperiment -> "experiment"
        RolloutKindRollout -> "rollout"

instance FromJSON RolloutKind where
    parseJSON x = case x of
        (String "experiment") -> pure RolloutKindExperiment
        (String "rollout") -> pure RolloutKindRollout
        _ -> mzero

data Rollout = Rollout
    { variations :: ![WeightedVariation]
    , bucketBy :: !(Maybe Text)
    , kind :: !RolloutKind
    , contextKind :: !(Maybe Text)
    , seed :: !(Maybe Int)
    }
    deriving (Generic, ToJSON, Show, Eq)

instance FromJSON Rollout where
    parseJSON = withObject "rollout" $ \o -> do
        variations <- o .: "variations"
        bucketBy <- o .:? "bucketBy"
        kind <- o .:? "kind" .!= RolloutKindRollout
        contextKind <- o .:? "contextKind"
        seed <- o .:? "seed"
        pure Rollout {..}

data VariationOrRollout = VariationOrRollout
    { variation :: !(Maybe Integer)
    , rollout :: !(Maybe Rollout)
    }
    deriving (Generic, FromJSON, ToJSON, Show, Eq)

data ClientSideAvailability = ClientSideAvailability
    { usingEnvironmentId :: !Bool
    , usingMobileKey :: !Bool
    , explicit :: !Bool
    }
    deriving (Generic, Show, Eq)

instance FromJSON ClientSideAvailability where
    parseJSON = withObject "ClientSideAvailability" $ \obj ->
        ClientSideAvailability
            <$> obj .: "usingEnvironmentId"
            <*> obj .: "usingMobileKey"
            <*> pure True

instance ToJSON ClientSideAvailability where
    toJSON (ClientSideAvailability env mob _) =
        object ["usingEnvironmentId" .= env, "usingMobileKey" .= mob]

data Flag = Flag
    { key :: !Text
    , version :: !Natural
    , on :: !Bool
    , trackEvents :: !Bool
    , trackEventsFallthrough :: !Bool
    , deleted :: !Bool
    , prerequisites :: ![Prerequisite]
    , salt :: !Text
    , targets :: ![Target]
    , contextTargets :: ![Target]
    , rules :: ![Rule]
    , fallthrough :: !VariationOrRollout
    , offVariation :: !(Maybe Integer)
    , variations :: ![Value]
    , debugEventsUntilDate :: !(Maybe Natural)
    , clientSideAvailability :: !ClientSideAvailability
    }
    deriving (Generic, Show, Eq)

instance ToJSON Flag where
    toJSON flag =
        object $
            [ "key" .= getField @"key" flag
            , "version" .= getField @"version" flag
            , "on" .= getField @"on" flag
            , "trackEvents" .= getField @"trackEvents" flag
            , "trackEventsFallthrough" .= getField @"trackEventsFallthrough" flag
            , "deleted" .= getField @"deleted" flag
            , "prerequisites" .= getField @"prerequisites" flag
            , "salt" .= getField @"salt" flag
            , "targets" .= getField @"targets" flag
            , "contextTargets" .= getField @"contextTargets" flag
            , "rules" .= getField @"rules" flag
            , "fallthrough" .= getField @"fallthrough" flag
            , "offVariation" .= getField @"offVariation" flag
            , "variations" .= getField @"variations" flag
            , "debugEventsUntilDate" .= getField @"debugEventsUntilDate" flag
            , "clientSide" .= (getField @"usingEnvironmentId" $ getField @"clientSideAvailability" flag)
            ]
                <> case getField @"explicit" $ getField @"clientSideAvailability" flag of
                    True -> ["clientSideAvailability" .= getField @"clientSideAvailability" flag]
                    False -> []

instance FromJSON Flag where
    parseJSON = withObject "Flag" $ \obj -> do
        key <- obj .: "key"
        version <- obj .: "version"
        on <- obj .: "on"
        trackEvents <- obj .: "trackEvents"
        trackEventsFallthrough <- obj .: "trackEventsFallthrough"
        deleted <- obj .: "deleted"
        prerequisites <- obj .: "prerequisites"
        salt <- obj .: "salt"
        targets <- obj .: "targets"
        contextTargets <- obj .:? "contextTargets" .!= mempty
        rules <- obj .: "rules"
        fallthrough <- obj .: "fallthrough"
        offVariation <- obj .:? "offVariation"
        variations <- obj .: "variations"
        debugEventsUntilDate <- obj .:? "debugEventsUntilDate"
        clientSide <- obj .:? "clientSide" .!= False
        clientSideAvailability <- obj .:? "clientSideAvailability" .!= ClientSideAvailability clientSide True False
        pure Flag {..}

isClientSideOnlyFlag :: Flag -> Bool
isClientSideOnlyFlag flag = getField @"usingEnvironmentId" $ getField @"clientSideAvailability" flag

-- If the reason for the flag is in an experiment,
-- or if it's a fallthrough reason and the flag has trackEventsFallthrough
-- or if it's a rule match and the rule that matched has track events turned on
-- otherwise false
isInExperiment :: Flag -> EvaluationReason -> Bool
isInExperiment _ reason
    | D.isInExperiment reason = True
isInExperiment flag EvaluationReasonFallthrough {} = getField @"trackEventsFallthrough" flag
isInExperiment flag (EvaluationReasonRuleMatch ruleIndex _ _) =
    let index = fromIntegral ruleIndex
        rules = getField @"rules" flag
        rule = rules ^? element index
     in fromMaybe False $ fmap (getField @"trackEvents") rule
isInExperiment _ _ = False

data Prerequisite = Prerequisite
    { key :: !Text
    , variation :: !Integer
    }
    deriving (Generic, FromJSON, ToJSON, Show, Eq)

data SegmentRule = SegmentRule
    { id :: !Text
    , clauses :: ![Clause]
    , weight :: !(Maybe Float)
    , bucketBy :: !(Maybe Text)
    , rolloutContextKind :: !(Maybe Text)
    }
    deriving (Generic, ToJSON, Show, Eq)

instance FromJSON SegmentRule where
    parseJSON = withObject "SegmentRule" $ \o -> do
        id <- o .: "id"
        clauses <- o .: "clauses"
        weight <- o .:? "weight"
        bucketBy <- o .:? "bucketBy"
        rolloutContextKind <- o .:? "rolloutContextKind"
        return $ SegmentRule {..}

data Segment = Segment
    { key :: !Text
    , included :: !(HashSet Text)
    , includedContexts :: ![SegmentTarget]
    , excluded :: !(HashSet Text)
    , excludedContexts :: ![SegmentTarget]
    , salt :: !Text
    , rules :: ![SegmentRule]
    , version :: !Natural
    , deleted :: !Bool
    }
    deriving (Generic, FromJSON, ToJSON, Show, Eq)

data SegmentTarget = SegmentTarget
    { values :: !(HashSet Text)
    , contextKind :: !Text
    }
    deriving (Generic, FromJSON, ToJSON, Show, Eq)

data Clause = Clause
    { attribute :: !Reference
    , contextKind :: !Text
    , negate :: !Bool
    , op :: !Op
    , values :: ![Value]
    }
    deriving (Generic, ToJSON, Show, Eq)

instance FromJSON Clause where
    parseJSON = withObject "Clause" $ \o -> do
        attr <- o .: "attribute"
        kind <- o .:? "contextKind"
        negate <- o .: "negate"
        op <- o .: "op"
        values <- o .: "values"

        let contextKind = fromMaybe "user" kind
            attribute = case kind of Nothing -> makeLiteral (attr); _ -> makeReference (attr)
        return $ Clause {..}