module LaunchDarkly.Server.Evaluate where
import Control.Monad (mzero, msum)
import Control.Monad.Extra (ifM, anyM, allM, firstJustM)
import Crypto.Hash.SHA1 (hash)
import Data.Scientific (Scientific, floatingOrInteger)
import Data.Either (either, fromLeft)
import Data.Aeson.Types (Value(..))
import Data.Maybe (maybe, fromJust, isJust, fromMaybe)
import Data.Text (Text)
import Data.Generics.Product (getField)
import Data.List (genericIndex, null, find)
import qualified Data.Vector as V
import qualified Data.Text as T
import qualified Data.ByteString as B
import qualified Data.ByteString.Base16 as B16
import Data.Text.Encoding (encodeUtf8)
import GHC.Natural (Natural, naturalToInt)
import Data.Word (Word8)
import Data.ByteString (ByteString)
import Data.IORef (readIORef)
import LaunchDarkly.Server.Client.Internal (ClientI, Status(Initialized))
import LaunchDarkly.Server.User.Internal (UserI, valueOf)
import LaunchDarkly.Server.Features (Flag, Segment, Prerequisite, SegmentRule, Clause, VariationOrRollout, Rule)
import LaunchDarkly.Server.Store.Internal (LaunchDarklyStoreRead, getFlagC, getSegmentC)
import LaunchDarkly.Server.Operators (Op(OpSegmentMatch), getOperation)
import LaunchDarkly.Server.Events (EvalEvent, newUnknownFlagEvent, newSuccessfulEvalEvent, processEvalEvents)
import LaunchDarkly.Server.Details (EvaluationDetail(..), EvaluationReason(..), EvalErrorKind(..))
setFallback :: EvaluationDetail Value -> Value -> EvaluationDetail Value
setFallback detail fallback = case getField @"variationIndex" detail of
Nothing -> detail { value = fallback }; _ -> detail
setValue :: EvaluationDetail Value -> a -> EvaluationDetail a
setValue x v = x { value = v }
isError :: EvaluationReason -> Bool
isError reason = case reason of (EvaluationReasonError _) -> True; _ -> False
evaluateTyped :: ClientI -> Text -> UserI -> a -> (a -> Value) -> Bool -> (Value -> Maybe a) -> IO (EvaluationDetail a)
evaluateTyped client key user fallback wrap includeReason convert = readIORef (getField @"status" client) >>= \status -> if status /= Initialized
then pure $ EvaluationDetail fallback Nothing $ EvaluationReasonError EvalErrorClientNotReady
else evaluateInternalClient client key user (wrap fallback) includeReason >>= \r -> pure $ maybe
(EvaluationDetail fallback Nothing $ if isError (getField @"reason" r)
then (getField @"reason" r) else EvaluationReasonError EvalErrorWrongType)
(setValue r) (convert $ getField @"value" r)
evaluateInternalClient :: ClientI -> Text -> UserI -> Value -> Bool -> IO (EvaluationDetail Value)
evaluateInternalClient client key user fallback includeReason = do
(reason, unknown, events) <- getFlagC (getField @"store" client) key >>= \case
Left err -> do
let event = newUnknownFlagEvent key fallback (EvaluationReasonError $ EvalErrorExternalStore err)
pure (errorDetail $ EvalErrorExternalStore err, True, pure event)
Right Nothing -> do
let event = newUnknownFlagEvent key fallback (EvaluationReasonError EvalErrorFlagNotFound)
pure (errorDetail EvalErrorFlagNotFound, True, pure event)
Right (Just flag) -> do
(reason, events) <- case getField @"key" user of
Nothing -> pure (errorDetail EvalErrorUserNotSpecified, [])
Just _ -> evaluateDetail flag user $ getField @"store" client
let reason' = setFallback reason fallback
pure (reason', False, flip (:) events $ newSuccessfulEvalEvent flag (getField @"variationIndex" reason')
(getField @"value" reason') (Just fallback) (getField @"reason" reason') Nothing)
processEvalEvents (getField @"config" client) (getField @"events" client) user includeReason events unknown
pure reason
getOffValue :: Flag -> EvaluationReason -> EvaluationDetail Value
getOffValue flag reason = case getField @"offVariation" flag of
Just offVariation -> getVariation flag offVariation reason
Nothing -> EvaluationDetail { value = Null, variationIndex = mzero, reason = reason }
getVariation :: Flag -> Natural -> EvaluationReason -> EvaluationDetail Value
getVariation flag index reason = let variations = getField @"variations" flag in
if naturalToInt index >= length variations
then EvaluationDetail { value = Null, variationIndex = mzero, reason = EvaluationReasonError EvalErrorKindMalformedFlag }
else EvaluationDetail { value = genericIndex variations index, variationIndex = pure index, reason = reason }
evaluateDetail :: (Monad m, LaunchDarklyStoreRead store m) => Flag -> UserI -> store
-> m (EvaluationDetail Value, [EvalEvent])
evaluateDetail flag user store = if getField @"on" flag
then checkPrerequisites flag user store >>= \case
(Nothing, events) -> evaluateInternal flag user store >>= (\x -> pure (x, events))
(Just reason, events) -> pure (getOffValue flag reason, events)
else pure (getOffValue flag EvaluationReasonOff, [])
status :: Prerequisite -> EvaluationDetail a -> Flag -> Bool
status prereq result prereqFlag = getField @"on" prereqFlag && (getField @"variationIndex" result) ==
pure (getField @"variation" prereq)
checkPrerequisite :: (Monad m, LaunchDarklyStoreRead store m) => store -> UserI -> Flag -> Prerequisite
-> m (Maybe EvaluationReason, [EvalEvent])
checkPrerequisite store user flag prereq = getFlagC store (getField @"key" prereq) >>= \case
Left err -> pure (pure $ EvaluationReasonError $ EvalErrorExternalStore err, [])
Right Nothing -> pure (pure $ EvaluationReasonPrerequisiteFailed (getField @"key" prereq), [])
Right (Just prereqFlag) -> evaluateDetail prereqFlag user store >>= \(r, events) -> let
event = newSuccessfulEvalEvent prereqFlag (getField @"variationIndex" r) (getField @"value" r) Nothing
(getField @"reason" r) (Just $ getField @"key" flag)
in if status prereq r prereqFlag then pure (Nothing, event : events) else
pure (pure $ EvaluationReasonPrerequisiteFailed (getField @"key" prereq), event : events)
sequenceUntil :: Monad m => (a -> Bool) -> [m a] -> m [a]
sequenceUntil _ [] = return []
sequenceUntil p (m:ms) = m >>= \a -> if p a then return [a] else
sequenceUntil p ms >>= \as -> return (a:as)
checkPrerequisites :: (Monad m, LaunchDarklyStoreRead store m) => Flag -> UserI -> store
-> m (Maybe EvaluationReason, [EvalEvent])
checkPrerequisites flag user store = let p = getField @"prerequisites" flag in if null p then pure (Nothing, []) else do
evals <- sequenceUntil (isJust . fst) $ map (checkPrerequisite store user flag) p
pure (msum $ map fst evals, concatMap snd evals)
evaluateInternal :: (Monad m, LaunchDarklyStoreRead store m) => Flag -> UserI -> store -> m (EvaluationDetail Value)
evaluateInternal flag user store = result where
checkTarget target = if elem (getField @"key" user) (Just <$> getField @"values" target)
then Just $ getVariation flag (getField @"variation" target) EvaluationReasonTargetMatch else Nothing
checkRule (ruleIndex, rule) = ifM (ruleMatchesUser rule user store)
(pure $ Just $ getValueForVariationOrRollout flag (getField @"variationOrRollout" rule) user
EvaluationReasonRuleMatch { ruleIndex = ruleIndex, ruleId = getField @"id" rule })
(pure Nothing)
fallthrough = getValueForVariationOrRollout flag (getField @"fallthrough" flag) user EvaluationReasonFallthrough
result = let
ruleMatch = checkRule <$> zip [0..] (getField @"rules" flag)
targetMatch = return . checkTarget <$> getField @"targets" flag
in fromMaybe fallthrough <$> firstJustM Prelude.id (ruleMatch ++ targetMatch)
errorDetail :: EvalErrorKind -> EvaluationDetail Value
errorDetail kind = EvaluationDetail { value = Null, variationIndex = mzero, reason = EvaluationReasonError kind }
getValueForVariationOrRollout :: Flag -> VariationOrRollout -> UserI -> EvaluationReason -> EvaluationDetail Value
getValueForVariationOrRollout flag vr user reason =
case variationIndexForUser vr user (getField @"key" flag) (getField @"salt" flag) of
Nothing -> errorDetail EvalErrorKindMalformedFlag
Just x -> getVariation flag x reason
ruleMatchesUser :: Monad m => LaunchDarklyStoreRead store m => Rule -> UserI -> store -> m Bool
ruleMatchesUser rule user store =
allM (\clause -> clauseMatchesUser store clause user) (getField @"clauses" rule)
variationIndexForUser :: VariationOrRollout -> UserI -> Text -> Text -> Maybe Natural
variationIndexForUser vor user key salt
| (Just variation) <- getField @"variation" vor = pure variation
| (Just rollout) <- getField @"rollout" vor = let
variations = getField @"variations" rollout
bucket = bucketUser user key (fromMaybe "key" $ getField @"bucketBy" rollout) salt
c acc i = acc >>= \acc -> let t = acc + ((getField @"weight" i) / 100000.0) in
if bucket < t then Left (getField @"variation" i) else Right t
in if null variations then Nothing else pure $ fromLeft (getField @"variation" $ last variations) $
foldl c (Right (0.0 :: Float)) variations
| otherwise = Nothing
hexCharToNumber :: Word8 -> Maybe Natural
hexCharToNumber w = fmap fromIntegral $ if
| 48 <= w && w <= 57 -> pure $ w - 48
| 65 <= w && w <= 70 -> pure $ w - 55
| 97 <= w && w <= 102 -> pure $ w - 87
| otherwise -> Nothing
hexStringToNumber :: ByteString -> Maybe Natural
hexStringToNumber bytes = B.foldl' step (Just 0) bytes where
step acc x = acc >>= \acc' -> hexCharToNumber x >>= pure . (+) (acc' * 16)
bucketUser :: UserI -> Text -> Text -> Text -> Float
bucketUser user key attribute salt = fromMaybe 0 $ do
i <- valueOf user attribute >>= bucketableStringValue >>= \x -> pure $ B.take 15 $ B16.encode $ hash $ encodeUtf8 $
T.concat [key, ".", salt, ".", x, maybe "" (T.append ".") $ getField @"secondary" user]
pure $ ((fromIntegral $ fromJust $ hexStringToNumber i) :: Float) / (0xFFFFFFFFFFFFFFF)
floatingOrInteger' :: Scientific -> Either Double Integer
floatingOrInteger' = floatingOrInteger
bucketableStringValue :: Value -> Maybe Text
bucketableStringValue (String x) = pure x
bucketableStringValue (Number s) = either (const Nothing) (pure . T.pack . show) (floatingOrInteger' s)
bucketableStringValue _ = Nothing
maybeNegate :: Clause -> Bool -> Bool
maybeNegate clause value = if getField @"negate" clause then not value else value
matchAny :: (Value -> Value -> Bool) -> Value -> [Value] -> Bool
matchAny op value = any (op value)
clauseMatchesUserNoSegments :: Clause -> UserI -> Bool
clauseMatchesUserNoSegments clause user = case valueOf user $ getField @"attribute" clause of
Nothing -> False
Just (Array a) -> maybeNegate clause $ V.any (\x -> matchAny f x v) a
Just x -> maybeNegate clause $ matchAny f x v
where
f = getOperation $ getField @"op" clause
v = getField @"values" clause
clauseMatchesUser :: (Monad m, LaunchDarklyStoreRead store m) => store -> Clause -> UserI -> m Bool
clauseMatchesUser store clause user
| getField @"op" clause == OpSegmentMatch = do
let values = [ x | String x <- getField @"values" clause]
x <- anyM (\k -> getSegmentC store k >>= pure . checkSegment) values
pure $ maybeNegate clause x
| otherwise = pure $ clauseMatchesUserNoSegments clause user
where
checkSegment :: Either Text (Maybe Segment) -> Bool
checkSegment (Right (Just segment)) = segmentContainsUser segment user
checkSegment _ = False
segmentRuleMatchesUser :: SegmentRule -> UserI -> Text -> Text -> Bool
segmentRuleMatchesUser rule user key salt = (&&)
(all (flip clauseMatchesUserNoSegments user) (getField @"clauses" rule))
(flip (maybe True) (getField @"weight" rule) $ \weight ->
bucketUser user key (fromMaybe "key" $ getField @"bucketBy" rule) salt < weight / 100000.0)
segmentContainsUser :: Segment -> UserI -> Bool
segmentContainsUser segment user
| Nothing <- getField @"key" user = False
| elem (fromJust $ getField @"key" user) (getField @"included" segment) = True
| elem (fromJust $ getField @"key" user) (getField @"excluded" segment) = False
| Just _ <- find
(\r -> segmentRuleMatchesUser r user (getField @"key" segment) (getField @"salt" segment))
(getField @"rules" segment) = True
| otherwise = False