{-# LANGUAGE CPP #-} {-# LANGUAGE NoImplicitPrelude #-} -------------------------------------------------- {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} -------------------------------------------------- {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE PackageImports #-} -------------------------------------------------- {-# LANGUAGE DeriveFunctor , DeriveFoldable , DeriveTraversable , DeriveGeneric , DeriveAnyClass #-} -------------------------------------------------- -------------------------------------------------- {-| Simple "lookup-based" parsers. -} module Prelude.Spiros.Parse ( SimpleParse , SimpleParseM , ParseError(..) , ParseErrorConfig(..) , mkBoundedEnumParser , mkShowParserWith , mkParserFromList , mkParserFromPrinterWith , displayParseError , displayParseErrorWith ) where -------------------------------------------------- -- Imports --------------------------------------- -------------------------------------------------- import Prelude.Spiros.Types import Prelude.Spiros.Classes import Prelude.Spiros.Reexports import Prelude.Spiros.Utilities import qualified Prelude.Spiros.GUI as GUI -- import qualified Prelude.Spiros.Base as Base -- import Prelude.Spiros.Base (String) -------------------------------------------------- -------------------------------------------------- import "exceptions" Control.Monad.Catch (MonadThrow(..)) -------------------------------------------------- import qualified "containers" Data.Map as Map -------------------------------------------------- ------------------------------------------------ import "base" Control.Exception (Exception(..)) --import qualified "base" Text.Show as Show -------------------------------------------------- import qualified "base" Prelude -------------------------------------------------- -- Types ----------------------------------------- -------------------------------------------------- {-| Simple parser. a Type Alias for parsing values from strings: @ (readThrow) :: (Read a) => 'SimpleParse' a @ Expansions. @ 'SimpleParse' a ≡ ('MonadThrow' m) => 'ParseM' m a ≡ ('MonadThrow' m) => (String -> m a) @ Specializations. Specializations include: @ 'SimpleParse' a ≡ (String -> 'Maybe' a) 'SimpleParse' a ≡ (String -> [a]) 'SimpleParse' a ≡ (String -> 'Either' 'SomeException' a) 'SimpleParse' a ≡ (String -> 'IO' a) @ Usage: @ -- an example printer: parseVerbosity :: 'SimpleParse' Verbosity parseVerbosity s = go s where go = \case \"concise\" -> return Concise \"verbose\" -> return Verbose \"Concise\" -> return Concise \"Verbose\" -> return Verbose \"default\" -> return def _ -> throwString s -- for this type: data Verbosity = Concise | Verbose instance Default Verbosity where def = Concise @ Also see 'SimpleParseM'. -} type SimpleParse a = (forall m. (MonadThrow m) => SimpleParseM m a) ---(forall m. (MonadThrow m) => String -> m a) -------------------------------------------------- {-| Simple (monadic) parser. Usage: @ -- an example printer: parseVerbosity :: ('MonadThrow' m) => 'SimpleParseM' m Verbosity parseVerbosity s = go s where go = \case \"concise\" -> return Concise \"verbose\" -> return Verbose \"Concise\" -> return Concise \"Verbose\" -> return Verbose \"default\" -> return def _ -> throwString s -- for this type: data Verbosity = Concise | Verbose instance Default Verbosity where def = Concise -- which can be instantiated as: parseVerbosity_Maybe :: 'SimpleParseM' 'Maybe' Verbosity parseVerbosity_Maybe = parseVerbosity parseVerbosity_Either :: 'SimpleParseM' 'Either' Verbosity parseVerbosity_Either = parseVerbosity parseVerbosity_List :: 'SimpleParseM' [] Verbosity parseVerbosity_List = parseVerbosity parseVerbosity_IO :: 'SimpleParseM' 'IO' Verbosity parseVerbosity_IO = parseVerbosity @ -} type SimpleParseM m a = (String -> m a) -------------------------------------------------- -------------------------------------------------- -- {-| -- -} -- newtype SimpleParserM (m :: * -> *) (a :: *) = SimpleParserM -- { getSimpleParserM :: -- (String -> m a) -- } -- deriving (Functor,Generic) -- --TODO Cpp for DerivingStrategies -- -- deriving newtype (Functor,Foldable,Traversable) -- -- deriving stock (Generic) -- -- deriving newtype (NFData) data ParseError = ParseError { stringBeingParsed :: !String , thingToParseInto :: !String } deriving ({-Show,-}Eq,Ord {-,Lift-},Generic ,NFData,Hashable ) -------------------------------------------------- instance Exception ParseError where -- | @'displayException' \@'ParseError' ≡ 'displayParseError'@ displayException = displayParseErrorWith (def :: ParseErrorConfig) -------------------------------------------------- {-| >>> :set -XOverloadedStrings >>> Prelude.putStrLn (Prelude.show ("unparseable" :: ParseError)) [ParseError] Can't parse <<< "unparseable" >>>. -} instance Show ParseError where -- | @show \@'ParseError' ≡ 'displayParseError'@ showsPrec precedence x = showParen (precedence >= maximumPrecedence) (displayed ++) where displayed :: String displayed = displayParseErrorWith (def :: ParseErrorConfig) x maximumPrecedence :: Int maximumPrecedence = 11 -------------------------------------------------- -- | Inject into 'stringBeingParsed' ('thingToParseInto' stays empty). instance IsString ParseError where fromString s = ParseError { stringBeingParsed = s , thingToParseInto = "" } -------------------------------------------------- -------------------------------------------------- data ParseErrorConfig = ParseErrorConfig { useUnicodeCharacters :: !Bool , useANSIColorCodes :: !Bool } deriving (Show,Eq,Ord {-,Lift-},Generic ,NFData,Hashable ) -------------------------------------------------- -- | all @False@ (for portability). instance Default ParseErrorConfig where def = ParseErrorConfig { useUnicodeCharacters = False , useANSIColorCodes = False } -------------------------------------------------- -- Definitions ----------------------------------- -------------------------------------------------- {-| Create a simple parser for a type. @ ≡ 'mkShowParserWith' ('constructors' _) @ == Examples (@doctest@ed) >>> parseBool = mkBoundedEnumParser :: String -> Maybe Bool >>> parseBool "True" Just True >>> parseBool "Abolish ICE" Nothing == Exceptions throws 'ParseError'. -} mkBoundedEnumParser :: forall a. forall m. (MonadThrow m, BoundedEnum a, Show a, Typeable a) => SimpleParseM m a mkBoundedEnumParser = mkShowParserWith (constructors proxy) where proxy = [] :: [a] {-# INLINEABLE mkBoundedEnumParser #-} -------------------------------------------------- {-| Create a simple parser from a list of ('Show'able) values. == Examples (@doctest@ed) >>> parseHaskellBool = mkShowParserWith [False, True] >>> parseHaskellBool "True" True >>> parseHaskellBool "true" *** Exception: [ParseError] Can't parse <<< ghc-prim:GHC.Types.(type Bool) >>> from <<< "true" >>>. == Exceptions throws 'ParseError'. -} mkShowParserWith :: forall a. forall m. (MonadThrow m, Show a, Typeable a) => [a] -> SimpleParseM m a mkShowParserWith values = mkParserFromList title aliases where aliases = (go <$> values) go x = (x, [show x]) title = GUI.displayGUI (GUI.fromTypeProxy proxy) proxy = [] :: [a] {-# INLINEABLE mkShowParserWith #-} -------------------------------------------------- {-| Create a simple parser from a "printing" function. == Examples (@doctest@ed) >>> printINIBool = (fmap Data.Char.toLower . show) >>> parseINIBool = mkParserFromPrinterWith "INI Bool" printINIBool [False,True] >>> parseINIBool "true" :: Maybe Bool Just True >>> parseINIBool "2" :: Maybe Bool Nothing in @(mkParserFromPrinterWith _ p)@, the printing function @p@ should be injective (otherwise, some values will be ignored). e.g. for a type @XYZ@: @ data XYZ = ... deriving (Show, Enum, Eq, Ord, ...) allXYZs :: [XYZ] allXYZs = 'constructors' printXYZ :: XYZ -> String printXYZ = show parseXYZ :: ('MonadThrow' m) => String -> m XYZ parseXYZ = 'mkParserFromPrinterWith' "XYZ" printXYZ allXYZs @ == Exceptions throws 'ParseError'. -} mkParserFromPrinterWith :: (MonadThrow m) => String -> (a -> String) -> [a] -> SimpleParseM m a mkParserFromPrinterWith title printer values = mkParserFromList title aliases where aliases = (go <$> values) go x = (x, [printer x]) --aliases = zip (values) (printer <$> values) {-# INLINEABLE mkParserFromPrinterWith #-} -------------------------------------------------- {-| Create a simple parser from a list. == Examples (@doctest@ed) >>> parseINIBool = mkParserFromList "INI Bool" [ False -: ["false","no","0"], True -: ["true","yes","1"] ] >>> parseINIBool "true" True >>> parseINIBool "2" *** Exception: [ParseError] Can't parse <<< INI Bool >>> from <<< "2" >>>. Strings should be distinct. Within a @[String]@, duplicates are ignored. Across each @[(a, [String])]@, all but one are ignored. == Exceptions throws 'ParseError'. == Implementation Internally, builds a @Map@. -} mkParserFromList :: (MonadThrow m) => String -> [(a, [String])] -> SimpleParseM m a mkParserFromList title aliases = lookupM where lookupM s = (Map.lookup s table) & (maybe (throwM e) return) where e = ParseError { stringBeingParsed = s , thingToParseInto = title } table = Map.fromList entries entries = aliases & concatMap mkEntry mkEntry (x, ts) = ts & fmap (\t -> (t, x)) {-# INLINEABLE mkParserFromList #-} -------------------------------------------------- -------------------------------------------------- {-| @ 'displayParseError' ≡ 'displayParseErrorWith' 'def' @ -} displayParseError :: ParseError -> String displayParseError = displayParseErrorWith def -------------------------------------------------- {-| == Examples (@doctest@ed) >>> :set -XOverloadedStrings >>> Prelude.putStrLn (Control.Exception.displayException ("unparseable" :: ParseError)) [ParseError] Can't parse <<< "unparseable" >>>. >>> Prelude.putStrLn (displayParseErrorWith def{ useUnicodeCharacters = True } ParseError{ stringBeingParsed = "2", thingToParseInto = "INI Bool" }) [ParseError] Can't parse « INI Bool » from « "2" ». >>> Prelude.putStrLn (displayParseErrorWith def{ useUnicodeCharacters = False } ParseError{ stringBeingParsed = "2", thingToParseInto = "INI Bool" }) [ParseError] Can't parse <<< INI Bool >>> from <<< "2" >>>. -} displayParseErrorWith :: ParseErrorConfig -> ParseError -> String displayParseErrorWith ParseErrorConfig{ useUnicodeCharacters {-, useANSIColorCodes-} } ParseError{ stringBeingParsed, thingToParseInto } = concats [ [ "[ParseError] ", "Can't parse " ] , (if (Prelude.not (Prelude.null thingToParseInto)) then [ bracketedString (thingToParseInto), " from " ] else []) , [ bracketedString (show stringBeingParsed), "." ] ] where concats = (Prelude.concat . Prelude.concat) bracketedString :: String -> String bracketedString s = concat [ (if useUnicodeCharacters then "«" else "<<<") , " " , s , " " , (if useUnicodeCharacters then "»" else ">>>") ] -------------------------------------------------- -- Utilities ------------------------------------- -------------------------------------------------- -- __useUnicode :: Bool -- __useUnicode = False -------------------------------------------------- --------------------------------------------------