{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Text.LambdaOptions.Core (
runOptions,
Options,
OptionsError(..),
OptionCallback,
addOption,
getHelpDescription,
getKeywords
) where
import Control.Monad.State
import Data.Function
import Data.List
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe
import Data.Proxy
import Data.Typeable hiding (typeRep)
import Text.LambdaOptions.Formatter
import Text.LambdaOptions.Internal.Opaque
import Text.LambdaOptions.Internal.OpaqueParser
import Text.LambdaOptions.Internal.Wrap
import Text.LambdaOptions.Keyword
import Text.LambdaOptions.Parseable ()
getProxy :: a -> Proxy a
getProxy _ = Proxy
type OptionCallback m a f = (Monad m, GetOpaqueParsers a f, Wrap (m a) f)
internalizeKeyword :: Keyword -> Keyword
internalizeKeyword k = k {
kwNames = nub $ sort $ kwNames k }
data OptionInfo m a = OptionInfo {
optionKeyword :: Keyword,
optionTypeReps :: [TypeRep],
optionOpaqueCallback :: OpaqueCallback (m a)
} deriving ()
newtype Options m a b = Options {
unOptions :: State (OptionsState m a) b
} deriving (Applicative, Functor, Monad, MonadState (OptionsState m a))
data OptionsState m a = OptionsState {
stateOpaqueParsers :: Map TypeRep OpaqueParser,
stateOptionsByArity :: [[OptionInfo m a]],
stateCollectedActions :: [m a],
stateCurrMark :: Int,
stateHighMark :: Int,
stateArgs :: [String],
stateFormatConfig :: FormatConfig
} deriving ()
data OptionsError = ParseFailed {
parseFailedMessage :: String,
parseFailedBeginArgsIndex :: Int,
parseFailedEndArgsIndex :: Int
} deriving (Show)
mkParseFailed :: Int -> Int -> [String] -> OptionsError
mkParseFailed beginIndex endIndex args = ParseFailed {
parseFailedMessage = mkParseFailedMessage beginIndex endIndex 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 :: (Monad m) => Options m a () -> [String] -> Either OptionsError [m a]
runOptions options args = runOptions' args $ runOptionsInternal defaultFormatConfig args (options >> tryParseAll)
runOptionsInternal :: (Monad m) => FormatConfig -> [String] -> Options m a b -> (b, OptionsState m a)
runOptionsInternal config args options = runState (unOptions options) $ OptionsState {
stateOpaqueParsers = Map.empty,
stateOptionsByArity = [],
stateCollectedActions = [],
stateCurrMark = 0,
stateHighMark = 0,
stateArgs = args,
stateFormatConfig = config }
runOptions' :: (Monad m) => [String] -> (Bool, OptionsState m a) -> Either OptionsError [m a]
runOptions' args result = case result of
(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 n = case n of
0 -> case xss of
[] -> [[x]]
xs : rest -> (x : xs) : rest
_ -> case xss of
[] -> [] : addByArity x [] (n - 1)
xs : rest -> xs : addByArity x rest (n - 1)
addOption :: (OptionCallback m a f) => Keyword -> f -> Options m a ()
addOption inKwd f = do
let (typeReps, opaqueParsers) = unzip $ getOpaqueParsers $ getProxy f
arity = length typeReps
f' = wrap f
kwd = internalizeKeyword inKwd
info = OptionInfo {
optionKeyword = kwd,
optionTypeReps = typeReps,
optionOpaqueCallback = f' }
forM_ (zip typeReps opaqueParsers) $ \(typeRep, opaqueParser) -> do
modify $ \st -> st { stateOpaqueParsers = Map.insert typeRep opaqueParser $ stateOpaqueParsers st }
modify $ \st -> st { stateOptionsByArity = addByArity info (stateOptionsByArity st) arity }
firstM :: (Monad m) => [m Bool] -> m Bool
firstM actions = case actions of
m : ms -> m >>= \result -> case result of
False -> firstM ms
True -> return True
[] -> return False
whileM :: (Monad m) => m Bool -> m ()
whileM m = m >>= \result -> case result of
True -> whileM m
False -> return ()
tryParseAll :: (Monad m) => Options m a Bool
tryParseAll = do
whileM tryParse
gets (null . stateArgs)
tryParse :: (Monad m) => Options m a Bool
tryParse = gets (null . stateArgs) >>= \result -> case result of
True -> return False
False -> tryParseByArity
tryParseByArity :: (Monad m) => Options m a Bool
tryParseByArity = do
optionsByArity <- gets $ reverse . stateOptionsByArity
firstM $ map tryParseByOptions optionsByArity
tryParseByOptions :: (Monad m) => [OptionInfo m a] -> Options m a Bool
tryParseByOptions = firstM . map tryParseByOption
tryParseByOption :: (Monad m) => OptionInfo m a -> Options m a Bool
tryParseByOption option = do
restorePoint <- get
matchKeyword (optionKeyword option) >>= \match -> case match of
False -> return False
True -> do
let knownParsers = stateOpaqueParsers restorePoint
args <- gets stateArgs
beginMark <- gets stateCurrMark
let typeReps = optionTypeReps option
opaqueParsers = mapMaybe (flip Map.lookup knownParsers) typeReps
(mOpaques, n) = sequenceParsers args opaqueParsers
args' = drop n args
result <- case mOpaques of
Nothing -> do
put restorePoint
return False
Just opaques -> do
let action = optionOpaqueCallback option opaques
modify $ \st -> st {
stateCurrMark = beginMark + n,
stateCollectedActions = action : stateCollectedActions st,
stateArgs = args' }
return True
modify $ \st -> let
oldHighMark = stateHighMark st
newHighMark = max oldHighMark (beginMark + n)
in st { stateHighMark = newHighMark }
return result
matchKeyword :: (Monad m) => Keyword -> Options m a Bool
matchKeyword kwd = gets stateArgs >>= \args -> case args of
[] -> return False
(arg : rest) -> case matchKeyword' arg kwd of
Nothing -> return False
Just n -> do
modify $ \st -> let
newCurrMark = stateCurrMark st + n
in st {
stateCurrMark = newCurrMark,
stateHighMark = max newCurrMark (stateHighMark st),
stateArgs = rest }
return 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 parsers = case parsers of
[] -> (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 :: (Monad m) => Options m a [Keyword]
collectKeywords = 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 :: (Monad m) => Options m a String
createHelpDescription = do
config <- gets stateFormatConfig
kwds <- collectKeywords
return $ formatKeywords config kwds
getHelpDescription :: (Monad m) => Options m a () -> String
getHelpDescription options = fst $ runOptionsInternal defaultFormatConfig [] $ do
options
createHelpDescription
getKeywords :: (Monad m) => Options m a () -> [Keyword]
getKeywords options = fst $ runOptionsInternal defaultFormatConfig [] $ do
options
collectKeywords