module LaunchDarkly.Server.Operators ( Op(..) , getOperation ) where import Data.Maybe (fromMaybe, isJust) import Data.Either (fromRight) import Data.Text as T import Data.Text (Text, isPrefixOf, isInfixOf, isSuffixOf, unpack) import Data.Char (isDigit) import Data.Text.Encoding (encodeUtf8) import Data.Scientific (Scientific, toRealFloat) import Data.Aeson.Types (Value(..), FromJSON, ToJSON(..), withText, parseJSON) import Data.Time.ISO8601 (parseISO8601) import Data.Time.Clock (UTCTime) import Data.Time.Clock.POSIX (POSIXTime, posixSecondsToUTCTime) import Data.SemVer (Version, fromText, toText, metadata) import Control.Monad (liftM2) import Control.Lens ((.~)) import GHC.Generics (Generic) import Text.Regex.PCRE.Light (compileM, match) data Op = OpIn | OpEndsWith | OpStartsWith | OpMatches | OpContains | OpLessThan | OpLessThanOrEqual | OpGreaterThan | OpGreaterThanOrEqual | OpBefore | OpAfter | OpSemVerEqual | OpSemVerLessThan | OpSemVerGreaterThan | OpSegmentMatch | OpUnknown deriving (Generic, Show, Eq) instance FromJSON Op where parseJSON = withText "Op" $ \v -> case v of "in" -> pure OpIn "endsWith" -> pure OpEndsWith "startsWith" -> pure OpStartsWith "matches" -> pure OpMatches "contains" -> pure OpContains "lessThan" -> pure OpLessThan "lessThanOrEqual" -> pure OpLessThanOrEqual "greaterThan" -> pure OpGreaterThan "greaterThanOrEqual" -> pure OpGreaterThanOrEqual "before" -> pure OpBefore "after" -> pure OpAfter "semVerEqual" -> pure OpSemVerEqual "semVerLessThan" -> pure OpSemVerLessThan "semVerGreaterThan" -> pure OpSemVerGreaterThan "segmentMatch" -> pure OpSegmentMatch _ -> pure OpUnknown instance ToJSON Op where toJSON op = String $ case op of OpIn -> "in" OpEndsWith -> "endsWith" OpStartsWith -> "startsWith" OpMatches -> "matches" OpContains -> "contains" OpLessThan -> "lessThan" OpLessThanOrEqual -> "lessThanOrEqual" OpGreaterThan -> "greaterThan" OpGreaterThanOrEqual -> "greaterThanOrEqual" OpBefore -> "before" OpAfter -> "after" OpSemVerEqual -> "semVerEqual" OpSemVerLessThan -> "semVerLessThan" OpSemVerGreaterThan -> "semVerGreaterThan" OpSegmentMatch -> "segmentMatch" OpUnknown -> "unknown" checkString :: (Text -> Text -> Bool) -> Value -> Value -> Bool checkString op (String x) (String y) = op x y checkString _ _ _ = False checkNumber :: (Scientific -> Scientific -> Bool) -> Value -> Value -> Bool checkNumber op (Number x) (Number y) = op x y checkNumber _ _ _ = False doubleToPOSIXTime :: Double -> POSIXTime doubleToPOSIXTime = realToFrac parseTime :: Value -> Maybe UTCTime parseTime (Number x) = Just $ posixSecondsToUTCTime $ doubleToPOSIXTime $ (toRealFloat x) / 1000 parseTime (String x) = parseISO8601 $ unpack x parseTime _ = Nothing compareTime :: (UTCTime -> UTCTime -> Bool) -> Value -> Value -> Bool compareTime op x y = fromMaybe False $ liftM2 op (parseTime x) (parseTime y) padSemVer :: Text -> Text padSemVer text = T.concat [l, padding, r] where (l, r) = T.span (\c -> isDigit c || c == '.') text dots = T.count "." l padding = if dots < 2 then T.replicate (2 - dots) ".0" else "" parseSemVer :: Text -> Either String Version parseSemVer raw = fmap (metadata .~ []) (fromText $ padSemVer raw) >>= \x -> if T.isPrefixOf (toText x) (padSemVer raw) then Right x else Left "mismatch" where compareSemVer :: (Version -> Version -> Bool) -> Text -> Text -> Bool compareSemVer op x y = fromRight False $ liftM2 op (parseSemVer x) (parseSemVer y) matches :: Text -> Text -> Bool matches text pattern = case compileM (encodeUtf8 pattern) [] of Left _ -> False Right compiled -> isJust $ match compiled (encodeUtf8 text) [] getOperation :: Op -> (Value -> Value -> Bool) getOperation op = case op of OpIn -> (==) OpEndsWith -> checkString (flip isSuffixOf) OpStartsWith -> checkString (flip isPrefixOf) OpContains -> checkString (flip isInfixOf) OpMatches -> checkString matches OpLessThan -> checkNumber (<) OpLessThanOrEqual -> checkNumber (<=) OpGreaterThan -> checkNumber (>) OpGreaterThanOrEqual -> checkNumber (>=) OpBefore -> compareTime (<) OpAfter -> compareTime (>) OpSemVerEqual -> checkString $ compareSemVer (==) OpSemVerLessThan -> checkString $ compareSemVer (<) OpSemVerGreaterThan -> checkString $ compareSemVer (>) OpSegmentMatch -> error "cannot get operation for OpSegmentMatch" OpUnknown -> const $ const False