repl-toolkit 1.0.1.0 → 1.1.0.0
raw patch · 8 files changed
+138/−34 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- System/REPL.hs +9/−1
- System/REPL/Ask.hs +0/−5
- System/REPL/Command.hs +95/−15
- System/REPL/Config.hs +0/−3
- System/REPL/Prompt.hs +1/−5
- System/REPL/Types.hs +0/−3
- changelog.txt +4/−0
- repl-toolkit.cabal +29/−2
System/REPL.hs view
@@ -1,5 +1,5 @@ -- |Contents: --- +-- -- [/Ask/] -- Asking the user for input in a principled way. -- Reading, parsing errors, predicate checks are all handled. @@ -8,6 +8,14 @@ -- The main module of the package. Functions for creating -- commands, which can receive and ask for arguments. -- Commands are composable and can be built into a REPL. +-- +-- +-- With commands, making a full-fledged CLI is as simple as writing: +-- +-- @ +-- main :: IO () +-- main = makeREPLSimple [cmd1, cmd2, cmd3] +-- @ -- -- [/Config/] -- Read configuration files in various formats.
System/REPL/Ask.hs view
@@ -1,8 +1,3 @@-{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE TupleSections #-} - -- |Asking the user for input on the console. -- -- The main type is 'Asker', which takes care of parsing
System/REPL/Command.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE OverloadedStrings #-} - -- |Provides Commands for REPLs. Commands are there to provide high-level -- handling of user input and to offer functionality in a standard, composable -- way. @@ -149,6 +147,7 @@ import Data.ListLike.IO (ListLikeIO(..)) import Data.Maybe (fromJust, isJust, fromMaybe) import Data.Ord +import Data.Typeable (cast) import qualified Data.Text as T import System.REPL.Ask import System.REPL.Types @@ -184,9 +183,11 @@ runSingleCommand :: (MonadThrow m) => Command m T.Text a -> T.Text -> m a runSingleCommand c t = fromJust <$> runSingleCommandIf (c{commandTest = const True}) t --- |Runs the command with the input text as parameter. If the input doesn't --- pass the command test, @Nothing@ is returned. +-- |Runs the command with the input text as parameter. -- +-- The first parameter (or the empty string, if no input was given) +-- is passed to the command test. If it fails the test, 'Nothing' is returned. +-- -- Can throw: -- -- * 'MalformedParamsError' @@ -194,15 +195,15 @@ runSingleCommandIf :: MonadThrow m => Command m T.Text a -> T.Text -> m (Maybe a) runSingleCommandIf c t = do t' <- readArgs t - if L.null t' || not (commandTest c $ LU.head t') then return Nothing + let t'' = if L.null t' then "" else LU.head t' + if not (commandTest c t'') then return Nothing else do (res, output) <- runPartialCommand c t' - let act = length t' + let act = max 0 (length t' - 1) mx = act - length output when (not . L.null $ output) (throwM $ TooManyParamsError mx act) return $ Just res - -- |Takes a list @xs@ and executes the first command in a list whose -- 'commandTest' matches the input. -- @@ -577,15 +578,13 @@ -- |Throws a 'TooFewParamsError' if the length of the list is smaller than the second argument. checkParamNum :: MonadThrow m => [a] -> Int -> m () checkParamNum xs need = if have < need then throwM $ TooFewParamsError need have else return () - where have = length xs - 1 + where have = max 0 (length xs - 1) -- |Wrapper for 'ask'. askC :: (MonadIO m, MonadCatch m) => Asker m a0 a -> [T.Text] -> Int -> m a askC f xs i = ask f (xs L.!! i) ---askC False f xs j i = maybe (throwM $ TooFewParamsError j (length xs - 1)) (ask f . Just) (xs L.!! i) - -- |Runs a REPL based on a set of commands. -- For a line of input, the commands are tried in following order: -- @@ -624,6 +623,7 @@ -- |A variant of 'makeREPL' with some default settings: -- -- * The "exit" command is 'defExitCmd'. +-- * Commands consistining only of whitespace are ignored. -- * The "unknown" command prints "Unknown command: <user input>". -- * The prompt is "> ". -- * The error handler is 'defErrorHandler'. @@ -632,9 +632,12 @@ -> m () makeREPLSimple regular = makeREPL regular defExitCmd unknownCmd PR.prompt defErrorHandler where - unknownCmd = makeCommandN "" (const True) "" False [] (repeat lineAsker) - (\t _ -> liftIO $ PR.putStrLn $ "Unknown command: " ++ t) + unknownCmd = makeCommandN "" (const True) "" False [] (repeat lineAsker) f + f t ts = if T.all isSpace t && L.all (T.all isSpace) ts + then return () + else liftIO $ PR.putStrLn $ "Unknown command: " ++ t ++ "." + -- Example commands ------------------------------------------------------------------------------- @@ -676,13 +679,90 @@ -- |A default error handler that catches 'SomeREPLError' and prints it to stdout. -- --- Since all the sub-types of 'SomeREPLError' just wrap a 'SomeException', we --- use the 'Show'-instance of that inner exception. +-- For the following errors, we print a user-friendly error message: -- +-- * 'GenericTypeError' (when wrapped in an 'AskerTypeError'), +-- * 'GenericPredicateError' (when wrapped in an 'AskerPredicateError'), +-- * 'PathRootDoesNotExist' (when wrapped in an 'AskerPredicateError'), +-- * 'PathIsNotWritable' (when wrapped in an 'AskerPredicateError'), +-- * 'GenericPredicateError' (when wrapped in an 'AskerPredicateError'), +-- * 'AskerInputAbortedError', +-- * 'MalformedParamsError', +-- * 'TooManyParamsError', +-- * 'TooFewParamsError', +-- * 'NoConfigFileParseError'. +-- +-- For every other subtype of 'SomeREPLError', we just print the Show-instance. +-- -- Useful in combination with 'makeREPL'. defErrorHandler :: MonadIO m => [Handler m ()] -defErrorHandler = [Handler h] +defErrorHandler = + [Handler h_askerGenericTypeError, + Handler h_askerGenericPredicateError, + Handler h_askerPathRootDoesNotExist, + Handler h_askerPathIsNotWritable, + Handler h_tooMalformedParamsError, + Handler h_tooManyParamsError, + Handler h_tooFewParamsError, + Handler h_noConfigFileParseError, + Handler h] where + put :: String -> IO () + put = putStrLn + h :: MonadIO m => SomeREPLError -> m () h = liftIO . print + + h_askerGenericTypeError :: MonadIO m => AskerTypeError -> m () + h_askerGenericTypeError (AskerTypeError e) = case fromException e of + Just (GenericTypeError t) -> liftIO . put . T.unpack $ t + Nothing -> liftIO . print $ e + + + h_askerGenericPredicateError :: MonadIO m => AskerPredicateError -> m () + h_askerGenericPredicateError (AskerPredicateError e) = case fromException e of + Just (GenericPredicateError t) -> liftIO . put . T.unpack $ t + Nothing -> liftIO . print $ e + + h_askerPathRootDoesNotExist :: MonadIO m => AskerPredicateError -> m () + h_askerPathRootDoesNotExist (AskerPredicateError e) = case fromException e of + Just (PathRootDoesNotExist fp) -> liftIO $ put $ + "The root of the path '" ++ fp ++ + "' does not exist." + Nothing -> liftIO . print $ e + + h_askerPathIsNotWritable :: MonadIO m => AskerPredicateError -> m () + h_askerPathIsNotWritable (AskerPredicateError e) = case fromException e of + Just (PathIsNotWritable fp) -> liftIO $ put $ + "The path '" ++ fp ++ + "' is not writable." + Nothing -> liftIO . print $ e + + h_askerInputAbortedError :: MonadIO m => AskerTypeError -> m () + h_askerInputAbortedError (AskerTypeError e) = + liftIO $ put "Input aborted." + + h_tooMalformedParamsError :: MonadIO m => MalformedParamsError -> m () + h_tooMalformedParamsError (MalformedParamsError t) = liftIO . put $ + "Error parsing parameters: " ++ T.unpack t + + h_tooManyParamsError :: MonadIO m => TooManyParamsError -> m () + h_tooManyParamsError (TooManyParamsError m x) = liftIO . put $ + "Expected " ++ exp ++ " parameters, got " ++ got + where + exp = if m > 0 + then "at most " ++ show m + else "no" + + got = if x <= 0 then "none." else show x ++ "." + + h_tooFewParamsError :: MonadIO m => TooFewParamsError -> m () + h_tooFewParamsError (TooFewParamsError m x) = liftIO . put $ + "Expected at least " ++ show m ++ " parameters, got " ++ got + where + got = if x <= 0 then "none." else show x ++ "." + + h_noConfigFileParseError :: MonadIO m => NoConfigFileParseError -> m () + h_noConfigFileParseError (NoConfigFileParseError t) = liftIO . put $ + "Error parsing configuration file: " ++ T.unpack t
System/REPL/Config.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE DeriveDataTypeable #-} - -- |Contains logic for reading configuration files. module System.REPL.Config ( readConfigFile,
System/REPL/Prompt.hs view
@@ -1,9 +1,5 @@-{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE ScopedTypeVariables #-} - -- |Little helper functions for getting and putting lines. --- +-- -- This module re-exports part of "Data.ListLike.IO", which contains names that clash with Prelude. module System.REPL.Prompt ( -- *String-generic versions of Prelude Functions
System/REPL/Types.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} - -- |Types used by other modules in the package. -- -- The module contains the following exception hierarchy:
changelog.txt view
@@ -1,3 +1,7 @@+1.1.0.0 Expanded defErrorHandler with user-friendly messages. + Fixed the handling of empty input. + makeREPLSimple now ignores input consisting only of whitespace. + 1.0.1.0 Fixed name clashes with base, added new exports, fixed typos. 1.0.0.1 Updated aeson dependency's upper bound.
repl-toolkit.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: repl-toolkit -version: 1.0.1.0 +version: 1.1.0.0 synopsis: Toolkit for quickly whipping up config files and command-line interfaces. description: A simple toolkit for quickly whipping up REPLs, input validation and sets of commands included. homepage: https://github.com/ombocomp/repl-toolkit @@ -27,7 +27,7 @@ System.REPL.Prompt, System.REPL.State, System.REPL.Types - other-extensions: OverloadedStrings, + default-extensions: OverloadedStrings, DeriveDataTypeable, FlexibleContexts, LambdaCase, @@ -52,3 +52,30 @@ text >=1.1 default-language: Haskell2010 +-- executable repl-toolkit-test +-- main-is: Test.hs +-- build-depends: base >=4.8 && <5, +-- aeson >=0.8.0.2 && <2, +-- bytestring >= 0.10 && <0.20, +-- data-default >= 0.5.3, +-- directory >= 1.2.1 && <2, +-- exceptions >=0.8 && <1, +-- filepath >= 1.3 && <2, +-- functor-monadic >=0.1, +-- ListLike >=4.1, +-- listsafe >= 0.1, +-- monad-loops >= 0.3 && <0.6, +-- mtl >= 2.2 && <3, +-- parsec >=3.1 && <4, +-- semigroupoids >= 4 && <6, +-- transformers >= 0.3 && <0.7, +-- text >=1.1, +-- repl-toolkit +-- default-extensions: OverloadedStrings, +-- DeriveDataTypeable, +-- FlexibleContexts, +-- LambdaCase, +-- ScopedTypeVariables, +-- ExistentialQuantification, +-- TupleSections +-- default-language: Haskell2010