packages feed

envparse-0.5.0: src/Env/Internal/Parser.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Env.Internal.Parser
  ( Parser(..)
  , VarF(..)
  , parsePure
  , traverseSensitiveVar
  , Mod(..)
  , prefixed
  , var
  , Var(..)
  , defaultVar
  , Reader
  , str
  , char
  , nonempty
  , splitOn
  , auto
  , def
  , helpDef
  , showDef
  , flag
  , switch
  , Flag
  , HasHelp
  , help
  , sensitive
  ) where

import           Control.Applicative
import           Control.Arrow (left)
import           Control.Monad ((<=<))
import           Data.Foldable (traverse_)
import           Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.Set as Set
#if __GLASGOW_HASKELL__ < 710
import           Data.Monoid (Monoid(..))
#endif
#if MIN_VERSION_base(4,9,0)
import           Data.Semigroup (Semigroup(..))
#endif
import           Data.String (IsString(..))

import           Env.Internal.Free
import qualified Env.Internal.Error as Error
import           Env.Internal.Val


-- | Try to parse a pure environment
parsePure :: Error.AsUnset e => Parser e a -> [(String, String)] -> Either [(String, e)] a
parsePure (Parser p) (Map.fromList -> env) =
  toEither (runAlt (fromEither . left pure . go) p)
 where
  go v@VarF {..} =
    case lookupVar v env of
      Left lookupErr ->
        maybe (Left lookupErr) pure varfDef
      Right val ->
        readVar v val

traverseSensitiveVar :: Applicative m => Parser e a -> (String -> m b) -> m ()
traverseSensitiveVar Parser {unParser} f =
  traverse_ f sensitiveVars
 where
  sensitiveVars =
    foldAlt (\VarF {varfSensitive, varfName} -> if varfSensitive then Set.singleton varfName else Set.empty) unParser

readVar :: VarF e a -> String -> Either (String, e) a
readVar VarF {..} =
  addName varfName . varfReader

lookupVar :: Error.AsUnset e => VarF e a -> Map String String -> Either (String, e) String
lookupVar VarF {..} =
  addName varfName . maybe (Left Error.unset) Right . Map.lookup varfName

addName :: String -> Either e a -> Either (String, e) a
addName name =
  left ((,) name)

-- | An environment parser
newtype Parser e a = Parser { unParser :: Alt (VarF e) a }
    deriving (Functor)

instance Applicative (Parser e) where
  pure =
    Parser . pure
  Parser f <*> Parser x =
    Parser (f <*> x)

instance Alternative (Parser e) where
  empty =
    Parser empty
  Parser f <|> Parser x =
    Parser (f <|> x)

-- | The string to prepend to the name of every declared environment variable
prefixed :: String -> Parser e a -> Parser e a
prefixed pre =
  Parser . hoistAlt (\v -> v {varfName=pre ++ varfName v}) . unParser

-- | Mark the enclosed variables as sensitive to remove them from the environment
-- once they've been parsed successfully.
sensitive :: Parser e a -> Parser e a
sensitive =
  Parser . hoistAlt (\v -> v {varfSensitive = True}) . unParser


data VarF e a = VarF
  { varfName      :: String
  , varfReader    :: Reader e a
  , varfHelp      :: Maybe String
  , varfDef       :: Maybe a
  , varfHelpDef   :: Maybe String
  , varfSensitive :: Bool
  } deriving (Functor)

liftVarF :: VarF e a -> Parser e a
liftVarF =
  Parser . liftAlt

-- | An environment variable's value parser. Use @(<=<)@ and @(>=>)@ to combine these
type Reader e a = String -> Either e a

-- | Parse a particular variable from the environment
--
-- @
-- >>> var 'str' \"EDITOR\" ('def' \"vim\" <> 'helpDef' show)
-- @
var :: Error.AsUnset e => Reader e a -> String -> Mod Var a -> Parser e a
var r n (Mod f) =
  liftVarF $ VarF
    { varfName = n
    , varfReader = r
    , varfHelp = varHelp
    , varfDef = varDef
    , varfHelpDef = varHelpDef <*> varDef
    , varfSensitive = varSensitive
    }
 where
  Var {varHelp, varDef, varHelpDef, varSensitive} = f defaultVar

