{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Safe #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Text.LambdaOptions.Core (
runOptions,
Options,
OptionsError(..),
prettyOptionsError,
OptionCallback,
addOption,
getHelpDescription,
getKeywords,
) where
import Control.Monad
( forM_ )
import qualified Control.Monad.State as State
import Control.Monad.State
( MonadState, State )
import Data.Function
( on )
import Data.List
( nub, sort, sortBy )
import qualified Data.Map as Map
import Data.Map
( Map )
import Data.Maybe
( mapMaybe )
import Data.Proxy
( Proxy(Proxy) )
import Data.Typeable
( TypeRep )
import Text.LambdaOptions.Formatter
( FormatConfig, defaultFormatConfig, formatKeywords )
import Text.LambdaOptions.Internal.Opaque
( Opaque, OpaqueCallback )
import Text.LambdaOptions.Internal.OpaqueParser
( OpaqueParser, GetOpaqueParsers, getOpaqueParsers )
import Text.LambdaOptions.Internal.Wrap
( Wrap, wrap )
import Text.LambdaOptions.Keyword
( Keyword(..) )
import Text.LambdaOptions.Parseable
( )
type OptionCallback r f = (GetOpaqueParsers r f, Wrap r f)
internalizeKeyword :: Keyword -> Keyword
internalizeKeyword k = k
{ kwNames = nub $ sort $ kwNames k
}
data OptionInfo r
= OptionInfo
{ optionKeyword :: Keyword
, optionTypeReps :: [TypeRep]
, optionOpaqueCallback :: OpaqueCallback r
}
newtype Options r a
= Options
{ unOptions :: State (OptionsState r) a
}
instance Functor (Options r) where
fmap f = Options . fmap f . unOptions
instance Applicative (Options r) where
pure = Options . pure
Options f <*> Options x = Options (f <*> x)
instance Monad (Options r) where
return = Options . return
Options x >>= f = Options (x >>= unOptions . f)
instance MonadState (OptionsState r) (Options r) where
get = Options State.get
put = Options . State.put
state = Options . State.state
data OptionsState r
= OptionsState
{ stateOpaqueParsers :: Map TypeRep OpaqueParser
, stateOptionsByArity :: [[OptionInfo r]]
, stateCollectedActions :: [r]
, stateCurrMark :: Int
, stateHighMark :: Int
, stateArgs :: [String]
, stateFormatConfig :: FormatConfig
}
{-# DEPRECATED parseFailedMessage "Use 'prettyOptionsError' instead." #-}
data OptionsError
= ParseFailed
{ parseFailedMessage :: String
, parseFailedArgs :: [String]
, parseFailedBeginArgsIndex :: Int
, parseFailedEndArgsIndex :: Int
}
deriving (Show)
prettyOptionsError :: OptionsError -> String
prettyOptionsError = \case
ParseFailed
{ parseFailedArgs = args
, parseFailedBeginArgsIndex = beginIndex
, parseFailedEndArgsIndex = endIndex
} -> mkParseFailedMessage beginIndex endIndex args
mkParseFailed :: Int -> Int -> [String] -> OptionsError
mkParseFailed beginIndex endIndex args = ParseFailed
{ parseFailedMessage = mkParseFailedMessage beginIndex endIndex args
, parseFailedArgs = args
, parseFailedBeginArgsIndex = beginIndex
, parseFailedEndArgsIndex = endIndex
}
mkParseFailedMessage :: Int -> Int -> [String] -> String
mkParseFailedMessage beginIndex endIndex args
| endIndex == beginIndex + 1
= "Unknown option at index " ++ beginIndexStr ++ ": `" ++ begin ++ "'"
| endIndex == length args + 1
= "Bad input for `" ++ begin ++ "' at index " ++ beginIndexStr ++ ": End of input."
| otherwise
= "Bad input for `" ++ begin ++ "' at index " ++ beginIndexStr ++ ": `" ++ end ++ "'"
where
begin = args !! beginIndex
end = args !! (endIndex - 1)
beginIndexStr = show beginIndex
runOptions :: Options r () -> [String] -> Either OptionsError [r]
runOptions action args = runOptions' args $ do
runOptionsInternal defaultFormatConfig args $ do
action
tryParseAll
runOptionsInternal :: FormatConfig -> [String] -> Options r a -> (a, OptionsState r)
runOptionsInternal config args action = State.runState (unOptions action) OptionsState
{ stateOpaqueParsers = Map.empty
, stateOptionsByArity = []
, stateCollectedActions = []
, stateCurrMark = 0
, stateHighMark = 0
, stateArgs = args
, stateFormatConfig = config
}
runOptions' :: [String] -> (Bool, OptionsState r) -> Either OptionsError [r]
runOptions' args = \case
(True , st) -> Right $ reverse $ stateCollectedActions st
(False, st) -> Left $ let
currMark = stateCurrMark st
highMark = stateHighMark st
in mkParseFailed currMark (highMark + 1) args
addByArity :: a -> [[a]] -> Int -> [[a]]
addByArity x xss = \case
0 -> case xss of
[] -> [[x]]
xs : rest -> (x : xs) : rest
n -> case xss of
[] -> [] : addByArity x [] (n - 1)
xs : rest -> xs : addByArity x rest (n - 1)
addOption :: forall r f. (OptionCallback r f) => Keyword -> f -> Options r ()
addOption inKwd f = do
let (reps, opaqueParsers) = unzip $ getOpaqueParsers (Proxy :: Proxy r) (Proxy :: Proxy f)
arity = length reps
f' = wrap f
kwd = internalizeKeyword inKwd
info = OptionInfo
{ optionKeyword = kwd
, optionTypeReps = reps
, optionOpaqueCallback = f'
}
forM_ (zip reps opaqueParsers) $ \(rep, opaqueParser) -> do
State.modify $ \st -> st
{ stateOpaqueParsers = Map.insert rep opaqueParser $ stateOpaqueParsers st
}
State.modify $ \st -> st
{ stateOptionsByArity = addByArity info (stateOptionsByArity st) arity
}
firstM :: (Monad m) => [m Bool] -> m Bool
firstM = \case
m : ms -> m >>= \case
False -> firstM ms
True -> pure True
[] -> pure False
whileM :: (Monad m) => m Bool -> m ()
whileM m = m >>= \case
True -> whileM m
False -> pure ()
tryParseAll :: Options r Bool
tryParseAll = do
whileM tryParse
State.gets (null . stateArgs)
tryParse :: Options r Bool
tryParse = State.gets (null . stateArgs) >>= \case
True -> pure False
False -> tryParseByArity
tryParseByArity :: Options r Bool
tryParseByArity = do
optionsByArity <- State.gets $ reverse . stateOptionsByArity
firstM $ map tryParseByOptions optionsByArity
tryParseByOptions :: [OptionInfo r] -> Options r Bool
tryParseByOptions = firstM . map tryParseByOption
tryParseByOption :: OptionInfo r -> Options r Bool
tryParseByOption option = do
restorePoint <- State.get
matchKeyword (optionKeyword option) >>= \case
False -> pure False
True -> do
let knownParsers = stateOpaqueParsers restorePoint
args <- State.gets stateArgs
beginMark <- State.gets stateCurrMark
let reps = optionTypeReps option
opaqueParsers = mapMaybe (flip Map.lookup knownParsers) reps
(mOpaques, n) = sequenceParsers args opaqueParsers
args' = drop n args
result <- case mOpaques of
Nothing -> do
State.put restorePoint
pure False
Just opaques -> do
let action = optionOpaqueCallback option opaques
State.modify $ \st -> st {
stateCurrMark = beginMark + n,
stateCollectedActions = action : stateCollectedActions st,
stateArgs = args' }
pure True
State.modify $ \st -> let
oldHighMark = stateHighMark st
newHighMark = max oldHighMark (beginMark + n)
in st { stateHighMark = newHighMark }
pure result
matchKeyword :: Keyword -> Options r Bool
matchKeyword kwd = State.gets stateArgs >>= \case
[] -> pure False
(arg : rest) -> case matchKeyword' arg kwd of
Nothing -> pure False
Just n -> do
State.modify $ \st -> let
newCurrMark = stateCurrMark st + n
in st
{ stateCurrMark = newCurrMark
, stateHighMark = max newCurrMark $ stateHighMark st
, stateArgs = rest
}
pure True
matchKeyword' :: String -> Keyword -> Maybe Int
matchKeyword' arg kwd = case kwNames kwd of
[] -> Just 0
names -> case any (arg ==) names of
False -> Nothing
True -> Just 1
sequenceParsers :: [String] -> [OpaqueParser] -> (Maybe [Opaque], Int)
sequenceParsers args = \case
[] -> (Just [], 0)
p : ps -> case p args of
(Nothing, n) -> (Nothing, n)
(Just o , n) -> let
rest = drop n args
in case sequenceParsers rest ps of
(Nothing, n') -> (Nothing, n + n')
(Just os, n') -> (Just $ o : os, n + n')
collectKeywords :: Options r [Keyword]
collectKeywords = State.gets $ sortBy cmp . map optionKeyword . concat . stateOptionsByArity
where
cmp = namesCmp `on` kwNames
namesCmp [] [] = EQ
namesCmp [] _ = LT
namesCmp _ [] = GT
namesCmp ns1 ns2 = (compare `on` head) ns1 ns2
createHelpDescription :: Options r String
createHelpDescription = do
config <- State.gets stateFormatConfig
kwds <- collectKeywords
pure $ formatKeywords config kwds
getHelpDescription :: Options r () -> String
getHelpDescription options = fst $ runOptionsInternal defaultFormatConfig [] $ do
options
createHelpDescription
getKeywords :: Options r () -> [Keyword]
getKeywords options = fst $ runOptionsInternal defaultFormatConfig [] $ do
options
collectKeywords