packages feed

kansoku-api-0.2.0.0: src/Kansoku/Cohort/Types.hs

-- | Saved cohort definitions and management payloads.
module Kansoku.Cohort.Types where

import Data.Aeson qualified as A
import Data.Maybe (catMaybes)
import Data.Text qualified as T
import Data.UUID (UUID)
import Kansoku.Metrics.Filter (MetricFilter)
import Kansoku.Prelude

data CohortCondition = CohortCondition
  {did :: !(Maybe Text), didNot :: !(Maybe Text), atLeast :: !(Maybe Int), atMost :: !(Maybe Int), withinDays :: !Int, filters :: ![MetricFilter]}
  deriving stock (Generic, Eq, Show)
  deriving anyclass (ToJSON)

instance FromJSON CohortCondition where
  parseJSON = A.withObject "cohort condition" $ \o ->
    CohortCondition
      <$> o A..:? "did"
      <*> o A..:? "didNot"
      <*> o A..:? "atLeast"
      <*> o A..:? "atMost"
      <*> o A..: "withinDays"
      <*> o A..:? "filters" A..!= []

data CohortDefinition = CohortDefinition
  {name :: !Text, kind :: !Text, conditions :: ![CohortCondition], traits :: ![MetricFilter]}
  deriving stock (Generic, Eq, Show)
  deriving anyclass (ToJSON)

instance FromJSON CohortDefinition where
  parseJSON = A.withObject "cohort definition" $ \o ->
    CohortDefinition
      <$> o A..: "name"
      <*> o A..: "kind"
      <*> o A..:? "conditions" A..!= []
      <*> o A..:? "traits" A..!= []

data Cohort = Cohort {id :: !UUID, definition :: !CohortDefinition, createdAt :: !UTCTime, updatedAt :: !UTCTime}
  deriving stock (Generic, Eq, Show)
  deriving anyclass (ToJSON, FromJSON)

newtype CohortMembers = CohortMembers {userIds :: [Text]}
  deriving stock (Generic, Eq, Show)
  deriving anyclass (ToJSON, FromJSON)

newtype CohortSize = CohortSize {size :: Integer}
  deriving stock (Generic, Eq, Show)
  deriving anyclass (ToJSON, FromJSON)

validateDefinition :: CohortDefinition -> Either Text ()
validateDefinition CohortDefinition {name, kind, conditions, traits} = do
  require (not (T.null (T.strip name)) && T.length name <= 200) "name must contain 1..200 characters"
  require (kind `elem` ["behavioural", "static"]) "kind must be behavioural or static"
  require (length conditions <= 4) "at most four conditions are supported"
  require (if kind == "static" then null conditions && null traits else not (null conditions && null traits)) "static cohorts have no conditions; behavioural cohorts require conditions or traits"
  mapM_ condition conditions
  where
    require True _ = Right ()
    require False message = Left message
    condition CohortCondition {did, didNot, atLeast, atMost, withinDays} = do
      require ((isJust did /= isJust didNot) && all (not . T.null) (catMaybes [did, didNot])) "exactly one of did or didNot is required"
      require (withinDays >= 1 && withinDays <= 365) "withinDays must be in 1..365"
      require (all (>= 0) (catMaybes [atLeast, atMost]) && maybe True (\lo -> maybe True (>= lo) atMost) atLeast) "occurrence bounds must be nonnegative and ordered"