packages feed

settei-0.1.0.0: src/Settei/Source.hs

{-# LANGUAGE ImportQualifiedPost #-}

-- |
-- Module: Settei.Source
-- Description: Ordered, hierarchical configuration inputs.
module Settei.Source
  ( Source,
    annotateSource,
    annotateSourceAt,
    locateSource,
    lookupSource,
    source,
    sourceAnnotations,
    sourceKind,
    sourceLeaves,
    sourceName,
  )
where

import Data.Generics.Labels ()
import Data.List.NonEmpty qualified as NonEmpty
import Data.Map.Strict qualified as Map
import Settei.Error (RawShape (..), StructuralError (..))
import Settei.Key (Key, keySegments, mkKey)
import Settei.Origin (Origin (..), SourceKind, SourceLocation)
import Settei.Prelude
import Settei.Provenance (Candidate, candidate)
import Settei.Value (RawValue (..))

-- | A reusable source tree. Precedence is supplied by its position in @[Source]@.
--
-- There is deliberately no 'Show' instance because 'root' may contain secrets.
data Source = Source
  { name :: !Text,
    kind :: !SourceKind,
    root :: !RawValue,
    locationAt :: !(Key -> Maybe SourceLocation),
    annotationsAt :: !(Key -> Map Text Text),
    annotations :: !(Map Text Text)
  }
  deriving stock (Generic)

-- | Construct a source with no retained locations or annotations.
source :: Text -> SourceKind -> RawValue -> Source
source name kind root =
  Source
    { name,
      kind,
      root,
      locationAt = const Nothing,
      annotationsAt = const Map.empty,
      annotations = Map.empty
    }

-- | Attach adapter-specific descriptive metadata. It never changes precedence.
annotateSource :: Map Text Text -> Source -> Source
annotateSource annotations sourceValue = sourceValue & #annotations .~ annotations

-- | Attach descriptive metadata for individual keys.
--
-- Later calls compose with earlier hooks. New per-key entries take precedence over
-- earlier per-key and source-wide entries with the same annotation name.
annotateSourceAt :: (Key -> Map Text Text) -> Source -> Source
annotateSourceAt additions sourceValue =
  sourceValue
    & #annotationsAt
    %~ \existing key -> additions key <> existing key

-- | Attach exact-key locations retained by an adapter parser.
locateSource :: (Key -> Maybe SourceLocation) -> Source -> Source
locateSource locationAt sourceValue = sourceValue & #locationAt .~ locationAt

-- | Return a source's stable descriptive name.
sourceName :: Source -> Text
sourceName value = value ^. #name

-- | Return a source's format-independent category.
sourceKind :: Source -> SourceKind
sourceKind value = value ^. #kind

-- | Return descriptive annotations shared by every candidate from the source.
sourceAnnotations :: Source -> Map Text Text
sourceAnnotations value = value ^. #annotations

-- | Look up one exact structural key.
--
-- A scalar or array is a valid value at the final segment. Traversing through one is a
-- structural error; dotted textual prefix tricks are never used.
lookupSource :: Key -> Source -> Either StructuralError (Maybe Candidate)
lookupSource target sourceValue =
  walk [] (NonEmpty.toList (keySegments target)) (sourceValue ^. #root)
  where
    walk _ [] rawValue = Right (Just (candidate rawValue (originFor target sourceValue)))
    walk prefix (segment : rest) rawValue = case rawValue of
      RawObject object ->
        case Map.lookup segment object of
          Nothing -> Right Nothing
          Just child -> walk (prefix <> [segment]) rest child
      RawNull -> Left (blocked prefix NullShape)
      RawArray _ -> Left (blocked prefix ArrayShape)
      RawText _ -> Left (blocked prefix ScalarShape)
      RawBool _ -> Left (blocked prefix ScalarShape)
      RawNumber _ -> Left (blocked prefix ScalarShape)

    blocked prefix shape =
      StructuralError
        { key = target,
          blockedAt = prefixKey target prefix,
          origin = originFor (prefixKey target prefix) sourceValue,
          shape
        }

-- | Enumerate addressable leaves in key order for unknown-key diagnostics.
--
-- Arrays and scalars are whole leaves. Empty objects contain no leaves.
sourceLeaves :: Source -> [(Key, Candidate)]
sourceLeaves sourceValue = go [] (sourceValue ^. #root)
  where
    go prefix = \case
      RawObject object ->
        concatMap
          (\(segment, rawValue) -> go (prefix <> [segment]) rawValue)
          (Map.toAscList object)
      rawValue -> case NonEmpty.nonEmpty prefix of
        Nothing -> []
        Just segments -> case mkKey segments of
          Left _ -> []
          Right key -> [(key, candidate rawValue (originFor key sourceValue))]

originFor :: Key -> Source -> Origin
originFor key sourceValue =
  Origin
    { kind = sourceValue ^. #kind,
      name = sourceValue ^. #name,
      key,
      location = (sourceValue ^. #locationAt) key,
      annotations =
        (sourceValue ^. #annotationsAt) key
          <> sourceValue ^. #annotations
    }

prefixKey :: Key -> [Text] -> Key
prefixKey target prefix =
  case NonEmpty.nonEmpty prefix >>= either (const Nothing) Just . mkKey of
    Just key -> key
    Nothing -> target