packages feed

ttc 0.1.0.1 → 0.2.0.0

raw patch · 14 files changed

+205/−732 lines, 14 filesdep −timedep ~basedep ~tastydep ~template-haskellPVP ok

version bump matches the API change (PVP)

Dependencies removed: time

Dependency ranges changed: base, tasty, template-haskell, text

API changes (from Hackage documentation)

+ Data.TTC: mkUntypedValid :: String -> Name -> DecsQ
+ Data.TTC: mkUntypedValidQQ :: String -> Name -> DecsQ
+ Data.TTC: untypedValidOf :: Parse a => Proxy a -> String -> ExpQ

Files

CHANGELOG.md view
@@ -24,6 +24,14 @@  [KaC]: <https://keepachangelog.com/en/1.0.0/> +## 0.2.0.0 (2019-12-15)++### Non-Breaking++* Add untyped validation functions+* Move examples to a separate package+* Refactor examples and add more+ ## 0.1.0.1 (2019-12-02)  ### Non-Breaking
README.md view
@@ -101,7 +101,7 @@ ```haskell instance TTC.Parse Username where   parse = TTC.asT $ \t-> do-    unless (T.all isAsciiLower t) $ Left "username has invalid characters"+    unless (T.all isAsciiLower t) $ Left "username has invalid character(s)"     let len = T.length t     when (len < 3) $ Left "username has fewer than 3 characters"     when (len > 12) $ Left "username has more than 12 characters"
− examples/CreditCard.hs
@@ -1,230 +0,0 @@---------------------------------------------------------------------------------- |--- Module      : CreditCard--- Description : credit card data types--- Copyright   : Copyright (c) 2019 Travis Cardwell--- License     : MIT------ This module defines the data types for a credit card, with 'TTC.Render' and--- 'TTC.Parse' instances, as an example of TTC usage.------ The data types in this module all have 'THS.Lift' instances, to demonstrate--- how to use 'TTC.valid' in `examples/valid.hs`.  All of the 'THS.Lift'--- instances are derived, using the `DeriveLift` extension.------ The constructors and accessors of composite data types 'CreditCard' and--- 'ExpirationDate' are exported, but the constructors for 'Name', 'Number',--- 'Year', 'Month', and 'SecurityCode' are not.  The 'TTC.Parse' instances--- serve as "smart constructors," ensuring that all values are valid.---------------------------------------------------------------------------------{-# LANGUAGE DeriveLift #-}--module CreditCard-  ( -- * CreditCard-    CreditCard(..)-    -- ** Name-  , Name-    -- ** Number-  , Number-    -- ** ExpirationDate-  , ExpirationDate(..)-  , toDay-  , Year-  , Month-    -- ** SecurityCode-  , SecurityCode-  ) where---- http://hackage.haskell.org/package/base-import Control.Monad (unless, when)-import Data.Char (digitToInt, isDigit, isSpace, toUpper)-import Data.List (dropWhileEnd, intersperse)-import Text.Read (readMaybe)---- https://hackage.haskell.org/package/template-haskell-import qualified Language.Haskell.TH.Syntax as THS---- http://hackage.haskell.org/package/time-import qualified Data.Time.Calendar as Calendar---- (ttc)-import qualified Data.TTC as TTC------------------------------------------------------------------------------------ | A credit card has a name, number, expiration date, and security code.-data CreditCard-  = CreditCard-    { name           :: !Name-    , number         :: !Number-    , expirationDate :: !ExpirationDate-    , securityCode   :: !SecurityCode-    }-  deriving Show------------------------------------------------------------------------------------ | After any leading and trailing whitespace is stripped and all lowercase--- characters are converted to uppercase, a name must meet the following--- constraints:------ * Only characters between `0x20` (space) and `0x5F` (underscore) are---   allowed.--- * The name must be between 1 and 26 characters in length.------ Reference:------ * https://stackoverflow.com/questions/2004532-newtype Name = Name String-  deriving (Eq, Ord, Show, THS.Lift)--instance TTC.Parse Name where-  parse = TTC.asS $ \s -> do-    let name' = map toUpper $ strip s-        invChars = filter ((||) <$> (< ' ') <*> (> '_')) name'-    unless (null invChars) . Left $-      "name has invalid character(s): " <> intersperse ',' invChars-    when (null name') $ Left "name is empty"-    when (length name' > 26) $ Left "name has more than 26 characters"-    pure $ Name name'--instance TTC.Render Name where-  render (Name name') = TTC.convert name'------------------------------------------------------------------------------------ | After any space and dash characters are removed, a number must meet the--- following constraints:------ * Only ASCII digits are allowed.--- * The number must be between 8 and 19 characters in length.--- * The number must have a valid checksum.------ Reference:------ * https://en.wikipedia.org/wiki/Payment_card_number--- * https://en.wikipedia.org/wiki/Luhn_algorithm--- * http://rosettacode.org/wiki/Luhn_test_of_credit_card_numbers#Haskell-newtype Number = Number String-  deriving (Eq, Ord, Show, THS.Lift)--instance TTC.Parse Number where-  parse = TTC.asS $ \s -> do-    let number' = filter ((&&) <$> (/= ' ') <*> (/= '-')) s-        invChars = filter (not . isDigit) number'-        len = length number'-    unless (null invChars) . Left $-      "number has invalid character(s): " <> intersperse ',' invChars-    unless (len >= 8) $ Left "number has fewer than 8 characters"-    unless (len <= 19) $ Left "number has more than 19 characters"-    unless (luhn number') $ Left "number checksum is invalid"-    pure $ Number number'--instance TTC.Render Number where-  render (Number number') = TTC.convert number'--luhn :: String -> Bool-luhn-    = (== 0)-    . (`mod` 10)-    . sum-    . map (uncurry (+) . (`divMod` 10))-    . zipWith (*) (cycle [1, 2])-    . map digitToInt-    . reverse------------------------------------------------------------------------------------ | When parsed from a single string, an expiration date must be in `YYYY-MM`--- format.-data ExpirationDate-  = ExpirationDate-    { year  :: !Year-    , month :: !Month-    }-  deriving (Eq, Ord, Show, THS.Lift)--instance TTC.Parse ExpirationDate where-  parse = TTC.asS $ \s -> case break (== '-') (strip s) of-    (year', '-':month') ->-      ExpirationDate <$> TTC.parse year' <*> TTC.parse month'-    _ -> Left "expiration date not in YYYY-MM format"--instance TTC.Render ExpirationDate where-  render (ExpirationDate year' month') =-    TTC.fromS $ TTC.render year' <> "-" <> TTC.render month'--toDay-  :: ExpirationDate-  -> Calendar.Day-toDay (ExpirationDate (Year year') (Month month')) =-    let yearZ = fromIntegral year'-        day = Calendar.gregorianMonthLength yearZ month'-    in Calendar.fromGregorian yearZ month' day------------------------------------------------------------------------------------ | A year must be in `YYYY` format, between 1900 and 9999.-newtype Year = Year Int-  deriving (Eq, Ord, Show, THS.Lift)--instance TTC.Parse Year where-  parse = TTC.asS $ \s -> do-    year' <- maybe (Left "year is not in YYYY format") pure $ readMaybe s-    unless (year' >= 1900) $ Left "year is before 1900"-    unless (year' <= 9999) $ Left "year is after 9999"-    pure $ Year year'--instance TTC.Render Year where-  render (Year year') = TTC.convert $ show year'------------------------------------------------------------------------------------ | A month must be in `MM` format, between 1 (January) and 12 (December).-newtype Month = Month Int-  deriving (Eq, Ord, Show, THS.Lift)--instance TTC.Parse Month where-  parse = TTC.asS $ \s -> do-    month' <- maybe (Left "month is not in MM format") pure $ readMaybe s-    unless (month' >= 1 && month' <= 12) $ Left "month is not in 1-12 range"-    pure $ Month month'--instance TTC.Render Month where-  render (Month month')-    | month' < 10 = TTC.convert $ '0' : show month'-    | otherwise   = TTC.convert $ show month'------------------------------------------------------------------------------------ | After any leading and trailing whitespace is stripped, a security code--- must meet the following constraints:------ * Only ASCII digits are allowed.--- * The number must be 3 or 4 characters in length.------ Reference:------ * https://en.wikipedia.org/wiki/Card_security_code-newtype SecurityCode = SecurityCode String-  deriving (Eq, Ord, Show, THS.Lift)--instance TTC.Parse SecurityCode where-  parse = TTC.asS $ \s -> do-    let securityCode' = strip s-        invChars = filter (not . isDigit) securityCode'-        len = length securityCode'-    unless (null invChars) . Left $-      "security code has invalid character(s): " <> intersperse ',' invChars-    unless (len >= 3) $ Left "security code has fewer than 3 characters"-    unless (len <= 4) $ Left "security code has more than 4 characters"-    pure $ SecurityCode securityCode'--instance TTC.Render SecurityCode where-  render (SecurityCode securityCode') = TTC.convert securityCode'----------------------------------------------------------------------------------strip :: String -> String-strip = dropWhile isSpace . dropWhileEnd isSpace
− examples/Duration.hs
@@ -1,127 +0,0 @@---------------------------------------------------------------------------------- |--- Module      : Duration--- Description : duration data type--- Copyright   : Copyright (c) 2019 Travis Cardwell--- License     : MIT------ This module defines a duration data type, with 'TTC.Render' and 'TTC.Parse'--- instances, as an example of TTC usage.------ The constructor for 'Duration' is not exported.  The 'TTC.Parse' instance--- serves as a "smart constructor," ensuring that all values are valid.---------------------------------------------------------------------------------{-# LANGUAGE TemplateHaskell #-}--module Duration-  ( -- * Type-    Duration-    -- * API-  , fromDouble-  , fromFloat-  , fromInt-  , fromMicro-  , fromNominalDiffTime-  , fromReal-  , toDouble-  , toFloat-  , toInt-  , toIntegral-  , toMicro-  , toNominalDiffTime-  , valid-  ) where---- https://hackage.haskell.org/package/base-import Data.List (dropWhileEnd)-import Text.Read (readMaybe)---- https://hackage.haskell.org/package/time-import Data.Time.Clock (NominalDiffTime)---- ttc-import qualified Data.TTC as TTC----------------------------------------------------------------------------------- $Type---- | A 'Duration' is a measure of seconds.  It is fractional and must be zero--- or greater.------ The underlying type is 'NominalDiffTime', which may be negative.  The--- constructor is not exported, and all functions that create a value ensure--- that the constraint holds.-newtype Duration = Duration NominalDiffTime-  deriving Show--instance TTC.Parse Duration where-  parse = TTC.asS $ \s ->-    maybe (Left $ "invalid Duration: " <> s) fromDouble $ readMaybe s--instance TTC.Render Duration where-  render (Duration t) = TTC.fromS . dropWhileEnd (== 's') $ show t----------------------------------------------------------------------------------- $API---- | Construct a 'Duration' from a 'Double' (seconds).-fromDouble :: Double -> Either String Duration-fromDouble = fromReal---- | Construct a 'Duration' from a 'Float' (seconds).-fromFloat :: Float -> Either String Duration-fromFloat = fromReal---- | Construct a 'Duration' from an 'Int' (seconds).-fromInt :: Int -> Either String Duration-fromInt = fromReal---- | Construct a 'Duration' from an 'Int' (microseconds).-fromMicro :: Int -> Either String Duration-fromMicro us = fromDouble $ fromIntegral us / 1000000.0---- | Construct a 'Duration' from a 'NominalDiffTime'.-fromNominalDiffTime :: NominalDiffTime -> Either String Duration-fromNominalDiffTime t-    | t >= 0.0 = Right $ Duration t-    | otherwise = Left $ "Duration less than zero: " ++ show t---- | Construct a 'Duration' from a 'Real' (seconds).-fromReal :: (Real a, Show a) => a -> Either String Duration-fromReal x-    | x >= 0 = Right . Duration $ realToFrac x-    | otherwise = Left $ "Duration less than zero: " ++ show x---- | Convert a 'Duration' to a 'Double' (seconds).-toDouble :: Duration -> Double-toDouble = toFractional---- | Convert a 'Duration' to a 'Float' (seconds).-toFloat :: Duration -> Float-toFloat = toFractional---- | Convert a 'Duration' to a 'Fractional' (seconds).-toFractional :: Fractional a => Duration -> a-toFractional (Duration t) = realToFrac t---- | Convert a 'Duration' to an 'Int' (seconds).-toInt :: Duration -> Int-toInt = toIntegral---- | Convert a 'Duration' to an 'Integral' (seconds).-toIntegral :: Integral a => Duration -> a-toIntegral = round . toFloat---- | Convert a 'Duration' to an 'Int' (microseconds).-toMicro :: Duration -> Int-toMicro (Duration t) = round (realToFrac t * 1000000.0 :: Double)---- | Convert a 'Duration' to a 'NominalDiffTime'.-toNominalDiffTime :: Duration -> NominalDiffTime-toNominalDiffTime (Duration t) = t---- | The 'valid' function is created using 'TTC.mkValid' via Template Haskell.--- Note that 'Duration' does not have a 'Language.Haskell.TH.Syntax.Lift'--- instance.-$(TTC.mkValid "valid" ''Duration)
− examples/Username.hs
@@ -1,43 +0,0 @@---------------------------------------------------------------------------------- |--- Module      : Username--- Description : username data type--- Copyright   : Copyright (c) 2019 Travis Cardwell--- License     : MIT------ This module defines a username data type, with 'TTC.Render' and 'TTC.Parse'--- instances, as an example of TTC usage.------ The constructor for 'Username' is not exported.  The 'TTC.Parse' instance--- serves as a "smart constructor," ensuring that all values are valid.---------------------------------------------------------------------------------module Username (Username) where---- https://hackage.haskell.org/package/base-import Control.Monad (unless, when)-import Data.Char (isAsciiLower)---- https://hackage.haskell.org/package/text-import qualified Data.Text as T-import Data.Text (Text)---- ttc-import qualified Data.TTC as TTC------------------------------------------------------------------------------------ | A 'Username' must consist of 3 to 12 lowercase ASCII letters.-newtype Username = Username Text-  deriving (Eq, Ord, Show)--instance TTC.Render Username where-  render (Username t) = TTC.convert t--instance TTC.Parse Username where-  parse = TTC.asT $ \t -> do-    unless (T.all isAsciiLower t) $ Left "username has invalid characters"-    let len = T.length t-    when (len < 3) $ Left "username has fewer than 3 characters"-    when (len > 12) $ Left "username has more than 12 characters"-    pure $ Username t
− examples/invalid.hs
@@ -1,37 +0,0 @@---------------------------------------------------------------------------------- |--- Module      : Main--- Description : example of compile-time validation failure--- Copyright   : Copyright (c) 2019 Travis Cardwell--- License     : MIT------ 'TTC.valid' is used to create validated constants.  The credit card number--- has an invalid checksum, triggering a compile-time error.---------------------------------------------------------------------------------{-# LANGUAGE TemplateHaskell #-}--module Main (main) where---- (ttc)-import qualified Data.TTC as TTC---- (ttc:example-invalid)-import CreditCard (CreditCard(CreditCard))---- HLint does not support typed expression splices-{-# ANN module "HLint: ignore" #-}----------------------------------------------------------------------------------sample :: CreditCard-sample = CreditCard-    $$(TTC.valid "John Q Doe")-    $$(TTC.valid "1234 5678 9012")-    $$(TTC.valid "2020-06")-    $$(TTC.valid "123")----------------------------------------------------------------------------------main :: IO ()-main = print sample
− examples/mkvalid.hs
@@ -1,33 +0,0 @@---------------------------------------------------------------------------------- |--- Module      : Main--- Description : example of compile-time validation--- Copyright   : Copyright (c) 2019 Travis Cardwell--- License     : MIT------ 'Data.TTC.mkValid' is used to create the 'Duration.valid' function, without--- the need for a 'Language.Haskell.TH.Syntax.Lift' instance.  In this--- example, the sample duration is validated at compile-time and parsed again--- at run-time.---------------------------------------------------------------------------------{-# LANGUAGE TemplateHaskell #-}--module Main (main) where---- (ttc:example-mkvalid)-import qualified Duration as Duration-import Duration (Duration)---- HLint does not support typed expression splices-{-# ANN module "HLint: ignore" #-}----------------------------------------------------------------------------------sample :: Duration-sample = $$(Duration.valid "123")----------------------------------------------------------------------------------main :: IO ()-main = print sample
− examples/prompt.hs
@@ -1,87 +0,0 @@---------------------------------------------------------------------------------- |--- Module      : Main--- Description : example CLI prompt using TTC--- Copyright   : Copyright (c) 2019 Travis Cardwell--- License     : MIT------ 'TTC.Parse' and 'TTC.Render' instances are used to create a CLI prompt,--- demonstrating TTC usage.---------------------------------------------------------------------------------module Main (main) where---- https://hackage.haskell.org/package/base-import qualified System.IO as IO---- https://hackage.haskell.org/package/time-import qualified Data.Time.Calendar as Calendar-import qualified Data.Time.Clock as Clock---- (ttc)-import qualified Data.TTC as TTC---- (ttc:example-prompt)-import qualified CreditCard as CC-import CreditCard (CreditCard(CreditCard), ExpirationDate(ExpirationDate))------------------------------------------------------------------------------------ | This function prompts for a value of the desired type.  When input is--- invalid, it displays the error and tries again.-prompt-  :: TTC.Parse a-  => String  -- ^ prompt string-  -> IO a-prompt promptString = loop-  where-    loop = do-      putStr promptString-      IO.hFlush IO.stdout-      s <- getLine-      case TTC.parse s of-        Right x -> return x-        Left err -> do-          putStrLn $ "  " <> err-          loop------------------------------------------------------------------------------------ | This function prompts for credit card details.-promptCC :: IO CreditCard-promptCC = CreditCard-    <$> prompt "Enter the name: "-    <*> prompt "Enter the number: "-    <*>-      ( ExpirationDate-          <$> prompt "Enter the expiration year (YYYY): "-          <*> prompt "Enter the expiration month (MM): "-      )-    <*> prompt "Enter the security code: "------------------------------------------------------------------------------------ | The program prompts for credit card details, prints out the normalized--- values, and shows information about expiration.-main :: IO ()-main = do-    putStrLn "Please enter some fake credit card details."-    cc <- promptCC-    putStrLn $ replicate 78 '-'-    putStrLn $ "Name:            " <> TTC.render (CC.name cc)-    putStrLn $ "Number:          " <> TTC.render (CC.number cc)-    putStrLn $ "Expiration date: " <> TTC.render (CC.expirationDate cc)-    putStrLn $ "Security code:   " <> TTC.render (CC.securityCode cc)-    putStrLn $ replicate 78 '-'-    today <- Clock.utctDay <$> Clock.getCurrentTime-    putStrLn . ("This credit card " <>) . (<> "!") $-      case CC.toDay (CC.expirationDate cc) of-        expiry-          | expiry > today -> "expires in " <> diffDays expiry today-          | expiry < today -> "expired " <> diffDays today expiry <> " ago"-          | otherwise      -> "expires today"-  where-    diffDays :: Calendar.Day -> Calendar.Day -> String-    diffDays day1 day2 = case Calendar.diffDays day1 day2 of-      1 -> "1 day"-      n -> show n <> " days"
− examples/uname.hs
@@ -1,22 +0,0 @@---------------------------------------------------------------------------------- |--- Module      : Main--- Description : minimal example of using Render and Parse instances--- Copyright   : Copyright (c) 2019 Travis Cardwell--- License     : MIT---------------------------------------------------------------------------------module Main (main) where---- (ttc)-import qualified Data.TTC as TTC---- (ttc:example-uname)-import Username (Username)----------------------------------------------------------------------------------main :: IO ()-main = case TTC.parse "tcard" :: Either String Username of-    Right uname -> putStrLn $ "valid username: " ++ TTC.render uname-    Left err -> putStrLn $ "invalid username: " ++ err
− examples/valid.hs
@@ -1,37 +0,0 @@---------------------------------------------------------------------------------- |--- Module      : Main--- Description : example of compile-time validation--- Copyright   : Copyright (c) 2019 Travis Cardwell--- License     : MIT------ 'TTC.valid' is used to create validated constants.  The sample credit card--- is validated at compile-time.---------------------------------------------------------------------------------{-# LANGUAGE TemplateHaskell #-}--module Main (main) where---- (ttc)-import qualified Data.TTC as TTC---- (ttc:example-valid)-import CreditCard (CreditCard(CreditCard))---- HLint does not support typed expression splices-{-# ANN module "HLint: ignore" #-}----------------------------------------------------------------------------------sample :: CreditCard-sample = CreditCard-    $$(TTC.valid "John Q Doe")-    $$(TTC.valid "1234 5678 9015")-    $$(TTC.valid "2020-06")-    $$(TTC.valid "123")----------------------------------------------------------------------------------main :: IO ()-main = print sample
src/Data/TTC.hs view
@@ -95,9 +95,13 @@   , parseWithRead'   , readsEnum   , readsWithParse+    -- ** Constant Validation   , valid   , validOf   , mkValid+  , untypedValidOf+  , mkUntypedValid+  , mkUntypedValidQQ   ) where  -- https://hackage.haskell.org/package/base@@ -112,6 +116,7 @@  -- https://hackage.haskell.org/package/template-haskell import qualified Language.Haskell.TH as TH+import qualified Language.Haskell.TH.Quote as Q import qualified Language.Haskell.TH.Syntax as THS  -- https://hackage.haskell.org/package/text@@ -352,6 +357,8 @@ -- There are no default instances for the 'Render' type class, so that all -- instances can be customized per project when desired.  Instances for some -- basic data types are available in "Data.TTC.Instances".+--+-- See the @uname@ and @prompt@ example programs in the @examples@ directory. class Render a where   render :: Textual t => a -> t @@ -401,6 +408,8 @@ -- There are no default instances for the 'Parse' type class, so that all -- instances can be customized per project when desired.  Instances for some -- basic data types are available in "Data.TTC.Instances".+--+-- See the @uname@ and @prompt@ example programs in the @examples@ directory. class Parse a where   parse :: Textual t => t -> Either String a @@ -513,6 +522,8 @@ -- $ParseUtils  -- | Parse a value in an enumeration+--+-- See the @enum@ example program in the @examples@ directory. parseEnum   :: (Bounded a, Enum a, Render a, Textual t)   => Bool        -- ^ case-insensitive when 'True'@@ -598,11 +609,22 @@     Left{}  -> [] {-# INLINEABLE readsWithParse #-} +-- $ParseValid+ -- | Validate a constant at compile-time using a 'Parse' instance ----- This function parses the 'String' at compile-time in order to validate it.--- When valid, the result is compiled in, so the result type must have a--- 'THS.Lift' instance.  Use 'validOf' when this is inconvenient.+-- This function parses the 'String' at compile-time and fails compilation on+-- error.  When valid, the result is compiled in, so the result type must have+-- a 'THS.Lift' instance.  When this is inconvenient, use one of the+-- alternative functions in this library.+--+-- This function uses a typed expression.  Typed expressions were not+-- supported in @haskell-src-exts <1.22.0@, which caused problems with+-- @hlint@.  If the issue effects you, use @hlint -i "Parse error"@ to ignore+-- parse errors or use one of the alternative functions in this library.+--+-- See the @valid@, @invalid@, and @lift@ example programs in the @examples@+-- directory. valid   :: (Parse a, THS.Lift a)   => String@@ -613,19 +635,40 @@  -- | Validate a constant at compile-time using a 'Parse' instance ----- This function parses the 'String' at compile-time in order to validate it.--- When valid, the 'String' is compiled in, to be parsed again at run-time.--- Since the result is not compiled in, no 'THS.Lift' instance is required.+-- This function requires a 'Proxy' of the result type.  Use 'mkValid' to+-- avoid having to pass a 'Proxy' during constant definition.+--+-- This function parses the 'String' at compile-time and fails compilation on+-- error.  When valid, the 'String' is compiled in, to be parsed again at+-- run-time.  Since the result is not compiled in, no 'THS.Lift' instance is+-- required.+--+-- This function uses a typed expression.  Typed expressions were not+-- supported in @haskell-src-exts <1.22.0@, which caused problems with+-- @hlint@.  If the issue effects you, use @hlint -i "Parse error"@ to ignore+-- parse errors or use 'untypedValidOf' instead.+--+-- See the @validof@ example program in the @examples@ directory. validOf   :: Parse a   => Proxy a   -> String   -> TH.Q (TH.TExp a) validOf proxy s = case (`asProxyTypeOf` proxy) <$> parse s of-    Right{} -> [|| parseUnsafe s ||]+    Right{} -> [|| parseUnsafeS s ||]     Left err -> fail $ "Invalid constant: " ++ err  -- | Make a @valid@ function using 'validOf' for the given type+--+-- Create a @valid@ function in the module for a type in order to avoid having+-- to write a 'Proxy' when defining constants.+--+-- This function uses a typed expression.  Typed expressions were not+-- supported in @haskell-src-exts <1.22.0@, which caused problems with+-- @hlint@.  If the issue effects you, use @hlint -i "Parse error"@ to ignore+-- parse errors or use 'mkUntypedValidOf' instead.+--+-- See the @mkvalid@ example program in the @examples@ directory. mkValid   :: String   -> TH.Name@@ -638,4 +681,69 @@     return       [ TH.SigD funName' funType       , TH.FunD funName' [TH.Clause [] (TH.NormalB body) []]+      ]++-- | Validate a constant at compile-time using a 'Parse' instance+--+-- This function requires a 'Proxy' of the result type.  Use 'mkUntypedValid'+-- to avoid having to pass a 'Proxy' during constant definition.+--+-- This function parses the 'String' at compile-time and fails compilation on+-- error.  When valid, the 'String' is compiled in, to be parsed again at+-- run-time.  Since the result is not compiled in, no 'THS.Lift' instance is+-- required.+--+-- See the @uvalidof@ example program in the @examples@ directory.+untypedValidOf+  :: Parse a+  => Proxy a+  -> String+  -> TH.ExpQ+untypedValidOf proxy s = case (`asProxyTypeOf` proxy) <$> parse s of+    Right{} -> [| parseUnsafeS s |]+    Left err -> fail $ "Invalid constant: " ++ err++-- | Make a @valid@ function using 'untypedValidOf' for the given type+--+-- Create a @valid@ function in the module for a type in order to avoid having+-- to write a 'Proxy' when defining constants.+--+-- See the @mkuvalid@ example program in the @examples@ directory.+mkUntypedValid+  :: String+  -> TH.Name+  -> TH.DecsQ+mkUntypedValid funName typeName = do+    let funName' = TH.mkName funName+        resultType = pure $ TH.ConT typeName+    funType <- [t| String -> TH.ExpQ |]+    body <- [| untypedValidOf (Proxy :: Proxy $resultType) |]+    return+      [ TH.SigD funName' funType+      , TH.FunD funName' [TH.Clause [] (TH.NormalB body) []]+      ]++-- | Make a @valid@ quasi-quoter using 'untypedValidOf' for the given type+--+-- See the @uvalidqq@ example program in the @examples@ directory.+mkUntypedValidQQ+  :: String+  -> TH.Name+  -> TH.DecsQ+mkUntypedValidQQ funName typeName = do+    let funName' = TH.mkName funName+        resultType = pure $ TH.ConT typeName+    expE <- [| untypedValidOf (Proxy :: Proxy $resultType) |]+    expP <- [| error "pattern not supported" |]+    expT <- [| error "type not supported" |]+    expD <- [| error "declaration not supported" |]+    let body = TH.NormalB $ TH.RecConE 'Q.QuasiQuoter+          [ ('Q.quoteExp, expE)+          , ('Q.quotePat, expP)+          , ('Q.quoteType, expT)+          , ('Q.quoteDec, expD)+          ]+    return+      [ TH.SigD funName' $ TH.ConT ''Q.QuasiQuoter+      , TH.FunD funName' [TH.Clause [] body []]       ]
test/Data/TTC/Test.hs view
@@ -1,4 +1,6 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}  module Data.TTC.Test (tests) where @@ -27,6 +29,13 @@ -- (ttc) import qualified Data.TTC as TTC +-- (ttc:test)+import qualified TestString+import TestString (TestString(TestString))++-- HLint does not support typed expression splices+{-# ANN module ("HLint: ignore" :: String) #-}+ ------------------------------------------------------------------------------ -- $HelperFunctions @@ -558,7 +567,37 @@     ]  ------------------------------------------------------------------------------+-- $Valid +testValid :: TestTree+testValid = testCase "valid" $ TestString "test" @=? validConst+  where+    validConst :: TestString+    validConst = $$(TTC.valid "test")++testValidOf :: TestTree+testValidOf = testCase "validOf" $+    TestString "test" @=? $$(TTC.validOf (Proxy :: Proxy TestString) "test")++testMkValid :: TestTree+testMkValid = testCase "mkValid" $+    TestString "test" @=? $$(TestString.valid "test")++testUntypedValidOf :: TestTree+testUntypedValidOf = testCase "untypedValidOf" $+    TestString "test" @=?+      $(TTC.untypedValidOf (Proxy :: Proxy TestString) "test")++testMkUntypedValid :: TestTree+testMkUntypedValid = testCase "mkUntypedValid" $+    TestString "test" @=? $(TestString.untypedValid "test")++testMkUntypedValidQQ :: TestTree+testMkUntypedValidQQ = testCase "mkUntypedValidQQ" $+    TestString "test" @=? [TestString.untypedValidQQ|test|]++------------------------------------------------------------------------------+ tests :: TestTree tests = testGroup "Data.TTC"     [ testGroup "Textual"@@ -619,5 +658,13 @@         , testParseEnum'         , testReadsWithParse         , testReadsEnum+        ]+    , testGroup "Valid"+        [ testValid+        , testValidOf+        , testMkValid+        , testUntypedValidOf+        , testMkUntypedValid+        , testMkUntypedValidQQ         ]     ]
+ test/TestString.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE DeriveLift #-}+{-# LANGUAGE TemplateHaskell #-}++module TestString where++-- https://hackage.haskell.org/package/template-haskell+import qualified Language.Haskell.TH.Syntax as THS++-- (ttc)+import qualified Data.TTC as TTC++------------------------------------------------------------------------------+-- $Type++newtype TestString = TestString String+  deriving (Eq, Ord, Show, THS.Lift)++instance TTC.Render TestString where+  render (TestString s) = TTC.convert s++instance TTC.Parse TestString where+  parse = TTC.asS $ Right . TestString++------------------------------------------------------------------------------+-- $API++$(TTC.mkValid "valid" ''TestString)++$(TTC.mkUntypedValid "untypedValid" ''TestString)++$(TTC.mkUntypedValidQQ "untypedValidQQ" ''TestString)
ttc.cabal view
@@ -1,5 +1,5 @@ name:           ttc-version:        0.1.0.1+version:        0.2.0.0 category:       Data, Text synopsis:       Textual Type Classes description:@@ -31,30 +31,6 @@   type: git   location: https://github.com/ExtremaIS/ttc-haskell.git -flag example-invalid-  description: build example-invalid-  default: False--flag example-mkvalid-  description: build example-mkvalid-  default: False--flag example-prompt-  description: build example-prompt-  default: False--flag example-uname-  description: build example-uname-  default: False--flag example-valid-  description: build example-valid-  default: False--flag examples-  description: build all buildable examples-  default: False- library   hs-source-dirs: src   exposed-modules:@@ -68,89 +44,6 @@   default-language: Haskell2010   ghc-options: -Wall -executable example-invalid-  if flag(example-invalid)-    buildable: True-  else-    buildable: False-  hs-source-dirs: examples-  main-is: invalid.hs-  other-modules:-      CreditCard-  build-depends:-      base-    , template-haskell-    , time >=1.8 && <1.10-    , ttc-  default-language: Haskell2010-  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N--executable example-mkvalid-  if flag(examples) || flag(example-mkvalid)-    buildable: True-  else-    buildable: False-  hs-source-dirs: examples-  main-is: mkvalid.hs-  other-modules:-      Duration-  build-depends:-      base-    , time >=1.8 && <1.10-    , ttc-  default-language: Haskell2010-  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N--executable example-prompt-  if flag(examples) || flag(example-prompt)-    buildable: True-  else-    buildable: False-  hs-source-dirs: examples-  main-is: prompt.hs-  other-modules:-      CreditCard-  build-depends:-      base-    , template-haskell-    , time >=1.8 && <1.10-    , ttc-  default-language: Haskell2010-  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N--executable example-uname-  if flag(examples) || flag(example-uname)-    buildable: True-  else-    buildable: False-  hs-source-dirs: examples-  main-is: uname.hs-  other-modules:-      Username-  build-depends:-      base-    , text-    , ttc-  default-language: Haskell2010-  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N--executable example-valid-  if flag(examples) || flag(example-valid)-    buildable: True-  else-    buildable: False-  hs-source-dirs: examples-  main-is: valid.hs-  other-modules:-      CreditCard-  build-depends:-      base-    , template-haskell-    , time >=1.8 && <1.10-    , ttc-  default-language: Haskell2010-  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N- test-suite ttc-test   type: exitcode-stdio-1.0   hs-source-dirs: test@@ -158,11 +51,13 @@   other-modules:       Data.TTC.Test     , Data.TTC.Instances.Test+    , TestString   build-depends:       base     , bytestring     , tasty >=1.0 && <1.3     , tasty-hunit >=0.10 && <0.11+    , template-haskell     , text     , ttc   default-language: Haskell2010