From 788de39262809040ebf1096aff22190ad526dc1b Mon Sep 17 00:00:00 2001 From: defanor Date: Sat, 27 Jan 2024 23:04:26 +0300 Subject: Rewrite, version 0.2: use Parsec, focus on DSV --- ChangeLog.md | 7 + Coalpit.hs | 595 ++++++++++++++++++++++++++++++++++++++++++++++++----- Coalpit/Core.hs | 504 --------------------------------------------- Coalpit/DSV.hs | 59 ------ Coalpit/Parsing.hs | 90 -------- README.md | 81 +++----- coalpit.cabal | 19 +- examples/Basic.hs | 16 +- examples/Pipes.hs | 67 ------ test/Test.hs | 33 +-- 10 files changed, 607 insertions(+), 864 deletions(-) delete mode 100644 Coalpit/Core.hs delete mode 100644 Coalpit/DSV.hs delete mode 100644 Coalpit/Parsing.hs delete mode 100644 examples/Pipes.hs diff --git a/ChangeLog.md b/ChangeLog.md index 239d0b7..bf356ee 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,12 @@ # Revision history for coalpit +## 0.2.0.0 -- 2024-01-26 + +Switched from Megaparsec to Parsec for more common and stable +dependencies. Now focusing on DSV, and always including constructor +names in order to avoid possible ambiguity. + + ## 0.1.1.0 -- 2018-01-03 Complete rewriting and first Hackage release. diff --git a/Coalpit.hs b/Coalpit.hs index edbade0..0eec909 100644 --- a/Coalpit.hs +++ b/Coalpit.hs @@ -5,61 +5,554 @@ Stability : unstable Portability : non-portable (uses GHC extensions) Coalpit is a library for building command-line interfaces: the goal is -to get interfaces quickly and easily, while keeping them -language-agnostic and more user- and shell scripting-friendly than -JSON and similar formats. +to build interfaces quickly and easily (by deriving those), while +keeping them language-agnostic and more user- and shell +scripting-friendly than JSON and similar formats. +-} + +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE DefaultSignatures #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE ScopedTypeVariables #-} -== Example +module Coalpit ( fromDSV + , fromDSVList + , toDSV + , toDSVList + , dsvFromList + , Coalpit(..) + -- * Usage + , usage + , usageString + , Usage(..) + -- * Options + , SelNamePolicy(..) + , Options(..) + , defOpt + ) where -@ -\{\-\# LANGUAGE DeriveGeneric, DeriveAnyClass \#\-\} import GHC.Generics -import Data.Proxy -import System.Environment -import Coalpit - -data Foo = Foo { bar :: Maybe Int - , baz :: String - } deriving (Show, Generic, 'Coalpit') - -main :: IO () -main = do - args <- getArgs - case 'fromArgs' 'defOpt' args of - Left err -> do - putStrLn err - putStrLn $ "Usage: " ++ 'usageString' 'defOpt' (Proxy :: Proxy Foo) - Right x -> do - print (x :: Foo) - print $ 'toArgs' 'defOpt' x - putStrLn $ showDSV defOpt x - print ('readDSV' 'defOpt' $ 'showDSV' 'defOpt' x :: Either String Foo) -@ - -Then, in a shell: - -> $ ./Example 'a string' -> Foo {bar = Nothing, baz = "a string"} -> ["a string"] -> "a string" -> Right (Foo {bar = Nothing, baz = "a string"}) -> $ ./Example --bar 42 'a string' -> Foo {bar = Just 42, baz = "a string"} -> ["--bar","42","a string"] -> --bar 42 "a string" -> Right (Foo {bar = Just 42, baz = "a string"}) -> $ ./Example --bar foo -> arguments:1:3: -> Failed to read: foo -> -> Usage: [--bar INT] STRING +import Text.Parsec +import Text.Parsec.String +import Data.Char (toLower) +import Data.Proxy (Proxy(..)) +import qualified Data.List.NonEmpty as NE +import Data.List.NonEmpty (NonEmpty(..)) +import Data.List (intercalate) +import Data.Word (Word8, Word16, Word32, Word64) +import Numeric.Natural (Natural) +import Data.Int (Int8, Int16, Int32, Int64) +import Data.Time.Clock (DiffTime, NominalDiffTime, UniversalTime, UTCTime) +import Data.Time.Format ( TimeLocale, formatTime + , iso8601DateFormat, defaultTimeLocale + , ParseTime, readSTime) +import Data.Time.Calendar (Day) +import Data.Time.LocalTime (TimeOfDay, LocalTime, ZonedTime) +import Data.Scientific (Scientific, FPFormat(..), formatScientific, scientificP) +import Text.ParserCombinators.ReadP (readP_to_S) +import Data.Complex (Complex) +import Data.Version (Version, parseVersion, showVersion) +import System.Exit (ExitCode) +import Network.URI (URI, parseURIReference, uriToString) --} +-- | Usage description: can be translated into help messages or +-- documentation formats. +data Usage = UConstructor String + -- ^ Data constructor. + | URecursive String + -- ^ Constructor of a recursive data structure (its second + -- appearance in the tree). + | USelector Bool String Usage + -- ^ Record selector. + | UOptional Usage + -- ^ Optional element. + | USum Usage Usage + -- ^ Sum. + | UProduct Usage Usage + -- ^ Product. + | UUnit + -- ^ Unit. + | UType String + -- ^ Type name (e.g., \"INT\"). + deriving (Show) -module Coalpit ( module Coalpit.Core - , module Coalpit.DSV - ) where +-- | How to handle selector names. +data SelNamePolicy = SNDisable + -- ^ Do not parse or print selector names + | SNAvoid + -- ^ Allow selector names on parsing, but do not + -- print them + | SNPrefer + -- ^ Allow selector names on parsing, print them + | SNRequire + -- ^ Require selector names on parsing, print them + deriving (Show, Eq) + +-- | Printing and parsing options. +data Options = Options { fieldSeparators :: NonEmpty Char + -- ^ Separators between fields + , recordSeparators :: NonEmpty Char + -- ^ Separators between records (which may + -- correspond to lines) + , conNameMod :: String -> String + -- ^ Constructor name modifier + , selNameMod :: String -> String + -- ^ Record selector name modifier + , selNamePolicy :: SelNamePolicy + , timeLocale :: TimeLocale + , dateFormat :: String + -- ^ See "Data.Time.Format". + , timeFormat :: String + , dateTimeFormat :: String + , scientificFormat :: FPFormat + , scientificDecimals :: Maybe Int + , uriUserInfo :: String -> String + -- ^ Used to map userinfo parts of URIs. + } + +-- | Default options. +defOpt :: Options +defOpt = Options (' ' :| ['\t']) ('\n' :| []) + (map toLower) (("--" ++) . map toLower) SNAvoid + defaultTimeLocale (iso8601DateFormat Nothing) "%H:%M:%S" + (iso8601DateFormat (Just "%H:%M:%S")) Generic Nothing id + +parseDSV :: Parser a -> String -> Either String a +parseDSV p s = case parse p "DSV" s of + Left err -> Left $ show err + Right x -> Right x + +-- | Parse a single record from a string. +fromDSV :: Coalpit a => Options -> String -> Either String a +fromDSV opt str = parseDSV (coalpitParser opt) str + +-- | Parse multiple records from a string. +fromDSVList :: Coalpit a => Options -> String -> Either String [a] +fromDSVList opt str = + parseDSV (coalpitParser opt `sepEndBy` pRecordSep opt) str + +escape :: Options -> String -> String +escape opt str + | not (null str) && + all (\fs -> not (fs `elem` str)) + ('\\' + : '\"' + : NE.toList (fieldSeparators opt) + ++ NE.toList (recordSeparators opt)) = str + | otherwise = '"' : escaped str ++ "\"" + where + escaped :: String -> String + escaped [] = [] + escaped (c:rest) + | c `elem` "\\\"" = '\\' : c : escaped rest + | otherwise = c : escaped rest + +-- | Build a record ("line") out of individual strings, escaping those +-- if needed. +dsvFromList :: Options -> [String] -> String +dsvFromList opt l = intercalate [NE.head (fieldSeparators opt)] + (map (escape opt) l) + +-- | Serialize a value. +toDSV :: Coalpit a => Options -> a -> String +toDSV opt x = dsvFromList opt (coalpitPrint opt x) + +-- | Serialize multiple values. +toDSVList :: Coalpit a => Options -> [a] -> String +toDSVList opt l = + concatMap (\x -> toDSV opt x ++ [NE.head (recordSeparators opt)]) l + +-- | Compose 'Usage' description. +usage :: Coalpit a => Options -> Proxy a -> Usage +usage opt = coalpitDescription opt [] + +-- | Compose a usage string. +usageString :: Coalpit a => Options -> Proxy a -> String +usageString opt = usageToString . usage opt + +-- | Translate 'Usage' into a string, used by 'usageString'. +usageToString :: Usage -> String +usageToString (UConstructor c) = c +usageToString (URecursive c) = c ++ "..." +usageToString (USelector False s u) = "[" ++ s ++ "] " ++ usageToString u +usageToString (USelector True s u) = s ++ " " ++ usageToString u +usageToString (UOptional u) = "[" ++ usageToString u ++ "]" +usageToString (USum ul ur) = concat [ "(" + , usageToString ul + , " | " + , usageToString ur + , ")"] +usageToString (UProduct u1 UUnit) = usageToString u1 +usageToString (UProduct u1 u2) = usageToString u1 ++ " " ++ usageToString u2 +usageToString UUnit = "" +usageToString (UType t) = t + +pFieldSep :: Options -> Parsec String m () +pFieldSep opt = + oneOf (NE.toList $ fieldSeparators opt) *> pure () + +pRecordSep :: Options -> Parsec String m () +pRecordSep opt = + choice (eof + : map (\c -> char c *> pure ()) + (NE.toList $ recordSeparators opt)) + +pString :: Options -> Parsec String m String +pString opt = + (try (quotedString "quoted string")) + <|> (unquotedString "unquoted string") + where + endChars = NE.toList (fieldSeparators opt) + ++ NE.toList (recordSeparators opt) + unquotedString = do + c <- escapedChar endChars + s <- manyTill (escapedChar endChars) + (lookAhead $ eof <|> oneOf endChars *> pure ()) + pure (c:s) + escapedChar ecs = (char '\\' *> oneOf ('\\' : ecs)) <|> anyChar + quotedString = char '"' + *> manyTill (escapedChar "\"") (char '"') + +-- | Parses a time argument. +pTime :: ParseTime a => Options -> String -> Parser a +pTime opt tf = try $ do + x <- pString opt + case readSTime False (timeLocale opt) tf x of + [(t, "")] -> pure t + _ -> fail "Failed to parse time" + +-- | Read an argument using its 'Read' instance. +pRead :: Read a => Options -> Parser a +pRead opt = do + x <- pString opt + case reads x of + [(n, "")] -> pure n + _ -> fail $ "Failed to read: " ++ x + +-- | Coalpit class: parsing, printing, usage strings. +class Coalpit a where + coalpitParser :: Options -> Parser a + default coalpitParser :: (Generic a, GCoalpit (Rep a)) => Options -> Parser a + coalpitParser opt = to <$> gCoalpitParser opt + + coalpitPrint :: Options -> a -> [String] + default coalpitPrint :: (Generic a, GCoalpit (Rep a)) => Options -> a -> [String] + coalpitPrint opt a = gCoalpitPrint opt (from a) + + coalpitDescription :: Options -> [String] -> Proxy a -> Usage + default coalpitDescription :: (GCoalpit (Rep a)) + => Options -> [String] -> Proxy a -> Usage + coalpitDescription opt path Proxy = + gCoalpitDescription opt path (Proxy :: Proxy (Rep a p)) + +class GCoalpit a where + gCoalpitParser :: Options -> Parser (a p) + gCoalpitPrint :: Options -> a p -> [String] + gCoalpitDescription :: Options -> [String] -> Proxy (a p) -> Usage + + +-- Units +instance GCoalpit U1 where + gCoalpitParser _ = pure U1 + gCoalpitPrint _ U1 = [] + gCoalpitDescription _ _ (Proxy :: Proxy (U1 f)) = UUnit + + +-- Products +instance (GCoalpit a, GCoalpit b) => GCoalpit (a :*: b) where + gCoalpitParser opt = + ((:*:) <$> + (gCoalpitParser opt <* pFieldSep opt) <*> gCoalpitParser opt) + "product" + gCoalpitPrint opt (x :*: y) = + gCoalpitPrint opt x ++ gCoalpitPrint opt y + gCoalpitDescription opt path (Proxy :: Proxy ((a :*: b) p)) = + UProduct (gCoalpitDescription opt path (Proxy :: Proxy (a p))) + (gCoalpitDescription opt path (Proxy :: Proxy (b p))) + + +-- Sums +instance + (GCoalpit a, GCoalpit b) => GCoalpit (a :+: b) where + gCoalpitParser opt = + (try (L1 <$> gCoalpitParser opt)) + <|> + (R1 <$> gCoalpitParser opt) + gCoalpitPrint opt (L1 x) = gCoalpitPrint opt x + gCoalpitPrint opt (R1 x) = gCoalpitPrint opt x + gCoalpitDescription opt path (Proxy :: Proxy ((a :+: b) p)) = + USum (gCoalpitDescription opt path (Proxy :: Proxy (a p))) + (gCoalpitDescription opt path (Proxy :: Proxy (b p))) + +-- Record Selectors + +parseS1 :: (GCoalpit a) => String -> Options -> Parser (S1 selA a p) +parseS1 nameA opt = + let sName = case (nameA, selNamePolicy opt) of + ("", _) -> pure () + (_, SNDisable) -> pure () + (_, SNRequire) -> string (selNameMod opt nameA) *> pFieldSep opt + (_, _) -> optional + (try $ (string (selNameMod opt nameA)) *> pFieldSep opt) + in M1 <$> (sName *> gCoalpitParser opt) + +printS1 :: (GCoalpit a, Selector selA) => Options -> S1 selA a p -> [String] +printS1 opt sel@(M1 x) = case (selName sel, selNamePolicy opt) of + ("", _) -> gCoalpitPrint opt x + (_, SNDisable) -> gCoalpitPrint opt x + (_, SNAvoid) -> gCoalpitPrint opt x + (name, _) -> selNameMod opt name : gCoalpitPrint opt x + +helpS1 :: (GCoalpit a) + => String -> Options -> [String] -> Proxy (S1 selA a p) -> Usage +helpS1 nameA opt path (Proxy :: Proxy (S1 selA a p)) = + case (nameA, selNamePolicy opt) of + ("", _) -> gCoalpitDescription opt path (Proxy :: Proxy (a p)) + (_, SNDisable) -> gCoalpitDescription opt path (Proxy :: Proxy (a p)) + (_, snpol) -> USelector (snpol == SNRequire) (selNameMod opt nameA) + (gCoalpitDescription opt path (Proxy :: Proxy (a p))) + +instance (GCoalpit a, Selector selA) => GCoalpit (S1 selA a) where + gCoalpitParser = parseS1 (selName (undefined :: S1 selA a p)) + gCoalpitPrint = printS1 + gCoalpitDescription = helpS1 (selName (undefined :: S1 selA a p)) + +-- Constructors + +-- | Handles recursive constructors. +handleRecCon :: GCoalpit a + => String + -- ^ Constructor name + -> Options + -> [String] + -> Proxy (a p) + -> Usage +handleRecCon nameA opt path (Proxy :: Proxy (a p)) = + let n = conNameMod opt nameA + in if nameA `elem` path + then URecursive n + else UProduct (UConstructor n) + (gCoalpitDescription opt (nameA : path) (Proxy :: Proxy (a p))) + +-- A constructor wrapping just a unit: no field separator is required +-- after such a constructor. +instance {-#OVERLAPPING#-} (Constructor conA) => GCoalpit (C1 conA U1) where + gCoalpitParser opt = + ((string + (conNameMod opt $ conName (undefined :: C1 conA U1 w)) + "constructor name")) + *> (fmap M1 (gCoalpitParser opt)) + gCoalpitPrint opt (M1 x) = conNameMod opt (conName (undefined :: C1 conA U1 w)) + : gCoalpitPrint opt x + gCoalpitDescription opt path (Proxy :: Proxy (C1 conA U1 p)) = + (handleRecCon (conName (undefined :: C1 conA U1 w)) opt path + (Proxy :: Proxy (U1 p))) + +-- A constructor with non-unit children, with a field separator +-- between constructor name and its children. +instance (GCoalpit a, Constructor conA) => GCoalpit (C1 conA a) where + gCoalpitParser opt = + ((string + (conNameMod opt $ conName (undefined :: C1 conA a w)) + "constructor name")) + *> (pFieldSep opt) + *> (fmap M1 (gCoalpitParser opt)) + gCoalpitPrint opt (M1 x) = conNameMod opt (conName (undefined :: C1 conA a w)) + : gCoalpitPrint opt x + gCoalpitDescription opt path (Proxy :: Proxy (C1 conA a p)) = + (handleRecCon (conName (undefined :: C1 conA a w)) opt path + (Proxy :: Proxy (a p))) + +-- Data types +instance (GCoalpit a) => GCoalpit (D1 conA a) where + gCoalpitParser = fmap M1 . gCoalpitParser + gCoalpitPrint opt (M1 x) = gCoalpitPrint opt x + gCoalpitDescription opt path (Proxy :: Proxy (D1 conA a p)) = + gCoalpitDescription opt path (Proxy :: Proxy (a p)) + +-- Constraints and such +instance (Coalpit a) => GCoalpit (K1 i a) where + gCoalpitParser = fmap K1 . coalpitParser + gCoalpitPrint opt (K1 x) = coalpitPrint opt x + gCoalpitDescription opt path (Proxy :: Proxy (K1 x a p)) = + coalpitDescription opt path (Proxy :: Proxy a) + + +-- Common types + +instance Coalpit Int where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "INT" + +instance Coalpit Integer where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "INTEGER" + +instance Coalpit Word8 where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "WORD8" + +instance Coalpit Word16 where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "WORD16" + +instance Coalpit Word32 where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "WORD32" + +instance Coalpit Word64 where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "WORD64" + +instance Coalpit Int8 where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "INT8" + +instance Coalpit Int16 where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "INT16" + +instance Coalpit Int32 where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "INT32" + +instance Coalpit Int64 where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "INT64" + +instance Coalpit Natural where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "NATURAL" + +instance Coalpit Rational where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "RATIONAL" + +instance Coalpit Double where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "DOUBLE" + +instance Coalpit Float where + coalpitParser opt = pRead opt + coalpitPrint _ i = [show i] + coalpitDescription _ _ _ = UType "FLOAT" + +instance Coalpit Char where + coalpitParser opt = pRead opt + coalpitPrint _ c = [show c] + coalpitDescription _ _ _ = UType "CHAR" + + +instance {-#OVERLAPPING#-} Coalpit String where + coalpitParser opt = pString opt + coalpitPrint _ i = [i] + coalpitDescription _ _ _ = UType "STRING" + +instance Coalpit Scientific where + coalpitParser opt = try $ do + x <- pString opt + case reverse $ readP_to_S scientificP x of + (n, ""):_ -> pure n + _ -> fail $ "Failed to read a scientific number: " ++ x + coalpitPrint opt n = [formatScientific + (scientificFormat opt) (scientificDecimals opt) n] + coalpitDescription _ _ _ = UType "SCIENTIFIC" + +instance Coalpit Version where + coalpitParser opt = try $ do + x <- pString opt + case reverse $ readP_to_S parseVersion x of + (v, ""):_ -> pure v + _ -> fail $ "Failed to read a version: " ++ x + coalpitPrint _ v = [showVersion v] + coalpitDescription _ _ _ = UType "VERSION" + + +-- | An URI reference (absolute or relative). +instance Coalpit URI where + coalpitParser opt = try $ do + x <- pString opt + maybe (fail $ "Failed to parse URI: " ++ x) pure (parseURIReference x) + coalpitPrint opt u = [uriToString (uriUserInfo opt) u ""] + coalpitDescription _ _ _ = UType "URI" + + +-- | Uses 'dateTimeFormat'. +instance Coalpit UTCTime where + coalpitParser opt = pTime opt (dateTimeFormat opt) + coalpitPrint opt t = [formatTime (timeLocale opt) (dateTimeFormat opt) t] + coalpitDescription _ _ _ = UType "UTC_TIME" + +-- | Uses 'dateTimeFormat'. +instance Coalpit ZonedTime where + coalpitParser opt = pTime opt (dateTimeFormat opt) + coalpitPrint opt t = [formatTime (timeLocale opt) (dateTimeFormat opt) t] + coalpitDescription _ _ _ = UType "ZONED_TIME" + +-- | Uses 'dateTimeFormat'. +instance Coalpit LocalTime where + coalpitParser opt = pTime opt (dateTimeFormat opt) + coalpitPrint opt t = [formatTime (timeLocale opt) (dateTimeFormat opt) t] + coalpitDescription _ _ _ = UType "LOCAL_TIME" + +-- | Uses 'dateTimeFormat'. +instance Coalpit UniversalTime where + coalpitParser opt = pTime opt (dateTimeFormat opt) + coalpitPrint opt t = [formatTime (timeLocale opt) (dateTimeFormat opt) t] + coalpitDescription _ _ _ = UType "UNIVERSAL_TIME" + +-- | Uses 'timeFormat'. +instance Coalpit TimeOfDay where + coalpitParser opt = pTime opt (timeFormat opt) + coalpitPrint opt t = [formatTime (timeLocale opt) (timeFormat opt) t] + coalpitDescription _ _ _ = UType "TIME_OF_DAY" + +-- | Uses 'dateFormat'. +instance Coalpit Day where + coalpitParser opt = pTime opt (dateFormat opt) + coalpitPrint opt t = [formatTime (timeLocale opt) (dateFormat opt) t] + coalpitDescription _ _ _ = UType "DAY" + +-- | Converts to/from 'Scientific'. +instance Coalpit NominalDiffTime where + coalpitParser opt = fromRational . toRational + <$> (coalpitParser opt :: Parser Scientific) + coalpitPrint opt = coalpitPrint opt . + (fromRational . toRational :: NominalDiffTime -> Scientific) + coalpitDescription _ _ _ = UType "NOMINAL_DIFF_TIME" + +-- | Converts to/from 'Scientific'. +instance Coalpit DiffTime where + coalpitParser opt = fromRational . toRational + <$> (coalpitParser opt :: Parser Scientific) + coalpitPrint opt = coalpitPrint opt . + (fromRational . toRational :: DiffTime -> Scientific) + coalpitDescription _ _ _ = UType "DIFF_TIME" -import Coalpit.Core -import Coalpit.DSV +instance Coalpit () +instance Coalpit Bool +instance Coalpit Ordering +instance Coalpit ExitCode +instance Coalpit a => Coalpit (Complex a) +instance Coalpit a => Coalpit (Maybe a) +instance Coalpit a => Coalpit [a] +instance Coalpit a => Coalpit (NonEmpty a) +instance (Coalpit a, Coalpit b) => Coalpit (Either a b) +instance (Coalpit a, Coalpit b) => Coalpit (a, b) +instance (Coalpit a, Coalpit b, Coalpit c) => Coalpit (a, b, c) +instance (Coalpit a, Coalpit b, Coalpit c, Coalpit d) => Coalpit (a, b, c, d) diff --git a/Coalpit/Core.hs b/Coalpit/Core.hs deleted file mode 100644 index f8e90be..0000000 --- a/Coalpit/Core.hs +++ /dev/null @@ -1,504 +0,0 @@ -{- | -Module : Coalpit.Core -Description : Core Coalpit definitions -Maintainer : defanor -Stability : unstable -Portability : non-portable (uses GHC extensions) - -The 'Coalpit' class with instances, a few functions to work with it, -and 'Options' are defined here. --} - -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE DefaultSignatures #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE ScopedTypeVariables #-} - -module Coalpit.Core ( Coalpit(..) - , fromArgs - -- * Usage - , Usage(..) - , usage - , usageString - -- * Options - , Options(..) - , defOpt - ) where - -import GHC.Generics -import Text.Megaparsec -import Text.Megaparsec.Char -import Data.Char (toLower) -import Data.Proxy (Proxy(..)) -import qualified Data.List.NonEmpty as NE -import Data.Word (Word8, Word16, Word32, Word64) -import Numeric.Natural (Natural) -import Data.Int (Int8, Int16, Int32, Int64) -import Data.Time.Clock (DiffTime, NominalDiffTime, UniversalTime, UTCTime) -import Data.Time.Format ( TimeLocale, formatTime - , iso8601DateFormat, defaultTimeLocale) -import Data.Time.Calendar (Day) -import Data.Time.LocalTime (TimeOfDay, LocalTime, ZonedTime) -import Data.Scientific (Scientific, FPFormat(..), formatScientific, scientificP) -import Text.ParserCombinators.ReadP (readP_to_S) -import Data.Complex (Complex) -import Data.Version (Version, parseVersion, showVersion) -import System.Exit (ExitCode) -import Network.URI (URI, parseURIReference, uriToString) - -import Coalpit.Parsing - --- | Usage description: can be translated into help messages or --- documentation formats. -data Usage = UConstructor String - -- ^ Data constructor. - | URecursive String - -- ^ Constructor of a recursive data structure (its second - -- appearance in the tree). - | USelector String Usage - -- ^ Record selector. - | UOptional Usage - -- ^ Optional element. - | USum Usage Usage - -- ^ Sum. - | UProduct Usage Usage - -- ^ Product. - | UUnit - -- ^ Unit. - | UType String - -- ^ Type name, e.g. \"INT\". - deriving (Show) - --- | Printing and parsing options. -data Options = Options { fieldSeparator :: Char - -- ^ DSV field separator ('showDSV', - -- 'readDSV'). - , conNameMod :: String -> String - -- ^ Constructor name modifier. - , selNameMod :: String -> String - -- ^ Record selector name modifier. - , alwaysUseSelName :: Bool - -- ^ Add record selector name always, not just - -- for optional arguments. - , omitNamedOptions :: Bool - -- ^ Omit named Maybe values to indicate - -- 'Nothing'. - , timeLocale :: TimeLocale - , dateFormat :: String - -- ^ See "Data.Time.Format". - , timeFormat :: String - , dateTimeFormat :: String - , scientificFormat :: FPFormat - , scientificDecimals :: Maybe Int - , uriUserInfo :: String -> String - -- ^ Used to map userinfo parts of URIs. - } - --- | Default options. -defOpt :: Options -defOpt = Options ' ' (map toLower) (("--" ++) . map toLower) False True - defaultTimeLocale (iso8601DateFormat Nothing) "%H:%M:%S" - (iso8601DateFormat (Just "%H:%M:%S")) Generic Nothing id - --- | Parses arguments. -fromArgs :: Coalpit a => Options -> [String] -> Either String a -fromArgs opt args = case parse (argParser opt) "arguments" (map CLArg args) of - Left err -> Left $ parseErrorPretty err - Right x -> Right x - --- | Composes 'Usage' description. -usage :: Coalpit a => Options -> Proxy a -> Usage -usage opt = argHelper opt [] - --- | Composes a usage string. -usageString :: Coalpit a => Options -> Proxy a -> String -usageString opt = usageToString . usage opt - --- | Translates 'Usage' into a string, used by 'usageString'. -usageToString :: Usage -> String -usageToString (UConstructor c) = c -usageToString (URecursive c) = c ++ "..." -usageToString (USelector s u) = s ++ " " ++ usageToString u -usageToString (UOptional u) = "[" ++ usageToString u ++ "]" -usageToString (USum ul ur) = concat [ "(" - , usageToString ul - , " | " - , usageToString ur - , ")"] -usageToString (UProduct u1 UUnit) = usageToString u1 -usageToString (UProduct u1 u2) = usageToString u1 ++ " " ++ usageToString u2 -usageToString UUnit = "" -usageToString (UType t) = t - - --- | Coalpit class: parsing, printing, usage strings. -class Coalpit a where - argParser :: Options -> Parser a - default argParser :: (Generic a, GCoalpit (Rep a)) => Options -> Parser a - argParser opt = to <$> gArgParser opt - - toArgs :: Options -> a -> [String] - default toArgs :: (Generic a, GCoalpit (Rep a)) => Options -> a -> [String] - toArgs opt a = gToArgs opt (from a) - - argHelper :: Options -> [String] -> Proxy a -> Usage - default argHelper :: (GCoalpit (Rep a)) - => Options -> [String] -> Proxy a -> Usage - argHelper opt path Proxy = gArgHelper opt path (Proxy :: Proxy (Rep a p)) - -class GCoalpit a where - gArgParser :: Options -> Parser (a p) - gToArgs :: Options -> a p -> [String] - gArgHelper :: Options -> [String] -> Proxy (a p) -> Usage - - --- Units -instance GCoalpit U1 where - gArgParser _ = pure U1 - gToArgs _ U1 = [] - gArgHelper _ _ (Proxy :: Proxy (U1 f)) = UUnit - - --- Products -instance (GCoalpit a, GCoalpit b) => GCoalpit (a :*: b) where - gArgParser opt = (:*:) <$> gArgParser opt <*> gArgParser opt - gToArgs opt (x :*: y) = gToArgs opt x ++ gToArgs opt y - gArgHelper opt path (Proxy :: Proxy ((a :*: b) p)) = - UProduct (gArgHelper opt path (Proxy :: Proxy (a p))) - (gArgHelper opt path (Proxy :: Proxy (b p))) - - --- Sums - --- | Handles recursive constructors. -handleRecCon :: GCoalpit a - => String - -- ^ Constructor name - -> Options - -> [String] - -> Proxy (a p) - -> Usage -handleRecCon nameA opt path (Proxy :: Proxy (a p)) = - let n = conNameMod opt nameA - in if nameA `elem` path - then URecursive n - else UProduct (UConstructor n) - (gArgHelper opt (nameA : path) (Proxy :: Proxy (a p))) - -instance (Constructor conA, GCoalpit a, GCoalpit (b :+: c)) => - GCoalpit ((b :+: c) :+: C1 conA a) where - gArgParser opt = - L1 <$> gArgParser opt - <|> - R1 <$> (pS (string (conNameMod opt $ conName (undefined :: C1 conA a p))) - *> gArgParser opt) - gToArgs opt (L1 x) = gToArgs opt x - gToArgs opt (R1 x) = conNameMod opt (conName x) : gToArgs opt x - gArgHelper opt path (Proxy :: Proxy (((b :+: c) :+: C1 conA a) p)) = - let nameA = conName (undefined :: C1 conA f p) - in USum (gArgHelper opt path (Proxy :: Proxy ((b :+: c) p))) - (handleRecCon nameA opt path (Proxy :: Proxy (a p))) - -instance (Constructor conA, GCoalpit a, GCoalpit (b :+: c)) => - GCoalpit (C1 conA a :+: (b :+: c)) where - gArgParser opt = - L1 <$> (pS (string (conNameMod opt $ conName (undefined :: C1 conA a p))) - *> gArgParser opt) - <|> - R1 <$> gArgParser opt - gToArgs opt (L1 x) = conNameMod opt (conName x) : gToArgs opt x - gToArgs opt (R1 x) = gToArgs opt x - gArgHelper opt path (Proxy :: Proxy ((C1 conA a :+: (b :+: c)) p)) = - let nameA = conName (undefined :: C1 conA a p) - in USum (handleRecCon nameA opt path (Proxy :: Proxy (a p))) - (gArgHelper opt path (Proxy :: Proxy ((b :+: c) p))) - -instance (Constructor conA, Constructor conB, GCoalpit a, GCoalpit b) => - GCoalpit (C1 conA a :+: C1 conB b) where - gArgParser opt = - L1 <$> (pS (string (conNameMod opt $ - conName (undefined :: C1 conA a p))) - *> gArgParser opt) - <|> - R1 <$> (pS (string (conNameMod opt $ - conName (undefined :: C1 conB b p))) - *> gArgParser opt) - gToArgs opt (L1 x) = conNameMod opt (conName x) : gToArgs opt x - gToArgs opt (R1 x) = conNameMod opt (conName x) : gToArgs opt x - gArgHelper opt path (Proxy :: Proxy ((C1 conA a :+: C1 conB b) p)) = - let nameA = conName (undefined :: C1 conA a p) - nameB = conName (undefined :: C1 conB b p) - in USum (handleRecCon nameA opt path (Proxy :: Proxy (a p))) - (handleRecCon nameB opt path (Proxy :: Proxy (b p))) - -instance {-# OVERLAPPABLE #-} - (GCoalpit a, GCoalpit b) => GCoalpit (a :+: b) where - gArgParser opt = - L1 <$> gArgParser opt - <|> - R1 <$> gArgParser opt - gToArgs opt (L1 x) = gToArgs opt x - gToArgs opt (R1 x) = gToArgs opt x - gArgHelper opt path (Proxy :: Proxy ((a :+: b) p)) = - -- let nameA = conName (undefined :: a p) - -- in - USum (gArgHelper opt path (Proxy :: Proxy (a p))) - (gArgHelper opt path (Proxy :: Proxy (b p))) - --- Record Selectors - -parseS1 :: (GCoalpit a) => String -> Options -> Parser (S1 selA a p) -parseS1 nameA opt = - let sName = case (nameA, alwaysUseSelName opt) of - ("", _) -> pure () - (_, False) -> pure () - (_, True) -> pS (string (selNameMod opt nameA)) >> pure () - in M1 <$> (sName *> gArgParser opt) - -printS1 :: (GCoalpit a, Selector selA) => Options -> S1 selA a p -> [String] -printS1 opt sel@(M1 x) = case (selName sel, alwaysUseSelName opt) of - ("", _) -> gToArgs opt x - (_, False) -> gToArgs opt x - (name, True) -> selNameMod opt name : gToArgs opt x - -helpS1 :: (GCoalpit a) - => String -> Options -> [String] -> Proxy (S1 selA a p) -> Usage -helpS1 nameA opt path (Proxy :: Proxy (S1 selA a p)) = - case (nameA, alwaysUseSelName opt) of - ("", _) -> gArgHelper opt path (Proxy :: Proxy (a p)) - (_, False) -> gArgHelper opt path (Proxy :: Proxy (a p)) - (_, True) -> USelector (selNameMod opt nameA) - (gArgHelper opt path (Proxy :: Proxy (a p))) - -instance (GCoalpit a, Selector selA) => GCoalpit (S1 selA a) where - gArgParser = parseS1 (selName (undefined :: S1 selA a p)) - gToArgs = printS1 - gArgHelper = helpS1 (selName (undefined :: S1 selA a p)) - --- Optional arguments -instance {-#OVERLAPPING#-} - (Coalpit a, Coalpit (Maybe a), Selector selA) => - GCoalpit (S1 selA (Rec0 (Maybe a))) where - gArgParser opt = - let nameA = selName (undefined :: S1 selA (Rec0 (Maybe a)) p) - in case (omitNamedOptions opt, null nameA) of - (True, True) -> M1 <$> gArgParser opt - (True, False) -> - M1 . K1 - <$> optional (pS (string (selNameMod opt nameA)) *> argParser opt) - _ -> parseS1 nameA opt - gToArgs opt sel@(M1 (K1 x)) - | omitNamedOptions opt = case (selName sel, x) of - ("", _) -> toArgs opt x - (_, Nothing) -> [] - (nameA, Just x') -> selNameMod opt nameA : toArgs opt x' - | otherwise = printS1 opt sel - gArgHelper opt path (Proxy :: Proxy (S1 selA (Rec0 (Maybe a)) p)) = - let nameA = selName (undefined :: S1 selA (Rec0 (Maybe a)) p) - in case (omitNamedOptions opt, null nameA) of - (True, True) -> gArgHelper opt path (Proxy :: Proxy (Rec0 (Maybe a) p)) - (True, False) -> UOptional $ USelector (selNameMod opt nameA) - (gArgHelper opt path (Proxy :: Proxy (Rec0 a p))) - _ -> helpS1 nameA opt path (Proxy :: Proxy (S1 selA (Rec0 (Maybe a)) p)) - - --- Constructors - -instance (GCoalpit a) => GCoalpit (C1 conA a) where - gArgParser = fmap M1 . gArgParser - gToArgs opt (M1 x) = gToArgs opt x - gArgHelper opt path (Proxy :: Proxy (C1 conA a p)) = - gArgHelper opt path (Proxy :: Proxy (a p)) - --- Data types -instance (GCoalpit a) => GCoalpit (D1 conA a) where - gArgParser = fmap M1 . gArgParser - gToArgs opt (M1 x) = gToArgs opt x - gArgHelper opt path (Proxy :: Proxy (D1 conA a p)) = - gArgHelper opt path (Proxy :: Proxy (a p)) - --- Constraints and such -instance (Coalpit a) => GCoalpit (K1 i a) where - gArgParser = fmap K1 . argParser - gToArgs opt (K1 x) = toArgs opt x - gArgHelper opt path (Proxy :: Proxy (K1 x a p)) = - argHelper opt path (Proxy :: Proxy a) - - --- Common types - -instance Coalpit Int where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "INT" - -instance Coalpit Integer where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "INTEGER" - -instance Coalpit Word8 where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "WORD8" - -instance Coalpit Word16 where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "WORD16" - -instance Coalpit Word32 where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "WORD32" - -instance Coalpit Word64 where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "WORD64" - -instance Coalpit Int8 where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "INT8" - -instance Coalpit Int16 where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "INT16" - -instance Coalpit Int32 where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "INT32" - -instance Coalpit Int64 where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "INT64" - -instance Coalpit Natural where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "NATURAL" - -instance Coalpit Rational where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "RATIONAL" - -instance Coalpit Double where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "DOUBLE" - -instance Coalpit Float where - argParser _ = readArg - toArgs _ i = [show i] - argHelper _ _ _ = UType "FLOAT" - -instance Coalpit Char where - argParser _ = readArg - toArgs _ c = [show c] - argHelper _ _ _ = UType "CHAR" - -instance {-#OVERLAPPING#-} Coalpit String where - argParser _ = token (Right . unArg) Nothing - toArgs _ i = [i] - argHelper _ _ _ = UType "STRING" - -instance Coalpit Scientific where - argParser _ = try $ do - x <- token (Right . unArg) Nothing - case reverse $ readP_to_S scientificP x of - (n, ""):_ -> pure n - _ -> fail $ "Failed to read a scientific number: " ++ x - toArgs opt n = [formatScientific - (scientificFormat opt) (scientificDecimals opt) n] - argHelper _ _ _ = UType "SCIENTIFIC" - -instance Coalpit Version where - argParser _ = try $ do - x <- token (Right . unArg) Nothing - case reverse $ readP_to_S parseVersion x of - (v, ""):_ -> pure v - _ -> fail $ "Failed to read a version: " ++ x - toArgs _ v = [showVersion v] - argHelper _ _ _ = UType "VERSION" - --- | An URI reference (absolute or relative). -instance Coalpit URI where - argParser _ = try $ do - x <- token (Right . unArg) Nothing - maybe (fail $ "Failed to parse URI: " ++ x) pure (parseURIReference x) - toArgs opt u = [uriToString (uriUserInfo opt) u ""] - argHelper _ _ _ = UType "URI" - - --- | Uses 'dateTimeFormat'. -instance Coalpit UTCTime where - argParser opt = pTime (timeLocale opt) (dateTimeFormat opt) - toArgs opt t = [formatTime (timeLocale opt) (dateTimeFormat opt) t] - argHelper _ _ _ = UType "UTC_TIME" - --- | Uses 'dateTimeFormat'. -instance Coalpit ZonedTime where - argParser opt = pTime (timeLocale opt) (dateTimeFormat opt) - toArgs opt t = [formatTime (timeLocale opt) (dateTimeFormat opt) t] - argHelper _ _ _ = UType "ZONED_TIME" - --- | Uses 'dateTimeFormat'. -instance Coalpit LocalTime where - argParser opt = pTime (timeLocale opt) (dateTimeFormat opt) - toArgs opt t = [formatTime (timeLocale opt) (dateTimeFormat opt) t] - argHelper _ _ _ = UType "LOCAL_TIME" - --- | Uses 'dateTimeFormat'. -instance Coalpit UniversalTime where - argParser opt = pTime (timeLocale opt) (dateTimeFormat opt) - toArgs opt t = [formatTime (timeLocale opt) (dateTimeFormat opt) t] - argHelper _ _ _ = UType "UNIVERSAL_TIME" - --- | Uses 'timeFormat'. -instance Coalpit TimeOfDay where - argParser opt = pTime (timeLocale opt) (timeFormat opt) - toArgs opt t = [formatTime (timeLocale opt) (timeFormat opt) t] - argHelper _ _ _ = UType "TIME_OF_DAY" - --- | Uses 'dateFormat'. -instance Coalpit Day where - argParser opt = pTime (timeLocale opt) (dateFormat opt) - toArgs opt t = [formatTime (timeLocale opt) (dateFormat opt) t] - argHelper _ _ _ = UType "DAY" - --- | Converts to/from 'Scientific'. -instance Coalpit NominalDiffTime where - argParser opt = fromRational . toRational - <$> (argParser opt :: Parser Scientific) - toArgs opt = toArgs opt . - (fromRational . toRational :: NominalDiffTime -> Scientific) - argHelper _ _ _ = UType "NOMINAL_DIFF_TIME" - --- | Converts to/from 'Scientific'. -instance Coalpit DiffTime where - argParser opt = fromRational . toRational - <$> (argParser opt :: Parser Scientific) - toArgs opt = toArgs opt . - (fromRational . toRational :: DiffTime -> Scientific) - argHelper _ _ _ = UType "DIFF_TIME" - - -instance Coalpit () -instance Coalpit Bool -instance Coalpit Ordering -instance Coalpit ExitCode -instance Coalpit a => Coalpit (Complex a) -instance Coalpit a => Coalpit (Maybe a) -instance Coalpit a => Coalpit [a] -instance Coalpit a => Coalpit (NE.NonEmpty a) -instance (Coalpit a, Coalpit b) => Coalpit (Either a b) -instance (Coalpit a, Coalpit b) => Coalpit (a, b) -instance (Coalpit a, Coalpit b, Coalpit c) => Coalpit (a, b, c) -instance (Coalpit a, Coalpit b, Coalpit c, Coalpit d) => Coalpit (a, b, c, d) diff --git a/Coalpit/DSV.hs b/Coalpit/DSV.hs deleted file mode 100644 index 890399d..0000000 --- a/Coalpit/DSV.hs +++ /dev/null @@ -1,59 +0,0 @@ -{- | -Module : Coalpit.DSV -Description : DSV printing and parsing -Maintainer : defanor -Stability : unstable -Portability : non-portable (uses GHC extensions) - -This module provides functions for DSV printing and parsing. --} - -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE FlexibleInstances #-} - -module Coalpit.DSV (showDSV, readDSV) where - -import Data.List -import Text.Megaparsec -import Text.Megaparsec.Char -import Data.Void - -import Coalpit.Core - - -composeDSVLine :: Char -> [String] -> String -composeDSVLine fs = intercalate [fs] . map escapeVal - where - escapeVal :: String -> String - -- not great, but will do for now - escapeVal s = let inner = show s - in if fs `elem` inner - then inner - else init $ tail inner - -pStr :: Char -> Parsec Void String String -pStr fs = do - s <- try (between (char '"') (char '"') - (concat <$> many (string "\\\\" - <|> string "\\\"" - <|> pure <$> notChar '"'))) - <|> many (notChar fs) - case reads (concat ["\"", s, "\""]) of - [(str, "")] -> pure str - other -> fail $ "Failed to read a string: " ++ show other ++ "(" ++ s ++ ")" - -pDSVLine :: Char -> Parsec Void String [String] -pDSVLine fs = pStr fs `sepBy` char fs - -parseDSVLine :: Char -> String -> Either String [String] -parseDSVLine fs l = case parse (pDSVLine fs) "line" l of - Left err -> Left $ parseErrorPretty err - Right x -> Right x - --- | Shows values in DSV format. -showDSV :: Coalpit a => Options -> a -> String -showDSV opt = composeDSVLine (fieldSeparator opt) . toArgs opt - --- | Reads values from DSV format. -readDSV :: Coalpit a => Options -> String -> Either String a -readDSV opt = (>>= fromArgs opt) . parseDSVLine (fieldSeparator opt) diff --git a/Coalpit/Parsing.hs b/Coalpit/Parsing.hs deleted file mode 100644 index f8fe8fc..0000000 --- a/Coalpit/Parsing.hs +++ /dev/null @@ -1,90 +0,0 @@ -{- | -Module : Coalpit.Parsing -Description : Argument parsing facilities -Maintainer : defanor -Stability : unstable -Portability : non-portable (uses GHC extensions) - -This module provides functions useful for argument parsing. --} - -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE FlexibleInstances #-} - -module Coalpit.Parsing ( Parser - , CLArg(..) - , pS - , readArg - , pTime - ) where - -import Text.Megaparsec -import Data.Proxy (Proxy(..)) -import Data.Time.Format (TimeLocale, ParseTime, readSTime) -import Data.Void (Void) -import qualified Data.List.NonEmpty as NE -import Data.List (foldl') -import Data.Semigroup ((<>)) - --- | Command-line argument wrapper, used to avoid orphan ShowToken --- String and Stream [String] instances. -newtype CLArg = CLArg { unArg :: String } - deriving (Ord, Eq) - --- | Advances by one token. -advance :: Pos -> SourcePos -> t -> SourcePos -advance _ (SourcePos n l c) _ = SourcePos n l (c <> pos1) - --- | A list of strings (command-line arguments) stream. -instance Stream [CLArg] where - type Token [CLArg] = CLArg - type Tokens [CLArg] = [CLArg] - tokenToChunk Proxy = pure - tokensToChunk Proxy = id - chunkToTokens Proxy = id - chunkLength Proxy = length - chunkEmpty Proxy = null - advance1 Proxy = advance - advanceN Proxy w = foldl' (advance w) - take1_ [] = Nothing - take1_ (t:ts) = Just (t, ts) - takeN_ n s - | n <= 0 = Just ([], s) - | null s = Nothing - | otherwise = Just (splitAt n s) - takeWhile_ = span - -instance ShowToken CLArg where - showTokens xs = concat $ NE.map unArg xs - --- | Command-line arguments parser. -type Parser = Parsec Void [CLArg] - --- | Applies a String parser to a single argument. -pS :: Parsec Void String a -> Parsec Void [CLArg] a -pS p = try $ do - x <- token (Right . unArg) Nothing - case parse p "argument" x of - Left e -> fail $ show e - Right x' -> pure x' - --- | Reads an argument using its 'Read' instance. -readArg :: Read a => Parser a -readArg = do - x <- token (Right . unArg) Nothing - case reads x of - [(n, "")] -> pure n - _ -> fail $ "Failed to read: " ++ x - --- | Parses a time argument. -pTime :: ParseTime a - => TimeLocale - -- ^ Options, to read 'timeLocale' from. - -> String - -- ^ Time format to use. - -> Parser a -pTime tl tf = try $ do - x <- token (Right . unArg) Nothing - case readSTime False tl tf x of - [(t, "")] -> pure t - _ -> fail "Failed to parse time" diff --git a/README.md b/README.md index 921ea8e..cd9d796 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,6 @@ e.g. [optparse-generic](https://hackage.haskell.org/package/optparse-generic), but the aim here is to handle more or less arbitrary types. -Warning: it is possible to run into ambiguity by defining a recursive -structure with optional named elements while using default options. -`omitNamedOptions` can be disabled to avoid that. - ## Example @@ -30,60 +26,45 @@ Input { something = Nothing , fooBar2 = Bar} ``` -With the default options, its serialized version should look like -this: +Its serialized version with the default options is: ```haskell -["--foobar","foo","1","a string","bar"] -``` - -What would look like this in a shell: - -```sh ---foobar foo 1 'a string' bar +input nothing just foo fooargs 1 "a string" bar ``` -And its usage string -- like this: +And its usage string: ``` -[--something STRING] [--foobar (foo INT STRING | bar)] (foo INT STRING | bar) -``` - -More verbose versions can be produced and parsed with -`alwaysUseSelName = True` and/or `omitNamedOptions = False`: - -```sh ---foobar foo --arg1 1 --arg2 'a string' --foobar2 bar -nothing just foo 1 'a string' bar ---something nothing --foobar just foo --arg1 1 --arg2 'a string' --foobar2 bar +input [--something] (nothing | just STRING) [--foobar] (nothing | just (foo fooargs [--arg1] INT [--arg2] STRING | bar)) [--foobar2] (foo fooargs [--arg1] INT [--arg2] STRING | bar) ``` -And here is output of the `help` function from the same file, with all -the (alwaysUseSelName, omitNamedOptions) combinations: +Other versions can be produced by varying selector name policy. Below +are triples of a policy, a corresponding example serialization, and an +example usage string (output of the `help` function from the example): ``` -(True,True) ---foo : 1 : 2 : 3 [] --bar "a string" ---foo ([] | : INT ([] | :...)) [--bar STRING] -(True,True) ---foo : 1 : 2 : 3 [] ---foo ([] | : INT ([] | :...)) [--bar STRING] -(True,False) ---foo : 1 : 2 : 3 [] --bar just "a string" ---foo ([] | : INT ([] | :...)) --bar (nothing | just STRING) -(True,False) ---foo : 1 : 2 : 3 [] --bar nothing ---foo ([] | : INT ([] | :...)) --bar (nothing | just STRING) -(False,True) -: 1 : 2 : 3 [] --bar "a string" -([] | : INT ([] | :...)) [--bar STRING] -(False,True) -: 1 : 2 : 3 [] -([] | : INT ([] | :...)) [--bar STRING] -(False,False) -: 1 : 2 : 3 [] just "a string" -([] | : INT ([] | :...)) (nothing | just STRING) -(False,False) -: 1 : 2 : 3 [] nothing -([] | : INT ([] | :...)) (nothing | just STRING) +SNDisable +test : 1 : 2 : 3 [] just "a string" +test ([] | : INT ([] | :...)) (nothing | just STRING) +SNDisable +test : 1 : 2 : 3 [] nothing +test ([] | : INT ([] | :...)) (nothing | just STRING) +SNAvoid +test : 1 : 2 : 3 [] just "a string" +test [--foo] ([] | : INT ([] | :...)) [--bar] (nothing | just STRING) +SNAvoid +test : 1 : 2 : 3 [] nothing +test [--foo] ([] | : INT ([] | :...)) [--bar] (nothing | just STRING) +SNPrefer +test --foo : 1 : 2 : 3 [] --bar just "a string" +test [--foo] ([] | : INT ([] | :...)) [--bar] (nothing | just STRING) +SNPrefer +test --foo : 1 : 2 : 3 [] --bar nothing +test [--foo] ([] | : INT ([] | :...)) [--bar] (nothing | just STRING) +SNRequire +test --foo : 1 : 2 : 3 [] --bar just "a string" +test --foo ([] | : INT ([] | :...)) --bar (nothing | just STRING) +SNRequire +test --foo : 1 : 2 : 3 [] --bar nothing +test --foo ([] | : INT ([] | :...)) --bar (nothing | just STRING) ``` diff --git a/coalpit.cabal b/coalpit.cabal index 4a6462a..d064d84 100644 --- a/coalpit.cabal +++ b/coalpit.cabal @@ -1,7 +1,7 @@ name: coalpit -version: 0.1.1.1 -synopsis: Command-line options and DSV parsing and printing -description: This library generates parsers and printers for +version: 0.2.0.0 +synopsis: DSV (de)serialization +description: The library generates parsers and printers for given data types, in the form of command-line arguments or DSVs – so that they can be used to quickly get CLIs via a shared library, while @@ -16,21 +16,17 @@ build-type: Simple extra-source-files: ChangeLog.md , README.md , examples/Basic.hs - , examples/Pipes.hs cabal-version: >=1.10 -tested-with: GHC == 8.0.1, GHC == 8.0.2, GHC == 8.2.1, GHC == 8.2.2 +tested-with: GHC == 8.0.1, GHC == 9.0.2 bug-reports: https://github.com/defanor/coalpit/issues source-repository head type: git - location: https://git.uberspace.net/coalpit + location: https://github.com/defanor/coalpit library exposed-modules: Coalpit - , Coalpit.Core - , Coalpit.DSV - , Coalpit.Parsing build-depends: base >= 4.9 && < 5 - , megaparsec >= 6.2 && < 7 + , parsec >= 3 && < 4 , scientific >= 0.3 && < 1 , time >= 1.6 && < 2 , network-uri >= 2.6 && < 3 @@ -45,7 +41,6 @@ test-suite test-coalpit build-depends: base >= 4.9 && < 5 , coalpit , generic-random >= 1 && < 2 - , tasty >= 0.12 && < 1 + , tasty >= 1 && < 2 , tasty-quickcheck >= 0.9 && < 1 - , tasty-travis >= 0.2 && < 1 ghc-options: -Wall -Wno-unused-top-binds diff --git a/examples/Basic.hs b/examples/Basic.hs index 13ee95d..60306ca 100644 --- a/examples/Basic.hs +++ b/examples/Basic.hs @@ -24,10 +24,10 @@ main = do , fooBar = Just (Foo FooArgs { arg1 = 1 , arg2 = "a string"}) , fooBar2 = Bar} - args = toArgs defOpt val + dsv = toDSV defOpt val print val - print args - print (fromArgs defOpt args :: Either String Input) + print dsv + print (fromDSV defOpt dsv :: Either String Input) data Test = Test { foo :: [Int], bar :: Maybe String } deriving (Show, Generic, Coalpit) @@ -35,11 +35,9 @@ data Test = Test { foo :: [Int], bar :: Maybe String } help :: IO () help = do mapM_ (\(o, x, y) -> print o >> putStrLn x >> putStrLn y) $ - [ let opts = defOpt { alwaysUseSelName = ausn - , omitNamedOptions = ono } - in ( (ausn, ono) - , showDSV opts (Test [1,2,3] vals) + [ let opts = defOpt { selNamePolicy = snpol } + in ( snpol + , toDSV opts (Test [1,2,3] vals) , usageString opts (Proxy :: Proxy Test)) - | ausn <- [True, False] - , ono <- [True, False] + | snpol <- [SNDisable, SNAvoid, SNPrefer, SNRequire] , vals <- [Just "a string", Nothing]] diff --git a/examples/Pipes.hs b/examples/Pipes.hs deleted file mode 100644 index b1096e3..0000000 --- a/examples/Pipes.hs +++ /dev/null @@ -1,67 +0,0 @@ -{-# LANGUAGE RankNTypes, ScopedTypeVariables, DeriveGeneric, - DeriveAnyClass #-} - -module Coalpit.IO (runMain, runMain', handleErrors) where - -import Data.Proxy (Proxy(..)) -import System.Environment (getProgName, getArgs) -import Control.Monad.IO.Class (MonadIO, liftIO) -import System.Exit (die) -import System.IO (hPutStrLn, stderr) -import Pipes ((>->), Pipe, yield, await, lift, runEffect) -import Control.Monad (mapM_, forever) -import qualified Pipes.Prelude as PP -import Coalpit.Core (Coalpit, fromArgs, defOpt, usage) -import Coalpit.DSV (readDSV, showDSV) -import GHC.Generics - --- | Runs a given action on each 'Left' value, embedding that action's --- result into the data stream. -handleErrors :: MonadIO m => (e -> m [a]) -> Pipe (Either e a) a m () -handleErrors e = forever $ do - v <- await - case v of - Left err -> do - vs <- lift $ e err - mapM_ yield vs - Right x -> yield x - --- | Runs a given 'Pipe' between input producer and output consumer. --- Prints an error and usage instructions if it fails to parse the --- arguments, and passes the input through 'handleErrors'. -runMain :: forall m a i o. (MonadIO m, Coalpit a, Coalpit i, Coalpit o) - => (String -> m [i]) - -- ^ An action to run on error (see 'handleErrors'). - -> (a -> Pipe i o m ()) - -- ^ Main function. - -> m () -runMain e f = do - pn <- liftIO getProgName - let u = Prelude.concat [ "Usage: ", pn, " " - , usageString defOpt (Proxy :: Proxy a)] - args <- liftIO getArgs - a <- either (liftIO . die . (++ u)) pure $ fromArgs defOpt args - runEffect $ - PP.stdinLn - >-> PP.map (readDSV defOpt) - >-> handleErrors e - >-> f a - >-> PP.map (showDSV defOpt) - >-> PP.stdoutLn - --- | Same as 'runMain', but just prints errors into 'stderr'. -runMain' :: forall m a i o. (MonadIO m, Coalpit a, Coalpit i, Coalpit o) - => (a -> Pipe i o m ()) - -- ^ Main function. - -> m () -runMain' = runMain (\e -> liftIO $ hPutStrLn stderr e >> pure []) - - -data Args = Args { arg1 :: Maybe Int, arg2 :: Double } - deriving (Generic, Coalpit) -data Input = Input Double deriving (Generic, Coalpit) -data Output = Foo Double | Bar deriving (Generic, Coalpit) - -main :: IO () -main = runMain' $ \a -> PP.mapM $ \(Input i) -> - pure $ Foo $ maybe (arg2 a) fromIntegral (arg1 a) + i diff --git a/test/Test.hs b/test/Test.hs index b8d4121..f8476d7 100644 --- a/test/Test.hs +++ b/test/Test.hs @@ -5,11 +5,9 @@ import Generic.Random (genericArbitraryU) import Test.Tasty import Test.Tasty.QuickCheck as QC import Data.Proxy -import Test.Tasty.Travis import Data.Word import Data.Int import Data.Complex -import Data.Either import Coalpit @@ -88,16 +86,11 @@ instance Arbitrary RecordStrings where arbitrary = genericArbitraryU printAndParse :: (Coalpit a, Eq a) => Options -> Proxy a -> a -> Bool -printAndParse opt _ r = Right r == fromArgs opt (toArgs opt r) - -printAndParseDSV :: (Coalpit a, Eq a) - -- It would take a long time to test with [a], so - -- just repeating it 0--2 times. - => Options -> Proxy a -> (a, Int) -> Bool -printAndParseDSV opt _ (x, n) = - let xs = (replicate (n `mod` 3) x) - in xs == (rights . map (readDSV opt) . lines . unlines . map (showDSV opt) $ xs) +printAndParse opt _ r = Right r == fromDSV opt (toDSV opt r) +printAndParseList :: (Coalpit a, Eq a) + => Options -> Proxy a -> [a] -> Bool +printAndParseList opt _ l = Right l == fromDSVList opt (toDSVList opt l) variousTypes :: (forall a. (Coalpit a, Eq a, Show a, Arbitrary a) => Proxy a -> String -> TestTree) @@ -122,24 +115,20 @@ variousTypes f = variousOptions :: (Options -> [TestTree]) -> [TestTree] variousOptions tt = - [ testGroup (concat [ "alwaysUseSelName = ", show ausn - , ", omitNamedOptions = ", show ono]) - (tt defOpt { alwaysUseSelName = ausn - , omitNamedOptions = ono }) - | ausn <- [True, False] - , ono <- [True, False] + [ testGroup (concat [ "selNamePolicy = ", show snpol ]) + (tt defOpt { selNamePolicy = snpol }) + | snpol <- [SNDisable, SNAvoid, SNPrefer, SNRequire] ] qcProps :: TestTree qcProps = testGroup "Quickcheck properties" - [ testGroup "Right == fromArgs opt . toArgs opt" + [ testGroup "Right == fromDSV opt . toDSV opt" (variousOptions $ \opt -> variousTypes $ \p n -> QC.testProperty n (printAndParse opt p)) - , testGroup - "id == rights . map (readDSV opt) . lines . unlines . map (showDSV opt)" + , testGroup "Right == fromDSVList opt . toDSVList opt" (variousOptions $ \opt -> - variousTypes $ \p n -> QC.testProperty n (printAndParseDSV opt p)) + variousTypes $ \p n -> QC.testProperty n (printAndParseList opt p)) ] main :: IO () -main = travisTestReporter defaultConfig [] qcProps +main = defaultMain qcProps -- cgit v1.2.3