packages feed

freckle-app-1.0.2.3: library/Freckle/App/Env/Internal.hs

{-# LANGUAGE TupleSections #-}

-- | Internal Env machinery exposed for testing
module Freckle.App.Env.Internal
  ( Error(..)
  , Parser(..)
  , bindParser
  , Reader(..)
  , Mod(..)
  , Var(..)
  , varParser
  ) where

import Freckle.App.Prelude

import Control.Applicative

-- | Environment parsing errors
data Error
  = UnsetError
  -- ^ A variable was not found, and no default was specified
  | InvalidError String
  -- ^ A variable was found, but it failed to parse
  deriving stock (Eq, Show)

isUnsetError :: Error -> Bool
isUnsetError UnsetError = True
isUnsetError (InvalidError _) = False

-- | Parse an Environment
--
-- Errors are accumulated into tuples mapping name to error.
--
newtype Parser a = Parser
  { unParser :: [(String, String)] -> Either [(String, Error)] a
  }
  deriving stock Functor

instance Applicative Parser where
  pure a = Parser . const $ Right a
  Parser f <*> Parser a = Parser $ \env -> case (f env, a env) of
    (Right f', Right a') -> Right $ f' a'

    -- Accumulate errors
    (Left e1, Left e2) -> Left $ e1 ++ e2
    (Left e, _) -> Left e
    (_, Left e) -> Left e

instance Alternative Parser where
  empty = Parser $ const $ Left []
  Parser f <|> Parser g = Parser $ \env -> case f env of
    Left ferrs | all (isUnsetError . snd) ferrs -> case g env of
      Left gerrs -> Left (ferrs ++ gerrs)
      y -> y
    x -> x

-- | Monadic bind for @'Parser'@
--
-- This short-circuits all parsing and is not ideal for an applicative style
-- parser, which ideally reports all errors instead of short-circuiting. As such
-- a `Monad` instance is not exposed for @'Parser'@.
--
bindParser :: Parser a -> (a -> Parser b) -> Parser b
bindParser (Parser f) g = Parser $ \envs -> do
  x <- f envs
  let h = unParser $ g x
  h envs

-- | Read a single environment variable's value
--
-- This will only ever fail with @'InvalidError'@, since @'UnsetError'@ is
-- handled before invoking any @'Reader'@.
--
newtype Reader a = Reader
  { unReader :: String -> Either Error a
  }
  deriving stock (Functor)

newtype Mod a = Mod (Var a -> Var a)

instance Semigroup (Mod a) where
  Mod f <> Mod g = Mod $ f . g

instance Monoid (Mod a) where
  mempty = Mod id

data Var a = Var
  { varName :: String
  , varReader :: Reader a
  , varDefault :: Maybe a
  }

varParser :: Var a -> Parser a
varParser Var {..} = Parser $ \env -> case (lookup varName env, varDefault) of
  (Nothing, Just d) -> Right d
  (Nothing, _) -> Left [(varName, UnsetError)]
  (Just v, _) -> first (pure . (varName, )) $ unReader varReader v