yamlet-aeson-1.0.0.0: tests/Yamlet/Aeson/Test/Decode.hs
module Yamlet.Aeson.Test.Decode
( decodeTests
) where
import Control.Applicative
import Data.Aeson qualified as A
import Data.Aeson.Types qualified as A
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as M
import Data.Text qualified as T
import Test.Tasty
import Test.Tasty.HUnit
import Yamlet
import Yamlet.Aeson
import Yamlet.Aeson.Test.Helpers
decodeTests :: TestTree
decodeTests =
testGroup
"decode"
[ testCase "scalar keys" test_scalarKeys
, testCase "keys with the same text" test_sameText
, testCase "collection keys" test_collectionKeys
, testCase "special floats" test_specialFloats
, testCase "tags" test_tags
, testCase "aliases" test_aliases
, testCase "merge key" test_mergeKey
, testCase "error location" test_errorLocation
, testCase "error of a key of a map" test_keyOfMap
, testCase "error with a message that ignores the value" test_constantMessage
, testCase "error at a missing key" test_missingKey
, testCase "error path beyond the node" test_pathBeyondNode
, testCase "field of a derived record" test_derivedField
, testCase "types of aeson" test_aesonTypes
]
test_scalarKeys :: Assertion
test_scalarKeys =
assertEqual
"the text of each key"
( Right $
A.object
["0x10" A..= 'a', "true" A..= 'b', "~" A..= 'c', "" A..= 'd', "1.0" A..= 'e']
)
(decodeText @A.Value "0x10: a\ntrue: b\n~: c\n'': d\n'1.0': e\n")
test_sameText :: Assertion
test_sameText = do
assertEqual
"a number and a string"
[(2, 1, "duplicate key \"1\" after conversion"), (1, 1, "the first key 1")]
(errorsOf (decodeText @A.Value "1: a\n\"1\": b\n"))
assertEqual
"a string with a tag"
[(2, 4, "duplicate key \"a\" after conversion"), (1, 1, "the first key \"a\"")]
(errorsOf (decodeText @A.Value "a: 1\n!x a: 2\n"))
test_collectionKeys :: Assertion
test_collectionKeys =
assertEqual
"an error at each key"
[ (1, 3, "expected a scalar key, but got a list")
, (3, 3, "expected a scalar key, but got a mapping")
]
(errorsOf (decodeText @A.Value "? [1]\n: a\n? {b: c}\n: d\n"))
test_specialFloats :: Assertion
test_specialFloats = do
assertEqual
"the values"
(Right (A.toJSON [A.String "+inf", A.String "-inf", A.Null, A.Number 0]))
(decodeText @A.Value "[.inf, -.inf, .nan, -0.0]")
assertEqual
"the doubles"
(Right ["Infinity", "-Infinity", "NaN", "0.0"])
(map show . (.value) <$> decodeText @(ViaAeson [Double]) "[.inf, -.inf, .nan, -0.0]")
test_tags :: Assertion
test_tags =
assertEqual
"the values without tags"
( Right $
A.object
["x" A..= ("abc" :: T.Text), "y" A..= ("1" :: T.Text), "z" A..= (2 :: Int)]
)
(decodeText @A.Value "!point {x: !secret abc, y: !!str 1, z: !!int 2}")
test_aliases :: Assertion
test_aliases =
assertEqual
"the alias gives the value of its anchor"
(Right (A.object ["a" A..= [1 :: Int], "b" A..= [1 :: Int]]))
(decodeText @A.Value "a: &x [1]\nb: *x\n")
test_mergeKey :: Assertion
test_mergeKey =
assertEqual
"an ordinary key"
(Right (A.object ["<<" A..= A.object ["a" A..= (1 :: Int)], "b" A..= (2 :: Int)]))
(decodeText @A.Value "<<: {a: 1}\nb: 2\n")
test_errorLocation :: Assertion
test_errorLocation = do
let result =
decodeText @(ViaAeson [Server]) "- port: 80\n host: a\n- port: http\n host: b\n"
assertEqual
"the error at the value"
[(3, 9, "parsing Int failed, expected Number, but encountered String")]
(errorsOf result)
assertEqual
"the path of the value"
["[1].port"]
(pathsOf result)
where
pathsOf :: Either (NE.NonEmpty Error) a -> [String]
pathsOf = \case
Left errs -> [renderPath err.path | err <- NE.toList errs]
Right _ -> []
test_keyOfMap :: Assertion
test_keyOfMap = do
assertEqual
"an error of the key at the value"
[(2, 6, "parsing Int failed, Unexpected 'a' while parsing number literal")]
(errorsOf (decodeText @(ViaAeson (M.Map Int T.Text)) "1: a\nabc: x\n"))
assertEqual
"an error of the value at the value"
[(2, 4, "parsing Int failed, expected Number, but encountered String")]
(errorsOf (decodeText @(ViaAeson (M.Map Int Int)) "1: 2\n3: x\n"))
test_constantMessage :: Assertion
test_constantMessage =
assertEqual
"the error at the value"
[(1, 7, "invalid port")]
(errorsOf (decodeText @(ViaAeson Endpoint) "port: http\n"))
test_missingKey :: Assertion
test_missingKey =
assertEqual
"the error at the mapping"
[ (3, 3, "parsing Yamlet.Aeson.Test.Helpers.Server(Server) failed, key \"host\" not found")
]
(errorsOf (decodeText @(ViaAeson [Server]) "- port: 80\n host: a\n- port: 81\n"))
test_pathBeyondNode :: Assertion
test_pathBeyondNode =
assertEqual
"the error at the deepest node with the rest of the path"
[(1, 3, "parsing Int failed, expected Number, but encountered String at outer[0].inner")]
(errorsOf (decodeText @(ViaAeson [Nested]) "- a\n"))
test_derivedField :: Assertion
test_derivedField = do
assertEqual
"a missing key is null for aeson"
(Right (Service "a" (ViaAeson Nothing)))
(decodeText "name: a\n")
assertEqual
"the field"
(Right (Service "a" (ViaAeson (Just (Server 80 "b")))))
(decodeText "name: a\nbackend: {port: 80, host: b}\n")
assertEqual
"the output"
"name: a\nbackend:\n port: 80\n host: b\n"
(encodeText (Service "a" (ViaAeson (Just (Server 80 "b")))))
test_aesonTypes :: Assertion
test_aesonTypes = do
assertEqual
"a float for an Int"
(Right (ViaAeson @Int 1))
(decodeText "1.0")
assertEqual
"yes of YAML 1.2"
(Right (ViaAeson @T.Text "yes"))
(decodeText "yes")
-- | A decoder with a path that goes beyond a scalar.
newtype Nested = Nested Int
deriving stock (Show)
instance A.FromJSON Nested where
parseJSON v =
Nested <$> A.parseJSON v A.<?> A.Key "inner" A.<?> A.Index 0 A.<?> A.Key "outer"
newtype Port = Port Int
deriving stock (Show)
-- | The same message for every invalid value.
instance A.FromJSON Port where
parseJSON v = Port <$> A.parseJSON v <|> fail "invalid port"
newtype Endpoint = Endpoint {port :: Port}
deriving stock (Show, Generic)
deriving anyclass (A.FromJSON)
data Service = Service {name :: T.Text, backend :: ViaAeson (Maybe Server)}
deriving stock (Eq, Show, Generic)
deriving anyclass (GenericYamlOptions)
deriving (FromYaml, ToYaml) via GenericYaml Service