packages feed

figment-0.2.0.0: src/Figment/Extract.hs

-- | Turning a loaded configuration into typed values.
module Figment.Extract
  ( extract
  , extractLossy
  , extractAt
  , extractWith
  , extractPartial
  , extractPartialWith
  , extractTotal
  , extractTotalWith
  , loadExtract
  , loadExtractOrThrow
  , decodeContext
  ) where

import Control.Exception (throwIO)
import Data.Functor.Identity (Identity)
import Data.Semigroup (sconcat)

import Figment.Core
import Figment.Decode
import Figment.Error
import Figment.Partial
import Figment.Path
import Figment.Value

-- | The context decoders run in for a loaded configuration.
decodeContext :: Loaded -> DecodeContext
decodeContext l = rootContext (loadedProfile l) (loadedSources l)

-- | Decode the selected profile.
extract :: FromConfig a => Loaded -> Either ConfigErrors a
extract = extractWith fromConfig

-- | Like 'extract', but accept @"8080"@ for numbers, @"yes"@ for
-- booleans, and scalars for strings (Rust: @extract_lossy@).
extractLossy :: FromConfig a => Loaded -> Either ConfigErrors a
extractLossy l = runDecoder fromConfig (decodeContext l) {ctxLossy = True} (selected l)

-- | Decode the selected profile with an explicit decoder.
extractWith :: Decoder a -> Loaded -> Either ConfigErrors a
extractWith d l = runDecoder d (decodeContext l) (selected l)

-- | Decode the value at a key path (Rust: @extract_inner@).
extractAt :: forall a. FromConfig a => Path -> Loaded -> Either ConfigErrors a
extractAt p l = case lookupPath p (selected l) of
  Just v -> runDecoder fromConfig (decodeContext l) {ctxPath = p} v
  Nothing -> case missingValue @a of
    Just d -> Right d
    Nothing -> Left (single (missingAt (decodeContext l) p))

-- | Decode a 'Partial' configuration, filling gaps from partial
-- defaults (provider values win). Reports every type error, or else
-- every missing required field.
extractPartial :: Partial c => c Maybe -> Loaded -> Either ConfigErrors (c Identity)
extractPartial = extractPartialWith defaultOptions

-- | 'extractPartial' with explicit generic t'Options' (e.g. 'snakeCase' field names).
extractPartialWith :: Partial c => Options -> c Maybe -> Loaded -> Either ConfigErrors (c Identity)
extractPartialWith o defs l = do
  found <- runDecoder (partialDecoder o) (decodeContext l) (selected l)
  case complete o (overlay defs found) of
    Right r -> Right r
    Left ps ->
      let errs = fmap (missingAt (decodeContext l)) ps
      in Left (sconcat (fmap single errs))

-- | With /total/ defaults a missing-field error is impossible by
-- construction: the type guarantees every field has a value.
extractTotal :: Partial c => c Identity -> Loaded -> Either ConfigErrors (c Identity)
extractTotal = extractTotalWith defaultOptions

-- | 'extractTotal' with explicit generic t'Options'.
extractTotalWith :: Partial c => Options -> c Identity -> Loaded -> Either ConfigErrors (c Identity)
extractTotalWith o d = extractPartialWith o (total d)

missingAt :: DecodeContext -> Path -> ConfigError
missingAt c p = case reverse (segments p) of
  (Key name : parentRev) -> missingFieldError c {ctxPath = fromSegments (reverse parentRev)} name
  _ -> atPath p (configError (MissingField (renderPath p)))

-- | 'load' then 'extract'.
loadExtract :: FromConfig a => Figment -> IO (Either ConfigErrors a)
loadExtract fig = (>>= extract) <$> load fig

-- | 'loadExtract', throwing t'ConfigErrors' (whose 'Control.Exception.displayException' is
-- the rendered report).
loadExtractOrThrow :: FromConfig a => Figment -> IO a
loadExtractOrThrow fig = loadExtract fig >>= either throwIO pure