packages feed

format-heavy-0.1.0.2: tests/Spec.hs

{-# 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: <hi      >"
      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|<undefined>}." (Single (2.718281828 :: Float)) `shouldBe` "Value: 2.718."
      format "Value: {:.3|<undefined>}." (Single (Nothing :: Maybe Float))
        `shouldBe` "Value: <undefined>."
      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"