{-# LANGUAGE OverloadedStrings #-} import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import Data.Map (Map) import qualified Data.Map as Map import qualified Data.Text as T import qualified Data.Text.Lazy as TL import Data.Time import Test.Hspec import Data.Text.Format.Heavy import Data.Text.Format.Heavy.Build (formatEither) import Data.Text.Format.Heavy.Parse (FormatParseItem (..), parse) import Data.Text.Format.Heavy.Parse.Shell import Data.Text.Format.Heavy.Time main :: IO () main = hspec $ do describe "trivial" $ do it "formats string literal without formatting characters" $ do format "hello world" () `shouldBe` "hello world" describe "parse" $ do it "returns replacement fields" $ do parse "{name}" `shouldBe` Right [FormatReplacementField "name" Nothing] parse "{name:.2}" `shouldBe` Right [FormatReplacementField "name" (Just ".2")] it "keeps strings and fields in order" $ do parse "{{name}} {name}" `shouldBe` Right [FormatString "{name} ", FormatReplacementField "name" Nothing] it "keeps nested format specs raw" $ do parse "{value:{width}}" `shouldBe` Right [FormatReplacementField "value" (Just "{width}")] describe "simple" $ do it "formats int properly" $ do format "integer: {}" (Single (7 :: Int)) `shouldBe` "integer: 7" it "formats strings properly" $ do format "string: {}" (Single ("hello" :: String)) `shouldBe` "string: hello" it "formats text values properly" $ do format "strict: {}" (Single (T.pack "hello")) `shouldBe` "strict: hello" format "lazy: {}" (Single (TL.pack "hello")) `shouldBe` "lazy: hello" it "formats utf8 byte strings properly" $ do format "strict: {}" (Single (BS.pack [104, 101, 108, 108, 111])) `shouldBe` "strict: hello" format "lazy: {}" (Single (BSL.pack [104, 101, 108, 108, 111])) `shouldBe` "lazy: hello" it "handles parameter numbers" $ do format "one: {0}, two: {1}" ((1 :: Int), (2 :: Int)) `shouldBe` "one: 1, two: 2" format "two: {1}, one: {0}" ((1 :: Int), (2 :: Int)) `shouldBe` "two: 2, one: 1" describe "handles parameters names" $ do it "with ascii characters" $ do format "one: {theKey}!" ((Map.singleton "theKey" "the string") :: Map TL.Text TL.Text) `shouldBe` "one: the string!" it "with dots" $ do format "one: {the.key}!" ((Map.singleton "the.key" "the string") :: Map TL.Text TL.Text) `shouldBe` "one: the string!" it "with dashes" $ do format "one: {the-key}!" ((Map.singleton "the-key" "the string") :: Map TL.Text TL.Text) `shouldBe` "one: the string!" it "with underscores" $ do format "one: {the_key}!" ((Map.singleton "the_key" "the string") :: Map TL.Text TL.Text) `shouldBe` "one: the string!" it "handles additional variable containers" $ do format "{0}, {1}, {2}" (("one" :: String), (2 :: Int), ("three" :: String)) `shouldBe` "one, 2, three" format "{0}, {2}" (Several ["zero", "one", "two" :: String]) `shouldBe` "zero, two" format "hello {name}" ([("name", "world" :: TL.Text)] :: [(TL.Text, TL.Text)]) `shouldBe` "hello world" it "handles defaulting containers" $ do format "present: {0}; missing: {1}" (Single ("value" :: String) `withDefault` Variable ("fallback" :: TL.Text)) `shouldBe` "present: value; missing: fallback" format "present: {0}; missing: {1}" (optional (Single ("value" :: String))) `shouldBe` "present: value; missing: " it "handles escaped braces" $ do format "{{}}" () `shouldBe` "{}" format "\\{name\\}" () `shouldBe` "{name}" format "{{{name}}}" ([("name", "world" :: TL.Text)] :: [(TL.Text, TL.Text)]) `shouldBe` "{world}" format "json: {{\"answer\": \"{answer}\"}}" ([("answer", "ok" :: TL.Text)] :: [(TL.Text, TL.Text)]) `shouldBe` "json: {\"answer\": \"ok\"}" describe "documentation" $ do it "formats examples from wiki" $ do format "hex: {:#x}" (Single (427 :: Int)) `shouldBe` "hex: 0x1ab" format "hex: {:#h}" (Single (427 :: Int)) `shouldBe` "hex: 0x1ab" format "hex: {:#X}" (Single (427 :: Int)) `shouldBe` "hex: 0x1AB" format "hex: {:#H}" (Single (427 :: Int)) `shouldBe` "hex: 0x1AB" format "dec: {:#d}" (Single (17 :: Int)) `shouldBe` "dec: 17" format "center: <{0:^10}>" (Single ("hello" :: String)) `shouldBe` "center: < hello >" format "float: {:+6.4}" (Single (2.718281828 :: Double)) `shouldBe` "float: +2.7183" it "formats alignment and text conversion options" $ do format "left: <{:<8}>" (Single ("hi" :: String)) `shouldBe` "left: " format "right: <{:>8}>" (Single ("hi" :: String)) `shouldBe` "right: < hi>" format "center: <{:^7}>" (Single ("hi" :: String)) `shouldBe` "center: < hi >" format "fill: <{:*>8}>" (Single ("hi" :: String)) `shouldBe` "fill: <******hi>" format "upper: {:~u}" (Single ("hello" :: String)) `shouldBe` "upper: HELLO" format "lower: {:~l}" (Single ("HELLO" :: String)) `shouldBe` "lower: hello" format "title: {:~t}" (Single ("hello world" :: String)) `shouldBe` "title: Hello World" it "formats numeric options" $ do format "positive: {:+d}" (Single (7 :: Int)) `shouldBe` "positive: +7" format "negative: {:+d}" (Single (-7 :: Int)) `shouldBe` "negative: -7" format "space: {: d}" (Single (7 :: Int)) `shouldBe` "space: 7" format "hex: {:#X}" (Single (255 :: Integer)) `shouldBe` "hex: 0xFF" it "formats booleans" $ do format "default: {}" (Single True) `shouldBe` "default: true" format "enable: {:yes:no}" (Single False) `shouldBe` "enable: no" it "formats maybes" $ do format "Value: {:.3|}." (Single (2.718281828 :: Float)) `shouldBe` "Value: 2.718." format "Value: {:.3|}." (Single (Nothing :: Maybe Float)) `shouldBe` "Value: ." format "Value: {:.3}." (Single (Nothing :: Maybe Float)) `shouldBe` "Value: ." it "formats either and shown values" $ do format "left: {}" (Single (Left ("error" :: String) :: Either String Int)) `shouldBe` "left: error" format "right: {}" (Single (Right (7 :: Int) :: Either String Int)) `shouldBe` "right: 7" format "shown: {}" (Single (Shown (True, False))) `shouldBe` "shown: (True,False)" it "formats time" $ do let yektLocale = defaultTimeLocale { knownTimeZones = [TimeZone (5 * 60) False "YEKT"] } -- `defaultTimeLocale` does not know about Yekaterinburg. Just time = parseTimeM True yektLocale rfc822DateFormat "Sat, 3 Jun 2017 19:06:01 YEKT" :: Maybe ZonedTime format "time: {:%H:%M:%S}" (Single time) `shouldBe` "time: 19:06:01" format "time: {:%H:%M:%S %Z}" (Single time) `shouldBe` "time: 19:06:01 YEKT" format "default: {}" (Single time) `shouldBe` "default: Sat, 3 Jun 2017 19:06:01 YEKT" describe "errors" $ do it "reports missing parameters" $ do formatEither "missing: {1}" (Single ("value" :: String)) `shouldBe` Left "Parameter not found: 1" formatEither "missing: {2}" (Several ["zero", "one" :: String]) `shouldBe` Left "Parameter not found: 2" describe "shell syntax" $ do it "formats shell-style substitutions" $ do format (parseShellFormat' "hello $name") ([("name", "world" :: TL.Text)] :: [(TL.Text, TL.Text)]) `shouldBe` "hello world" format (parseShellFormat' "hello ${name}") ([("name", "world" :: TL.Text)] :: [(TL.Text, TL.Text)]) `shouldBe` "hello world" format (parseShellFormat' "${} ${}") (("one" :: String), ("two" :: String)) `shouldBe` "one two" format (parseShellFormat' "cost: $$${amount}") ([("amount", "10" :: TL.Text)] :: [(TL.Text, TL.Text)]) `shouldBe` "cost: $10"