packages feed

toml-reader-0.2.0.0: test/tasty/TOML/DecodeTest.hs

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

module TOML.DecodeTest (test) where

import Data.Int (Int8)
import qualified Data.Text as Text
import qualified Data.Time as Time
import Numeric.Natural (Natural)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit

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 (..))

test :: TestTree
test =
  testGroup "TOML.Decode" . concat $
    [ getFieldTests
    , [decoderInstanceTests]
    ]

getFieldTests :: [TestTree]
getFieldTests =
  [ testGroup
      "getField"
      [ testCase "decodes field" $
          decodeWith (getField @Int "a") "a = 1" @?= Right 1
      , testCase "errors if field does not exist" $
          decodeWith (getField @Int "a") "" @?= Left (DecodeError [Key "a"] MissingField)
      , testCase "errors if field is the wrong type" $
          decodeWith (getField @Int "a") "a = true" @?= Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getFieldOr"
      [ testCase "decodes field" $
          decodeWith (getFieldOr @Int 42 "a") "a = 1" @?= Right 1
      , testCase "returns default if field does not exist" $
          decodeWith (getFieldOr @Int 42 "a") "" @?= Right 42
      , testCase "errors if field is the wrong type" $
          decodeWith (getFieldOr @Int 42 "a") "a = true" @?= Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getFieldOpt"
      [ testCase "decodes field" $
          decodeWith (getFieldOpt @Int "a") "a = 1" @?= Right (Just 1)
      , testCase "returns Nothing if field does not exist" $
          decodeWith (getFieldOpt @Int "a") "" @?= Right Nothing
      , testCase "errors if field is the wrong type" $
          decodeWith (getFieldOpt @Int "a") "a = true" @?= Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getFieldWith"
      [ testCase "decodes field" $
          decodeWith (getFieldWith @Int succDecoder "a") "a = 1" @?= Right 2
      , testCase "errors if field does not exist" $
          decodeWith (getFieldWith @Int succDecoder "a") "" @?= Left (DecodeError [Key "a"] MissingField)
      , testCase "errors if field is the wrong type" $
          decodeWith (getFieldWith @Int succDecoder "a") "a = true" @?= Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getFieldOptWith"
      [ testCase "decodes field" $
          decodeWith (getFieldOptWith @Int succDecoder "a") "a = 1" @?= Right (Just 2)
      , testCase "returns Nothing if field does not exist" $
          decodeWith (getFieldOptWith @Int succDecoder "a") "" @?= Right Nothing
      , testCase "errors if field is the wrong type" $
          decodeWith (getFieldOptWith @Int succDecoder "a") "a = true" @?= Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getFields"
      [ testCase "decodes field" $
          decodeWith (getFields @Int ["a", "b"]) "a = { b = 1 }" @?= Right 1
      , testCase "errors if field does not exist" $
          decodeWith (getFields @Int ["a", "b"]) "a = {}" @?= Left (DecodeError [Key "a", Key "b"] MissingField)
      , testCase "errors if intermediate field does not exist" $
          decodeWith (getFields @Int ["a", "b", "c"]) "a = {}" @?= Left (DecodeError [Key "a", Key "b"] MissingField)
      , testCase "errors if field is the wrong type" $
          decodeWith (getFields @Int ["a", "b"]) "a = { b = true }" @?= Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))
      , testCase "errors if intermediate field is the wrong type" $
          decodeWith (getFields @Int ["a", "b", "c"]) "a = { b = true }" @?= Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getFieldsOpt"
      [ testCase "decodes field" $
          decodeWith (getFieldsOpt @Int ["a", "b"]) "a = { b = 1 }" @?= Right (Just 1)
      , testCase "returns Nothing if field does not exist" $
          decodeWith (getFieldsOpt @Int ["a", "b"]) "a = {}" @?= Right Nothing
      , testCase "returns Nothing if intermediate field does not exist" $
          decodeWith (getFieldsOpt @Int ["a", "b", "c"]) "a = {}" @?= Right Nothing
      , testCase "errors if field is the wrong type" $
          decodeWith (getFieldsOpt @Int ["a", "b"]) "a = { b = true }" @?= Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))
      , testCase "errors if intermediate field is the wrong type" $
          decodeWith (getFieldsOpt @Int ["a", "b", "c"]) "a = { b = true }" @?= Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getFieldsWith"
      [ testCase "decodes field" $
          decodeWith (getFieldsWith @Int succDecoder ["a", "b"]) "a = { b = 1 }" @?= Right 2
      , testCase "errors if field does not exist" $
          decodeWith (getFieldsWith @Int succDecoder ["a", "b"]) "a = {}" @?= Left (DecodeError [Key "a", Key "b"] MissingField)
      , testCase "errors if intermediate field does not exist" $
          decodeWith (getFieldsWith @Int succDecoder ["a", "b", "c"]) "a = {}" @?= Left (DecodeError [Key "a", Key "b"] MissingField)
      , testCase "errors if field is the wrong type" $
          decodeWith (getFieldsWith @Int succDecoder ["a", "b"]) "a = { b = true }" @?= Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))
      , testCase "errors if intermediate field is the wrong type" $
          decodeWith (getFieldsWith @Int succDecoder ["a", "b", "c"]) "a = { b = true }" @?= Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getFieldsOptWith"
      [ testCase "decodes field" $
          decodeWith (getFieldsOptWith @Int succDecoder ["a", "b"]) "a = { b = 1 }" @?= Right (Just 2)
      , testCase "returns Nothing if field does not exist" $
          decodeWith (getFieldsOptWith @Int succDecoder ["a", "b"]) "a = {}" @?= Right Nothing
      , testCase "returns Nothing if intermediate field does not exist" $
          decodeWith (getFieldsOptWith @Int succDecoder ["a", "b", "c"]) "a = {}" @?= Right Nothing
      , testCase "errors if field is the wrong type" $
          decodeWith (getFieldsOptWith @Int succDecoder ["a", "b"]) "a = { b = true }" @?= Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))
      , testCase "errors if intermediate field is the wrong type" $
          decodeWith (getFieldsOptWith @Int succDecoder ["a", "b", "c"]) "a = { b = true }" @?= Left (DecodeError [Key "a", Key "b"] (TypeMismatch (Boolean True)))
      ]
  , testGroup
      "getArrayOf"
      [ testCase "decodes field" $
          decodeWith (getFieldWith (getArrayOf @Int succDecoder) "a") "a = [1, 2]" @?= Right [2, 3]
      , testCase "errors if value is the wrong type" $
          decodeWith (getFieldWith (getArrayOf @Int succDecoder) "a") "a = true" @?= Left (DecodeError [Key "a"] (TypeMismatch (Boolean True)))
      , testCase "errors if value in array is the wrong type" $
          decodeWith (getFieldWith (getArrayOf @Int succDecoder) "a") "a = [1, 2, true]" @?= Left (DecodeError [Key "a", Index 2] (TypeMismatch (Boolean True)))
      ]
  , testCase "Decoding multiple fields at once" $ 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 @?= Right (1, Just "hello", Nothing, True)
  ]
  where
    succDecoder :: (Enum a, DecodeTOML a) => Decoder a
    succDecoder = succ <$> tomlDecoder

