packages feed

settei-0.2.0.0: src/Settei/Internal/Schema.hs

module Settei.Internal.Schema
  ( Condition (..),
    Presence (..),
    Requirement (..),
    Schema (..),
    SchemaSetting (..),
    combineSchema,
    conditionalSchema,
    emptySchema,
    mergeSensitivity,
    requestSchema,
  )
where

import Data.Generics.Labels ()
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Settei.Key (Key)
import Settei.Prelude
import Settei.Setting
  ( Sensitivity (..),
    Setting,
    settingDescription,
    settingKey,
    settingSensitivity,
  )

-- | Whether absence of a candidate is an error at runtime.
data Requirement = Required | Optional
  deriving stock (Generic, Eq, Ord, Show)

-- | Whether a declaration is evaluated on every execution path.
data Presence = Necessary | Conditional
  deriving stock (Generic, Eq, Ord, Show)

-- | Static metadata for one possible setting request.
data SchemaSetting = SchemaSetting
  { key :: !Key,
    description :: !Text,
    sensitivity :: !Sensitivity,
    declaredSensitivities :: !(Set Sensitivity),
    requirement :: !Requirement,
    presence :: !Presence
  }
  deriving stock (Generic, Eq, Show)

-- | A conservative relationship introduced by a selective branch.
data Condition = Condition
  { dependencies :: !(Set Key),
    settings :: !(Set Key)
  }
  deriving stock (Generic, Eq, Show)

-- | The complete static over-approximation of a configuration declaration.
data Schema = Schema
  { settings :: !(Map Key SchemaSetting),
    conditions :: ![Condition]
  }
  deriving stock (Generic, Eq, Show)

emptySchema :: Schema
emptySchema = Schema {settings = Map.empty, conditions = []}

requestSchema :: Requirement -> Setting a -> Schema
requestSchema requirement settingSpec =
  Schema
    { settings = Map.singleton key entry,
      conditions = []
    }
  where
    key = settingKey settingSpec
    entry =
      SchemaSetting
        { key,
          description = settingDescription settingSpec,
          sensitivity = settingSensitivity settingSpec,
          declaredSensitivities = Set.singleton (settingSensitivity settingSpec),
          requirement,
          presence = Necessary
        }

combineSchema :: Schema -> Schema -> Schema
combineSchema left right =
  Schema
    { settings =
        Map.unionWith mergeSetting (left ^. #settings) (right ^. #settings),
      conditions = left ^. #conditions <> right ^. #conditions
    }

conditionalSchema :: Schema -> Schema -> Schema
conditionalSchema selector branch =
  combined
    & #conditions
    %~ (<> newCondition)
  where
    branchSettings = branch ^. #settings & each . #presence .~ Conditional
    branchKeys = Map.keysSet branchSettings
    selectorKeys = Map.keysSet (selector ^. #settings)
    conditionalBranch = branch & #settings .~ branchSettings
    combined = combineSchema selector conditionalBranch
    newCondition
      | Set.null branchKeys = []
      | otherwise = [Condition {dependencies = selectorKeys, settings = branchKeys}]

mergeSetting :: SchemaSetting -> SchemaSetting -> SchemaSetting
mergeSetting left right =
  left
    & #sensitivity
    .~ mergeSensitivity (left ^. #sensitivity) (right ^. #sensitivity)
    & #declaredSensitivities
    .~ Set.union (left ^. #declaredSensitivities) (right ^. #declaredSensitivities)
    & #requirement
    .~ mergeRequirement (left ^. #requirement) (right ^. #requirement)
    & #presence
    .~ mergePresence (left ^. #presence) (right ^. #presence)

-- | Secret wins: a key declared secret anywhere is secret everywhere.
mergeSensitivity :: Sensitivity -> Sensitivity -> Sensitivity
mergeSensitivity Secret _ = Secret
mergeSensitivity _ Secret = Secret
mergeSensitivity Public Public = Public

mergeRequirement :: Requirement -> Requirement -> Requirement
mergeRequirement Required _ = Required
mergeRequirement _ Required = Required
mergeRequirement Optional Optional = Optional

mergePresence :: Presence -> Presence -> Presence
mergePresence Necessary _ = Necessary
mergePresence _ Necessary = Necessary
mergePresence Conditional Conditional = Conditional