-- | A flag that takes the active value if the environment variable
-- is set and non-empty and the default value otherwise
--
-- /Note:/ this parser never fails.
flag
  :: a -- ^ default value
  -> a -- ^ active value
  -> String -> Mod Flag a -> Parser e a
flag f t n (Mod g) =
  liftVarF $ VarF
    { varfName = n
    , varfReader = \val ->
        pure $ case (nonempty :: Reader Error.Error String) val of
          Left  _ -> f
          Right _ -> t
    , varfHelp = flagHelp
    , varfDef = Just f
    , varfHelpDef = Nothing
    , varfSensitive = flagSensitive
    }
 where
  Flag {flagHelp, flagSensitive} = g defaultFlag

-- | A simple boolean 'flag'
--
-- /Note:/ this parser never fails.
switch :: String -> Mod Flag Bool -> Parser e Bool
switch =
  flag False True

-- | The trivial reader
str :: IsString s => Reader e s
str =
  Right . fromString

-- | The reader that accepts only non-empty strings
nonempty :: (Error.AsEmpty e, IsString s) => Reader e s
nonempty =
  fmap fromString . go where go [] = Left Error.empty; go xs = Right xs

-- | The reader that uses the 'Read' instance of the type
auto :: (Error.AsUnread e, Read a) => Reader e a
auto s =
  case reads s of [(v, "")] -> Right v; _ -> Left (Error.unread (show s))

-- | The single character string reader
char :: Error.AsUnread e => Reader e Char
char s =
  case s of [c] -> Right c; _ -> Left (Error.unread "must be a one-character string")

-- | The reader that splits a string into a list of strings consuming the separator.
splitOn :: Char -> Reader e [String]
splitOn sep = Right . go
 where
  go [] = []
  go xs = go' xs

  go' xs =
    case break (== sep) xs of
      (ys, []) ->
        ys : []
      (ys, _ : zs) ->
        ys : go' zs


-- | This represents a modification of the properties of a particular 'Parser'.
-- Combine them using the 'Monoid' instance.
newtype Mod t a = Mod (t a -> t a)

#if MIN_VERSION_base(4,9,0)
instance Semigroup (Mod t a) where
  Mod f <> Mod g = Mod (g . f)
#endif

instance Monoid (Mod t a) where
  mempty = Mod id
#if MIN_VERSION_base(4,11,0)
#elif MIN_VERSION_base(4,9,0)
  mappend = (<>)
#else
  mappend (Mod f) (Mod g) = Mod (g . f)
#endif

-- | Environment variable metadata
data Var a = Var
  { varHelp      :: Maybe String
  , varHelpDef   :: Maybe (a -> String)
  , varDef       :: Maybe a
  , varSensitive :: Bool
  }

defaultVar :: Var a
defaultVar = Var
  { varHelp = Nothing
  , varDef = Nothing
  , varHelpDef = Nothing
  , varSensitive = defaultSensitive
  }

defaultSensitive :: Bool
defaultSensitive = False

-- | The default value of the variable
--
-- /Note:/ specifying it means the parser won't ever fail.
def :: a -> Mod Var a
def d =
  Mod (\v -> v {varDef=Just d})

-- | Flag metadata
data Flag a = Flag
  { flagHelp      :: Maybe String
  , flagSensitive :: Bool
  }

defaultFlag :: Flag a
defaultFlag = Flag
  { flagHelp = Nothing
  , flagSensitive = defaultSensitive
  }

-- | Show the default value of the variable in help.
helpDef :: (a -> String) -> Mod Var a
helpDef d =
  Mod (\v -> v {varHelpDef=Just d})

-- | Use the 'Show' instance to show the default value of the variable in help.
showDef :: Show a => Mod Var a
showDef =
  helpDef show


-- | A class of things that can have a help message attached to them
class HasHelp t where
  setHelp :: String -> t a -> t a

instance HasHelp Var where
  setHelp h v = v {varHelp=Just h}

instance HasHelp Flag where
  setHelp h v = v {flagHelp=Just h}

-- | Attach help text to the variable
help :: HasHelp t => String -> Mod t a
help =
  Mod . setHelp