packages feed

toml-reader-0.3.0.0: test/specs/TOML/DecodeSpec.hs

{-# LANGUAGE NegativeLiterals #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module TOML.DecodeSpec (spec) where

import Data.Int (Int8)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Time qualified as Time
import Numeric.Natural (Natural)
import Skeletest
import Skeletest.Predicate qualified as P

import TOML.Decode (
  DecodeTOML,
  Decoder,
  decodeWith,
  getArrayOf,
  getField,
  getFieldOpt,
  getFieldOptWith,
  getFieldOr,
  getFieldWith,
  getFields,
  getFieldsOpt,
  getFieldsOptWith,
  getFieldsWith,
  tomlDecoder,
 )
import TOML.Error (
  ContextItem (..),
  DecodeError (..),
  TOMLError (..),
 )
import TOML.Value (Value (..))

-- | A test decoder
succDecoder :: (Enum a, DecodeTOML a) => Decoder a
succDecoder = succ <$> tomlDecoder

spec :: Spec
spec = do
  describe "getField" $ do
    it "decodes field" $
      decodeWith (getField @Int "a") "a = 1" `shouldBe` Right 1

    it "errors if field does not exist" $
      decodeWith (getField @Int "a") "" `shouldBe` Left (DecodeError [Key "a"] MissingField)

    it "errors if field is the wrong type" $
      decodeWith (getField @Int "a") "a = true" `shouldBe` Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))

  describe "getFieldOr" $ do
    it "decodes field" $
      decodeWith (getFieldOr @Int 42 "a") "a = 1" `shouldBe` Right 1

    it "returns default if field does not exist" $
      decodeWith (getFieldOr @Int 42 "a") "" `shouldBe` Right 42

    it "errors if field is the wrong type" $
      decodeWith (getFieldOr @Int 42 "a") "a = true" `shouldBe` Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))

  describe "getFieldOpt" $ do
    it "decodes field" $
      decodeWith (getFieldOpt @Int "a") "a = 1" `shouldBe` Right (Just 1)

    it "returns Nothing if field does not exist" $
      decodeWith (getFieldOpt @Int "a") "" `shouldBe` Right Nothing

    it "errors if field is the wrong type" $
      decodeWith (getFieldOpt @Int "a") "a = true" `shouldBe` Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))

  describe "getFieldWith" $ do
    it "decodes field" $
      decodeWith (getFieldWith @Int succDecoder "a") "a = 1" `shouldBe` Right 2

    it "errors if field does not exist" $
      decodeWith (getFieldWith @Int succDecoder "a") "" `shouldBe` Left (DecodeError [Key "a"] MissingField)

    it "errors if field is the wrong type" $
      decodeWith (getFieldWith @Int succDecoder "a") "a = true" `shouldBe` Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))

  describe "getFieldOptWith" $ do
    it "decodes field" $
      decodeWith (getFieldOptWith @Int succDecoder "a") "a = 1" `shouldBe` Right (Just 2)

    it "returns Nothing if field does not exist" $
      decodeWith (getFieldOptWith @Int succDecoder "a") "" `shouldBe` Right Nothing

    it "errors if field is the wrong type" $
      decodeWith (getFieldOptWith @Int succDecoder "a") "a = true" `shouldBe` Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))

  describe "getFields" $ do
    it "decodes field" $
      decodeWith (getFields @Int ["a", "b"]) "a = { b = 1 }" `shouldBe` Right 1

    it "errors if field does not exist" $
      decodeWith (getFields @Int ["a", "b"]) "a = {}" `shouldBe` Left (DecodeError [Key "a", Key "b"] MissingField)

    it "errors if intermediate field does not exist" $
      decodeWith (getFields @Int ["a", "b", "c"]) "a = {}" `shouldBe` Left (DecodeError [Key "a", Key "b"] MissingField)

    it "errors if field is the wrong type" $
      decodeWith (getFields @Int ["a", "b"]) "a = { b = true }" `shouldBe` Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))

    it "errors if intermediate field is the wrong type" $
      decodeWith (getFields @Int ["a", "b", "c"]) "a = { b = true }" `shouldBe` Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))

  describe "getFieldsOpt" $ do
    it "decodes field" $
      decodeWith (getFieldsOpt @Int ["a", "b"]) "a = { b = 1 }" `shouldBe` Right (Just 1)

    it "returns Nothing if field does not exist" $
      decodeWith (getFieldsOpt @Int ["a", "b"]) "a = {}" `shouldBe` Right Nothing

    it "returns Nothing if intermediate field does not exist" $
      decodeWith (getFieldsOpt @Int ["a", "b", "c"]) "a = {}" `shouldBe` Right Nothing

    it "errors if field is the wrong type" $
      decodeWith (getFieldsOpt @Int ["a", "b"]) "a = { b = true }" `shouldBe` Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))

    it "errors if intermediate field is the wrong type" $
      decodeWith (getFieldsOpt @Int ["a", "b", "c"]) "a = { b = true }" `shouldBe` Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))

  describe "getFieldsWith" $ do
    it "decodes field" $
      decodeWith (getFieldsWith @Int succDecoder ["a", "b"]) "a = { b = 1 }" `shouldBe` Right 2

    it "errors if field does not exist" $
      decodeWith (getFieldsWith @Int succDecoder ["a", "b"]) "a = {}" `shouldBe` Left (DecodeError [Key "a", Key "b"] MissingField)

    it "errors if intermediate field does not exist" $
      decodeWith (getFieldsWith @Int succDecoder ["a", "b", "c"]) "a = {}" `shouldBe` Left (DecodeError [Key "a", Key "b"] MissingField)

    it "errors if field is the wrong type" $
      decodeWith (getFieldsWith @Int succDecoder ["a", "b"]) "a = { b = true }" `shouldBe` Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))

    it "errors if intermediate field is the wrong type" $
      decodeWith (getFieldsWith @Int succDecoder ["a", "b", "c"]) "a = { b = true }" `shouldBe` Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))

  describe "getFieldsOptWith" $ do
    it "decodes field" $
      decodeWith (getFieldsOptWith @Int succDecoder ["a", "b"]) "a = { b = 1 }" `shouldBe` Right (Just 2)

    it "returns Nothing if field does not exist" $
      decodeWith (getFieldsOptWith @Int succDecoder ["a", "b"]) "a = {}" `shouldBe` Right Nothing

    it "returns Nothing if intermediate field does not exist" $
      decodeWith (getFieldsOptWith @Int succDecoder ["a", "b", "c"]) "a = {}" `shouldBe` Right Nothing

    it "errors if field is the wrong type" $
      decodeWith (getFieldsOptWith @Int succDecoder ["a", "b"]) "a = { b = true }" `shouldBe` Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))

    it "errors if intermediate field is the wrong type" $
      decodeWith (getFieldsOptWith @Int succDecoder ["a", "b", "c"]) "a = { b = true }" `shouldBe` Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))

  describe "getArrayOf" $ do
    it "decodes field" $
      decodeWith (getFieldWith (getArrayOf @Int succDecoder) "a") "a = [1, 2]" `shouldBe` Right [2, 3]

    it "errors if value is the wrong type" $
      decodeWith (getFieldWith (getArrayOf @Int succDecoder) "a") "a = true" `shouldBe` Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))

    it "errors if value in array is the wrong type" $
      decodeWith (getFieldWith (getArrayOf @Int succDecoder) "a") "a = [1, 2, true]" `shouldBe` Left (DecodeError [Key "a", Index 2] (TypeMismatch (Boolean True)))

  describe "decoding multiple fields at once" $ do
    it "works" $ do
      let decoder :: Decoder (Int, Maybe String, Maybe Int, Bool)
          decoder =
            (,,,)
              <$> getField "a"
              <*> getFieldOpt "b"
              <*> getFieldOpt "missing"
              <*> getFields ["c", "d"]
          doc =
            Text.unlines
              [ "a = 1"
              , "b = \"hello\""
              , "[c]"
              , "d = true"
              ]
      decodeWith decoder doc `shouldBe` Right (1, Just "hello", Nothing, True)

  describe "DecodeTOML instances" $ do
    describe "Bool" $ do
      it "decodes successfully" $ do
        decodeWith (getField @Bool "a") "a = true" `shouldBe` Right True
        decodeWith (getField @Bool "a") "a = false" `shouldBe` Right False

    describe "String" $ do
      it "decodes successfully" $ do
        decodeWith (getField @String "a") "a = 'z'" `shouldBe` Right "z"
        decodeWith (getField @String "a") "a = 'asdf'" `shouldBe` Right "asdf"

    describe "Char" $ do
      it "decodes successfully" $
        decodeWith (getField @Char "a") "a = 'z'" `shouldBe` Right 'z'

      it "errors with multi-character string" $
        decodeWith (getField @Char "a") "a = 'asdf'" `shouldFailWith` "Expected single character string"

    describe "Int" $ do
      it "decodes successfully" $ do
        decodeWith (getField @Int "a") "a = 42" `shouldBe` Right 42
        decodeWith (getField @Int "a") "a = -13" `shouldBe` Right (-13)

    describe "Natural" $ do
      it "decodes successfully" $
        decodeWith (getField @Natural "a") "a = 42" `shouldBe` Right 42

      it "errors with negative numbers" $
        decodeWith (getField @Natural "a") "a = -13" `shouldFailWith` "Got negative number"

    describe "Int8" $ do
      it "decodes successfully" $ do
        decodeWith (getField @Int8 "a") "a = 127" `shouldBe` Right 127
        decodeWith (getField @Int8 "a") "a = 42" `shouldBe` Right 42
        decodeWith (getField @Int8 "a") "a = -13" `shouldBe` Right (-13)
        decodeWith (getField @Int8 "a") "a = -128" `shouldBe` Right (-128)

      it "errors with underflow/overflow" $ do
        decodeWith (getField @Int8 "a") "a = 128" `shouldFailWith` "Overflow"
        decodeWith (getField @Int8 "a") "a = -129" `shouldFailWith` "Underflow"

    describe "Double" $ do
      it "decodes successfully" $ do
        decodeWith (getField @Double "a") "a = 42.0" `shouldBe` Right 42.0
        decodeWith (getField @Double "a") "a = -13.2" `shouldBe` Right (-13.2)

    describe "Array" $
      it "decodes successfully" $ do
        decodeWith (getField @[Int] "a") "a = [1, 2]" `shouldBe` Right [1, 2]

    describe "UTCTime" $
      it "decodes successfully" $ do
        decodeWith (getField @Time.UTCTime "a") "a = 2020-01-31T00:00:01-01:00"
          `shouldBe` Right (Time.UTCTime (Time.fromGregorian 2020 1 31) 3601)

    describe "ZonedTime" $
      it "decodes successfully" $ do
        -- ZonedTime does not have an Eq instance
        decodeWith (getField @Time.ZonedTime "a") "a = 2020-01-31T00:00:01-01:00"
          `shouldSatisfy` P.right
            ( P.con $
                Time.ZonedTime
                  (P.eq $ Time.LocalTime (Time.fromGregorian 2020 1 31) (Time.TimeOfDay 0 0 1))
                  (P.con $ Time.TimeZone (P.eq -60) P.anything P.anything)
            )

    describe "LocalTime" $
      it "decodes successfully" $ do
        decodeWith (getField @Time.LocalTime "a") "a = 2020-01-31T00:00:01"
          `shouldBe` Right (Time.LocalTime (Time.fromGregorian 2020 1 31) (Time.TimeOfDay 0 0 1))

    describe "Day" $
      it "decodes successfully" $ do
        decodeWith (getField @Time.Day "a") "a = 2020-01-31"
          `shouldBe` Right (Time.fromGregorian 2020 1 31)

    describe "TimeOfDay" $
      it "decodes successfully" $ do
        decodeWith (getField @Time.TimeOfDay "a") "a = 07:32:59.1"
          `shouldBe` Right (Time.TimeOfDay 7 32 59.1)

    describe "Maybe" $
      it "decodes successfully" $ do
        decodeWith (getField @(Maybe Int) "a") "a = 1" `shouldBe` Right (Just 1)

    describe "Either" $ do
      it "decodes successfully" $ do
        decodeWith (getField @(Either Int Double) "a") "a = 1" `shouldBe` Right (Left 1)
        decodeWith (getField @(Either Int Double) "a") "a = 1.0" `shouldBe` Right (Right 1)

    describe "()" $
      it "decodes successfully" $ do
        decodeWith (getField @() "a") "a = []" `shouldBe` Right ()

    describe "(a, b)" $ do
      it "decodes successfully" $ do
        decodeWith (getField @(Int, Double) "a") "a = [1, 2.5]" `shouldBe` Right (1, 2.5)

      it "includes index in errors" $
        decodeWith (getField @(Int, Double) "a") "a = [1, true]"
          `shouldSatisfy` P.left (P.con $ DecodeError (P.eq [Key "a", Index 1]) P.anything)

    describe "Map" $ do
      it "decodes successfully" $
        decodeWith (getField @(Map Text Int) "a") "a = {x = 1, y = 2}"
          `shouldBe` Right (Map.fromList [("x", 1), ("y", 2)])

      it "includes key in errors" $
        decodeWith (getField @(Map Text Int) "a") "a = {x = true}"
          `shouldSatisfy` P.left (P.con $ DecodeError (P.eq [Key "a", Key "x"]) P.anything)

shouldFailWith :: Either TOMLError a -> Text -> IO ()
result `shouldFailWith` message = result `shouldSatisfy` P.left decodeError
  where
    decodeError = P.con $ DecodeError P.anything invalidValue
    invalidValue = P.con $ InvalidValue (P.eq message) P.anything