{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Flags.Applicative
( Name, FlagParser, FlagError(..), ParserError(..)
, parseFlags
, boolFlag
, unaryFlag, textFlag, numericFlag
) where
import Control.Applicative ((<|>), Alternative, empty, optional)
import Control.Monad (when)
import Control.Monad.Except (Except, catchError, runExcept, throwError)
import Control.Monad.RWS.Strict (RWST, runRWST)
import Control.Monad.Reader (asks)
import Control.Monad.State.Strict (get, modify, put)
import Control.Monad.Writer.Strict (tell)
import Data.Bifunctor (first, second)
import Data.Foldable (toList)
import Data.List.NonEmpty (NonEmpty(..))
import Data.Map.Strict (Map)
import Data.Maybe (isJust)
import Data.Set (Set)
import Data.Text (Text)
import System.Environment (getArgs)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Text.Read as T
type Name = Text
data Arity = Nullary | Unary deriving Eq
data Flag = Flag Arity Text
data ValueError
= MissingValue Name
| InvalidValue Name Text String
type Action a = RWST
(Map Name Text)
(Set Name)
(Set Name)
(Except ValueError)
a
data ParserError
= DuplicateFlag Name
| Empty
deriving (Eq, Show)
data FlagParser a
= Actionable (Action a) (Map Name Flag)
| Invalid ParserError
deriving Functor
mergeFlags :: Map Name Flag -> Map Name Flag -> Either Name (Map Name Flag)
mergeFlags flags1 flags2 = case Map.minViewWithKey $ flags1 `Map.intersection` flags2 of
Just ((name, _), _) -> Left name
Nothing -> Right $ flags1 `Map.union` flags2
instance Applicative FlagParser where
pure res = Actionable (pure res) Map.empty
Invalid err <*> _ = Invalid err
_ <*> Invalid err = Invalid err
Actionable action1 flags1 <*> Actionable action2 flags2 =
case mergeFlags flags1 flags2 of
Left name -> Invalid $ DuplicateFlag name
Right flags -> Actionable (action1 <*> action2) flags
instance Alternative FlagParser where
empty = Invalid Empty
Invalid Empty <|> parser = parser
parser <|> Invalid Empty = parser
Invalid err <|> _ = Invalid err
_ <|> Invalid err = Invalid err
Actionable action1 flags1 <|> Actionable action2 flags2 = case mergeFlags flags1 flags2 of
Left name -> Invalid $ DuplicateFlag name
Right flags -> Actionable action flags where
wrap action = catchError (Just <$> action) $ \case
(MissingValue _) -> pure Nothing
err -> throwError err
action = do
used <- get
wrap action1 >>= \case
Nothing -> put used >> action2
Just res -> do
used' <- get
_ <- wrap action2
put used'
pure res
data FlagError
= InconsistentFlagValues Name
| InvalidFlagValue Name Text String
| InvalidParser ParserError
| MissingFlag Name
| MissingFlagValue Name
| UnexpectedFlags (NonEmpty Name)
| UnknownFlag Name
deriving (Eq, Show)
gatherValues :: Map Name Flag -> [Text] -> Either FlagError ((Map Name Text), [Text])
gatherValues flags = go where
go [] = Right (Map.empty, [])
go (token:tokens) = case T.stripPrefix "--" token of
Nothing -> second (token:) <$> go tokens
Just "" -> Right (Map.empty, tokens)
Just suffix ->
let
(name, pval) = T.breakOn "=" suffix
missing = Left $ MissingFlagValue name
insert val tokens' = do
(vals', args') <- go tokens'
case Map.lookup name vals' of
Nothing -> Right (Map.insert name val vals', args')
Just val' -> if val == val'
then Right (vals', args')
else Left $ InconsistentFlagValues name
in case Map.lookup name flags of
Nothing -> Left (UnknownFlag name)
Just (Flag Nullary _) -> insert "" tokens
Just (Flag Unary _) -> case T.uncons pval of
Nothing -> case tokens of
(token':tokens') -> if T.isPrefixOf "--" token'
then missing
else insert token' tokens'
_ -> missing
Just (_, val) -> insert val tokens
parseFlags :: FlagParser a -> [String] -> Either FlagError (a, [Text])
parseFlags parser tokens = case parser of
Invalid err -> Left $ InvalidParser err
Actionable action flags -> case gatherValues flags (fmap T.pack tokens) of
Left err -> Left err
Right (values, args) -> case runExcept $ runRWST action values Set.empty of
Right (res, usedNames, readNames) ->
let unused = Set.difference readNames usedNames
in case Set.minView unused of
Nothing -> Right (res, args)
Just (name, names) -> Left $ UnexpectedFlags $ name :| toList names
Left (MissingValue name) -> Left $ MissingFlag name
Left (InvalidValue name val msg) -> Left $ InvalidFlagValue name val msg
useFlag :: Name -> Action ()
useFlag name = tell (Set.singleton name) >> modify (Set.insert name)
boolFlag :: Name -> Text -> FlagParser Bool
boolFlag name desc = Actionable action flags where
action = do
present <- asks (Map.member name)
when present $ useFlag name
pure present
flags = Map.singleton name (Flag Nullary desc)
unaryFlag :: (Text -> Either String a) -> Name -> Text -> FlagParser a
unaryFlag convert name desc = Actionable action flags where
action = do
useFlag name
asks (Map.lookup name) >>= \case
Nothing -> throwError $ MissingValue name
Just val -> case convert val of
Left err -> throwError $ InvalidValue name val err
Right res -> pure res
flags = Map.singleton name (Flag Unary desc)
textFlag :: Name -> Text -> FlagParser Text
textFlag = unaryFlag Right
numericFlag :: T.Reader a -> Name -> Text -> FlagParser a
numericFlag reader = unaryFlag go where
go val = case reader val of
Right (res, "") -> Right res
Left msg -> Left msg
_ -> Left "trailing value data"