decoderInstanceTests :: TestTree
decoderInstanceTests =
  testGroup
    "DecodeTOML instances"
    [ testCase "Bool" $ do
        decodeWith (getField @Bool "a") "a = true" @?= Right True
        decodeWith (getField @Bool "a") "a = false" @?= Right False
    , testCase "String" $ do
        decodeWith (getField @String "a") "a = 'z'" @?= Right "z"
        decodeWith (getField @String "a") "a = 'asdf'" @?= Right "asdf"
    , testCase "Char" $
        decodeWith (getField @Char "a") "a = 'z'" @?= Right 'z'
    , testCase "Char errors with multi-character string" $
        assertInvalidValue "Expected single character string" $
          decodeWith (getField @Char "a") "a = 'asdf'"
    , testCase "Int" $ do
        decodeWith (getField @Int "a") "a = 42" @?= Right 42
        decodeWith (getField @Int "a") "a = -13" @?= Right (-13)
    , testCase "Natural" $
        decodeWith (getField @Natural "a") "a = 42" @?= Right 42
    , testCase "Natural errors with negative numbers" $
        assertInvalidValue "Got negative number" $
          decodeWith (getField @Natural "a") "a = -13"
    , testCase "Int8" $ do
        decodeWith (getField @Int8 "a") "a = 127" @?= Right 127
        decodeWith (getField @Int8 "a") "a = 42" @?= Right 42
        decodeWith (getField @Int8 "a") "a = -13" @?= Right (-13)
        decodeWith (getField @Int8 "a") "a = -128" @?= Right (-128)
    , testCase "Int8 errors with underflow/overflow" $ do
        assertInvalidValue "Overflow" $
          decodeWith (getField @Int8 "a") "a = 128"
        assertInvalidValue "Underflow" $
          decodeWith (getField @Int8 "a") "a = -129"
    , testCase "Double" $ do
        decodeWith (getField @Double "a") "a = 42.0" @?= Right 42.0
        decodeWith (getField @Double "a") "a = -13.2" @?= Right (-13.2)
    , testCase "Array" $
        decodeWith (getField @[Int] "a") "a = [1, 2]" @?= Right [1, 2]
    , testCase "UTCTime" $
        decodeWith (getField @Time.UTCTime "a") "a = 2020-01-31T00:00:01-01:00"
          @?= Right (Time.UTCTime (Time.fromGregorian 2020 1 31) 3601)
    , testCase "ZonedTime" $
        -- ZonedTime does not have an Eq instance
        case decodeWith (getField @Time.ZonedTime "a") "a = 2020-01-31T00:00:01-01:00" of
          Right (Time.ZonedTime (Time.LocalTime day time) (Time.TimeZone (-60) _ _))
            | day == Time.fromGregorian 2020 1 31
            , time == Time.TimeOfDay 0 0 1 ->
                return ()
          result -> unexpectedResult result
    , testCase "LocalTime" $
        decodeWith (getField @Time.LocalTime "a") "a = 2020-01-31T00:00:01"
          @?= Right (Time.LocalTime (Time.fromGregorian 2020 1 31) (Time.TimeOfDay 0 0 1))
    , testCase "Day" $
        decodeWith (getField @Time.Day "a") "a = 2020-01-31"
          @?= Right (Time.fromGregorian 2020 1 31)
    , testCase "TimeOfDay" $
        decodeWith (getField @Time.TimeOfDay "a") "a = 07:32:59.1"
          @?= Right (Time.TimeOfDay 7 32 59.1)
    , testCase "Maybe" $
        decodeWith (getField @(Maybe Int) "a") "a = 1" @?= Right (Just 1)
    , testCase "Either" $ do
        decodeWith (getField @(Either Int Double) "a") "a = 1" @?= Right (Left 1)
        decodeWith (getField @(Either Int Double) "a") "a = 1.0" @?= Right (Right 1)
    , testCase "()" $
        decodeWith (getField @() "a") "a = []" @?= Right ()
    , testCase "(a, b)" $
        decodeWith (getField @(Int, Double) "a") "a = [1, 2.5]" @?= Right (1, 2.5)
    , testCase "Tuples show errors with index" $
        case decodeWith (getField @(Int, Double) "a") "a = [1, true]" of
          Left (DecodeError [Key "a", Index 1] _) -> return ()
          result -> unexpectedResult result
    ]
  where
    assertInvalidValue expected result =
      case result of
        Left (DecodeError _ (InvalidValue actual _)) -> actual @?= expected
        _ -> unexpectedResult result

    unexpectedResult result = assertFailure $ "Got unexpected result: " <> show result