unjson-0.15.1.0: test/Test.hs
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
module Main where
import qualified Data.Text as Text
import qualified Data.ByteString.Lazy.Char8 as BSL
import Data.Int
import Data.Typeable
import Data.Unjson
import qualified Data.Aeson as Aeson
import Data.Aeson ((.=))
import Test.HUnit
import Data.List
import Data.Data
import Data.Functor.Invariant
import qualified Data.HashMap.Strict as HashMap
import qualified Data.HashMap.Lazy as LazyHashMap
import qualified Data.Map as Map
import System.Exit (ExitCode (..), exitWith)
#if !MIN_VERSION_base(4,6,0)
import Prelude hiding (catch)
#endif
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
import Data.Monoid
#endif
default (Text.Text, String, Int, Double)
-- As an example we will use a hypothetical configuration data.
-- There are some mandatory fields and some optional fields.
data Konfig =
Konfig { konfigHostname :: Text.Text
, konfigPort :: Integer
, konfigCredentials :: Credentials
, konfigComment :: Maybe Text.Text
, konfigOptions :: [Text.Text]
, konfigAlternates :: Maybe (Text.Text,Credentials)
, konfigInt32 :: Int32
}
deriving (Eq,Ord,Show,Typeable)
data Credentials =
Credentials { credentialsUsername :: Text.Text
, credentialsPassword :: Text.Text
, credentialsDomain :: Maybe Text.Text
}
deriving (Eq,Ord,Show,Typeable)
unjsonKonfig :: UnjsonDef Konfig
unjsonKonfig = objectOf $ pure Konfig
<*> field "hostname"
konfigHostname
"The hostname this service is visible as"
<*> fieldDef "port" 80
konfigPort
"Port to listen on, defaults to 80"
<*> fieldBy "credentials"
konfigCredentials
"User admin credentials"
unjsonCredentials
<*> fieldOpt "comment"
konfigComment
"Optional comment, free text"
<*> fieldDef "options" []
konfigOptions
"Additional options, defaults to empty"
<*> fieldOpt "alternates"
konfigAlternates
"Alternate names for this server"
<*> field "int32"
konfigInt32
"A bounded integer fieldd."
unjsonCredentials :: UnjsonDef Credentials
unjsonCredentials = objectOf $ pure Credentials
<*> field "username"
credentialsUsername
"Name of the user"
<*> field "password"
credentialsPassword
"Password for the user"
<*> fieldOpt "domain"
credentialsDomain
"Domain for user credentials"
instance Unjson Credentials where
unjsonDef = unjsonCredentials
test_proper_parse :: Test
test_proper_parse = "Proper parsing of a complex structure" ~: do
let json = Aeson.object
[ "hostname" .= "www.example.com"
, "comment" .= "nice server"
, "credentials" .= Aeson.object
[ "username" .= "usr1"
, "password" .= "pass1"
]
, "int32" .= 42
]
let expect = Konfig
{ konfigHostname = "www.example.com"
, konfigPort = 80
, konfigComment = Just "nice server"
, konfigCredentials = Credentials "usr1" "pass1" Nothing
, konfigAlternates = Nothing
, konfigOptions = []
, konfigInt32 = 42
}
let Result val iss = parse unjsonKonfig json
assertEqual "There are no issues in parsing" [] iss
assertEqual "Value parsed is the one expected" expect val
return ()
test_missing_key :: Test
test_missing_key = "Key missing" ~: do
-- Note:
--
-- This test is strange with respect to what is returned as
-- exceptions. We would expect to have whole path to problematic
-- place returned, for some reason only last part of the path is
-- returned. This is good enough to keep this mechanism in place but
-- overall it is unknown why not everything is returned.
--
-- Underlying mechanism is supposed to use mapException inside of
-- mapException and that should stack them on top concatenating the
-- path. Tests with external setup prove this is really the case,
-- but for some reason does not happen in general scenario.
--
-- *Data.Unjson> resultPrependKey "a" (resultPrependKey "b" (fail "d" :: Result ()))
-- Result *** Exception: a.b: "d"
--
let json1 = Aeson.object
[ "hostname" .= "www.example.com"
, "port" .= 12345
, "comment" .= "nice server"
, "credentials" .= Aeson.object
[ "username" .= "usr1"
]
, "int32" .= 999
]
let json = Aeson.object
[ "payload" .= json1
]
let unjsonEnvelope :: UnjsonDef Konfig
unjsonEnvelope = objectOf $ pure id
<*> fieldBy "payload"
id
"Enveloped Konfig"
unjsonKonfig
do
let Result _val iss = parse unjsonEnvelope json
assertEqual "There is one issue in parsing" [Anchored (Path [ PathElemKey "payload"
, PathElemKey "credentials"
, PathElemKey "password"
]) "missing key"] iss
do
let Result _val iss = parse unjsonKonfig json1
assertEqual "There is one issue in parsing" [Anchored (Path [ PathElemKey "credentials"
, PathElemKey "password"
]) "missing key"] iss
test_wrong_value_type :: Test
test_wrong_value_type = "Value at key is wrong type" ~: do
let json = Aeson.object
[ "hostname" .= 12345
, "port" .= Aeson.object
[ "username" .= "usr1"
]
, "credentials" .= "www.example.com"
, "int32" .= 999
]
let Result _val iss = parse unjsonKonfig json
assertEqual "Number of issues in parsing" 3 (length iss)
assertEqual "Hostname must be string error info is present"
(Anchored (Path [ PathElemKey "hostname"
]) "expected Text, encountered Number") (iss!!0)
assertEqual "Port must be number error info is present"
(Anchored (Path [ PathElemKey "port"
]) "expected Integer, encountered Object") (iss!!1)
assertEqual "Credentials must be object error info is present"
(Anchored (Path [ PathElemKey "credentials"
]) "Error in $: expected HashMap ~Text v, encountered String") (iss!!2)
return ()
test_tuple_parsing :: Test
test_tuple_parsing = "Tuple parsing" ~: do
let json = Aeson.toJSON
[ ("hostname" :: Aeson.Value)
, ("port" :: Aeson.Value)
, (Aeson.toJSON 123)
]
let Result (val1 :: String, val2 :: Text.Text, val3 ::Integer) iss = parse unjsonDef json
assertEqual "Number of issues in parsing" [] iss
assertEqual "First element of tuple" "hostname" val1
assertEqual "Second element of tuple" "port" val2
assertEqual "Third element of tuple" 123 val3
let Result (_ :: (String, Text.Text, Int, Int)) iss' = parse unjsonDef json
assertEqual "Issue in parsing" [Anchored mempty "cannot parse array of length 3 into tuple of size 4"
,Anchored (Path [PathElemIndex 3]) "missing key"] iss'
let Result (_ :: (Integer, Integer, Text.Text)) iss'' = parse unjsonDef json
assertEqual "Issues in parsing"
[ Anchored (Path [PathElemIndex 0]) "expected Integer, encountered String"
, Anchored (Path [PathElemIndex 1]) "expected Integer, encountered String"
, Anchored (Path [PathElemIndex 2]) "expected Text, encountered Number"
] iss''
let Result (_ :: (String, Text.Text)) iss''' = parse unjsonDef json
assertEqual "Array too long for 2-tuple" [Anchored mempty "cannot parse array of length 3 into tuple of size 2"] iss'''
return ()
test_symmetry_of_serialization :: Test
test_symmetry_of_serialization = "Key missing" ~: do
let expect = Konfig
{ konfigHostname = "www.example.com"
, konfigPort = 12345
, konfigComment = Just "nice server"
, konfigCredentials = Credentials "usr1" "pass1" Nothing
, konfigAlternates = Nothing
, konfigOptions = []
, konfigInt32 = 42
}
let json = unjsonToJSON unjsonKonfig expect
let Result val _iss = parse unjsonKonfig json
assertEqual "Serialize-parse produces no problems" expect val
assertEqual "Serialize-parse is identity" expect val
return ()
test_pretty_serialization :: Test
test_pretty_serialization = "Pretty serialization" ~: do
let konfig = Konfig
{ konfigHostname = "www.example.com"
, konfigPort = 12345
, konfigComment = Just "nice server"
, konfigCredentials = Credentials "usr1" "pass1" Nothing
, konfigAlternates = Nothing
, konfigOptions = []
, konfigInt32 = 42
}
let jsonstr = BSL.unpack $ unjsonToByteStringLazy' (Options { nulls = False, indent = 4, pretty = True }) unjsonKonfig konfig
let expect = intercalate "\n"
[ "{"
, " \"hostname\": \"www.example.com\","
, " \"port\": 12345,"
, " \"credentials\": {"
, " \"username\": \"usr1\","
, " \"password\": \"pass1\""
, " },"
, " \"comment\": \"nice server\","
, " \"options\": [],"
, " \"int32\": 42"
, "}"
]
assertEqual "Serialize pretty prints proper indents" expect jsonstr
let jsonstr5 = BSL.unpack $ unjsonToByteStringLazy' (Options { nulls = False, indent = 5, pretty = True }) unjsonKonfig konfig
let expect5 = intercalate "\n"
[ "{"
, " \"hostname\": \"www.example.com\","
, " \"port\": 12345,"
, " \"credentials\": {"
, " \"username\": \"usr1\","
, " \"password\": \"pass1\""
, " },"
, " \"comment\": \"nice server\","
, " \"options\": [],"
, " \"int32\": 42"
, "}"
]
assertEqual "Serialize pretty prints proper indents" expect5 jsonstr5
let jsonstr3 = BSL.unpack $ unjsonToByteStringLazy' (Options { nulls = False, indent = 3, pretty = False }) unjsonKonfig konfig
let expect3 = concat
[ "{"
, "\"hostname\":\"www.example.com\","
, "\"port\":12345,"
, "\"credentials\":{"
, "\"username\":\"usr1\","
, "\"password\":\"pass1\""
, "},"
, "\"comment\":\"nice server\","
, "\"options\":[],"
, "\"int32\":42"
, "}"
]
assertEqual "Serialize pretty prints proper indents" expect3 jsonstr3
return ()
test_serialize_with_nulls :: Test
test_serialize_with_nulls = "Serialize with nulls" ~: do
let konfig = Konfig
{ konfigHostname = "www.example.com"
, konfigPort = 12345
, konfigComment = Just "nice server"
, konfigCredentials = Credentials "usr1" "pass1" Nothing
, konfigAlternates = Nothing
, konfigOptions = []
, konfigInt32 = 42
}
let jsonstr = BSL.unpack $ unjsonToByteStringLazy' (Options { nulls = True, indent = 4, pretty = True }) unjsonKonfig konfig
let expect = intercalate "\n"
[ "{"
, " \"hostname\": \"www.example.com\","
, " \"port\": 12345,"
, " \"credentials\": {"
, " \"username\": \"usr1\","
, " \"password\": \"pass1\","
, " \"domain\": null"
, " },"
, " \"comment\": \"nice server\","
, " \"options\": [],"
, " \"alternates\": null,"
, " \"int32\": 42"
, "}"
]
assertEqual "Serialize pretty prints proper indents" expect jsonstr
unjsonButThirteen :: UnjsonDef Int
unjsonButThirteen = objectOf $ pure id
<*> fieldBy "value" id "Integer but god forbid 13" (unjsonInvmapR whenParse id $ unjsonDef)
where
whenParse 13 = fail "13 is a bad luck number"
whenParse x = return x
test_semantic_errors_on_values :: Test
test_semantic_errors_on_values = "test_semantic_errors_on_values" ~: do
do
let json = Aeson.object
[ "value" .= (13 :: Int)
]
let Result _val iss = parse unjsonButThirteen json
assertEqual "Problem is reported" [Anchored (Path [PathElemKey "value"]) "13 is a bad luck number"] iss
-- assertEqual "Just numerical_value present" (13) val
unjsonEitherIntText :: UnjsonDef (Either Int Text.Text)
unjsonEitherIntText = disjointUnionOf "mode"
[ ("number", unjsonIsConstrByName "Left",
pure Left
<*> field "numerical_value"
fromLeft
"Numerical value")
, ("text", unjsonIsConstrByName "Right",
pure Right
<*> field "text_value"
fromRight
"Text value")]
where fromLeft ~(Left x) = x
fromRight ~(Right x) = x
test_parse_either_field :: Test
test_parse_either_field = "test_parse_either_field" ~: do
do
let json = Aeson.object
[ "mode" .= "number"
, "numerical_value" .= 12345
]
let Result val iss = parse unjsonEitherIntText json
assertEqual "No problems" [] iss
assertEqual "Just numerical_value present" (Left 12345) val
do
let json = Aeson.object
[ "mode" .= "text"
, "text_value" .= "asfsdfaf"
]
let Result val iss = parse unjsonEitherIntText json
assertEqual "No problems" [] iss
assertEqual "Just text_value present" (Right "asfsdfaf") val
do
let json = Aeson.object
[ "text_value" .= False
, "numerical_value" .= 12345
]
let Result _val iss = parse unjsonEitherIntText json
assertEqual "Problem when mode is missing" [Anchored (Path [PathElemKey "mode"]) "missing key"] iss
do
let json = Aeson.object
[ "mode" .= "something else"
]
let Result _val iss = parse unjsonEitherIntText json
assertEqual "Problem when mode is missing" [Anchored (Path [PathElemKey "mode"]) "value 'something else' is not one of the allowed for enumeration [number,text]"] iss
do
let json = Aeson.object
[ "mode" .= "number"
, "numerical_value" .= 123
]
let ex = Left 123
let js = unjsonToJSON unjsonEitherIntText ex
assertEqual "Serialized makes what expected" json js
do
let docstr = render unjsonEitherIntText
assertBool "Documentation generates" (length docstr > 0)
return ()
data AB = A | B
deriving (Show, Eq, Ord)
unjsonEnumAB :: UnjsonDef AB
unjsonEnumAB = enumOf "mode"
[ ("A", A)
, ("B", B)]
test_enum_field :: Test
test_enum_field = "test_enum_field" ~: do
do
let json = Aeson.object
[ "mode" .= "A"
]
let Result val iss = parse unjsonEnumAB json
assertEqual "No problems" [] iss
assertEqual "Proper value present" A val
do
let json = Aeson.object
[ "mode" .= "B"
]
let Result val iss = parse unjsonEnumAB json
assertEqual "No problems" [] iss
assertEqual "Proper value present" B val
do
let json = Aeson.object
[ "mode" .= "wrong"
]
let Result _val iss = parse unjsonEnumAB json
assertEqual "No problems" [Anchored (Path [PathElemKey "mode"]) "value 'wrong' is not one of the allowed for enumeration [A,B]"] iss
data AutoAB = AutoA | AutoB
deriving (Show, Eq, Ord, Enum, Bounded, Data, Typeable)
unjsonAutoEnumAB :: UnjsonDef AutoAB
unjsonAutoEnumAB = enumUnjsonDef
test_auto_enum_field :: Test
test_auto_enum_field = "test_auto_enum_field" ~: do
do
let json = Aeson.object
[ "AutoAB" .= "AutoA"
]
let Result val iss = parse unjsonAutoEnumAB json
assertEqual "No problems" [] iss
assertEqual "Proper value present" AutoA val
do
let json = Aeson.object
[ "AutoAB" .= "AutoB"
]
let Result val iss = parse unjsonAutoEnumAB json
assertEqual "No problems" [] iss
assertEqual "Proper value present" AutoB val
do
let json = Aeson.object
[ "AutoAB" .= "wrong"
]
let Result _val iss = parse unjsonAutoEnumAB json
assertEqual "No problems" [Anchored (Path [PathElemKey "AutoAB"]) "value 'wrong' is not one of the allowed for enumeration [AutoA,AutoB]"] iss
test_update_from_serialization :: Test
test_update_from_serialization = "test_update_from_serialization" ~: do
let initial = Konfig
{ konfigHostname = "old-www.server.com"
, konfigPort = 12345
, konfigComment = Just "nice server"
, konfigCredentials = Credentials "usr1" "pass1" Nothing
, konfigAlternates = Nothing
, konfigOptions = []
, konfigInt32 = 42
}
let expect = Konfig
{ konfigHostname = "www.example.com"
, konfigPort = 999
, konfigComment = Just "a better server"
, konfigCredentials = Credentials "usr2" "pass1" (Just "domain")
, konfigAlternates = Nothing
, konfigOptions = []
, konfigInt32 = 256
}
let json = Aeson.object
[ "hostname" .= "www.example.com" -- mandatory field
, "port" .= 999 -- optional with default
, "comment" .= "a better server" -- optional field
, "credentials" .= Aeson.object
[ "domain" .= "domain"
, "username" .= "usr2" ]
, "int32" .= 256
]
let Result val iss = update initial unjsonKonfig json
assertEqual "No problems" [] iss
assertEqual "Object updated with json" expect val
return ()
test_update_from_serialization_with_reset_to_default :: Test
test_update_from_serialization_with_reset_to_default = "test_update_from_serialization_with_reset_to_default" ~: do
let initial = Konfig
{ konfigHostname = "old-www.server.com"
, konfigPort = 12345
, konfigComment = Just "nice server"
, konfigCredentials = Credentials "usr1" "pass1" (Just "domain")
, konfigAlternates = Nothing
, konfigOptions = []
, konfigInt32 = 0
}
let _expect = Konfig
{ konfigHostname = "www.example.com"
, konfigPort = 80
, konfigComment = Nothing
, konfigCredentials = Credentials "usr1" "pass1" (Nothing)
, konfigAlternates = Just ("abc"
, Credentials "usrx" "passx" Nothing)
, konfigOptions = []
, konfigInt32 = 256
}
let json = Aeson.object
[ "hostname" .= Aeson.Null -- mandatory field
, "port" .= Aeson.Null -- optional with default
, "comment" .= Aeson.Null -- optional field
, "credentials" .= Aeson.object
[ "domain" .= Aeson.Null ]
, "alternates" .= [ Aeson.toJSON "abc"
, Aeson.object
[ "username" .= "usrx"
, "password" .= "passx"
]
]
]
let Result _ iss = update initial unjsonKonfig json
assertEqual "Cannot reset mandatory field without default"
[Anchored (Path [PathElemKey "hostname"]) "expected Text, encountered Null"] iss
return ()
test_array_modes :: Test
test_array_modes = "test_array_modes" ~: do
let json = Aeson.object
[ "hostname" .= ("www.example.com" ::Text.Text)
]
let json1 = Aeson.object
[ "hostname" .= ["www.example.com" ::Text.Text]
]
let p0 :: UnjsonDef [Text.Text]
p0 = objectOf $ pure id
<*> fieldBy "hostname" id
"Single value or array"
(arrayOf unjsonDef)
let p1 :: UnjsonDef [Text.Text]
p1 = objectOf $ pure id
<*> fieldBy "hostname" id
"Single value or array"
(arrayWithModeOf ArrayModeParseSingle unjsonAeson)
let p2 :: UnjsonDef [Text.Text]
p2 = objectOf $ pure id
<*> fieldBy "hostname" id
"Single value or array"
(arrayWithModeOf ArrayModeParseAndOutputSingle unjsonDef)
let Result _val0 iss0 = parse p0 json
assertEqual "Does not parse value in strict array mode"
[Anchored (Path [PathElemKey "hostname"])
"Error in $: expected Vector a, encountered String"] iss0
let Result val1 iss1 = parse p1 json
assertEqual "No problems" [] iss1
assertEqual "Accepts singel value in ArrayModeParseSingle" ["www.example.com"] val1
let sjson1 = unjsonToJSON p1 val1
assertEqual "Same json" json1 sjson1
let Result val2 iss2 = parse p2 json
assertEqual "No problems" [] iss2
assertEqual "Array fetch produced result" ["www.example.com"] val2
let sjson2 = unjsonToJSON p2 val2
assertEqual "Same json" json sjson2
return ()
test_array_update_by_primary_key :: Test
test_array_update_by_primary_key = "test_array_update_by_primary_key" ~: do
let json = Aeson.object
[ "array" .= [ Aeson.object
[ "id" .= 12
, "value" .= "for 12"
]
, Aeson.object
[ "id" .= 17
, "value" .= "for 17"
]
, Aeson.object
[ "id" .= 3
, "value" .= "for 3"
]
, Aeson.object
[ "id" .= 17
, "value" .= "wrong value for 17"
]
]
]
let json1 = Aeson.object
[ "array" .= [ Aeson.object -- 17 is first now, value left intact
[ "id" .= 17
]
, Aeson.object -- 3 is not there, but 4 is new
[ "id" .= 4
, "value" .= "for 4"
]
, Aeson.object
[ "id" .= 12 -- 12 got new value
, "value" .= "for 12 new value"
]
]
]
let unjsonPair = objectOf $ pure (,)
<*> field "id"
fst
"Unique id"
<*> field "value"
snd
"Value"
let pk1 = fst
pk2 = objectOf $ field "id" id "Unique id"
let p0 :: UnjsonDef [(Int,Text.Text)]
p0 = objectOf $ pure id
<*> fieldBy "array"
id
"Array updated by primary key"
(arrayWithPrimaryKeyOf pk1 pk2 unjsonPair)
let Result val0 iss0 = parse p0 json
assertEqual "No problems" [] iss0
assertEqual "Parsing keeps proper order" [(12,"for 12"),(17,"for 17"),(3,"for 3"),(17,"wrong value for 17")] val0
let Result val1 iss1 = update val0 p0 json1
assertEqual "No problems" [] iss1
assertEqual "Update keeps proper order" [(17,"for 17"),(4,"for 4"),(12,"for 12 new value")] val1
return ()
test_maps :: Test
test_maps = "test_maps" ~: do
let json = Aeson.object
[ "k1" .= (12 :: Int)
, "k2" .= (1122 :: Int)
, "a4" .= (666 :: Int)
]
jsonEmbedded = Aeson.object
[ "a_map" .= json ]
let unjsonMapByInstance :: (Unjson a, Typeable a) => UnjsonDef a
unjsonMapByInstance = objectOf $ pure id
<*> field "a_map"
id
"The only map"
let unjsonMapByExplicit :: (Unjson a, Typeable a) => UnjsonDef (HashMap.HashMap Text.Text a)
unjsonMapByExplicit = objectOf $ pure id
<*> fieldBy "a_map"
id
"The only map"
(mapOf unjsonDef)
let Result val0 iss0 = parse unjsonMapByInstance jsonEmbedded
assertEqual "No problems" [] iss0
assertEqual "Parsing keeps proper order in Data.Map" (Map.fromList [("k1"::String, 12::Int),("k2", 1122), ("a4", 666)]) val0
let Result val1 iss1 = parse unjsonMapByInstance jsonEmbedded
assertEqual "No problems" [] iss1
assertEqual "Parsing keeps proper order" (HashMap.fromList [("k1"::String, 12::Int),("k2", 1122), ("a4", 666)]) val1
let Result val2 iss2 = parse unjsonMapByExplicit jsonEmbedded
assertEqual "No problems" [] iss2
assertEqual "Parsing keeps proper order" (LazyHashMap.fromList [("k1"::Text.Text, 12::Int),("k2", 1122), ("a4", 666)]) val2
return ()
data PlainUnion
= PlainUnionA
{ plainUnionKey1 :: String
, plainUnionKey2 :: Maybe Int
}
| PlainUnionB
{ plainUnionKey3 :: Int
, plainUnionKey4 :: Int
}
deriving (Eq, Show, Typeable, Data)
unjsonPlainUnion :: UnjsonDef PlainUnion
unjsonPlainUnion = unionOf
[ (unjsonIsConstrByName "PlainUnionA",
pure PlainUnionA
<*> field "key1" plainUnionKey1 ""
<*> fieldOpt "key2" plainUnionKey2 "")
, (unjsonIsConstrByName "PlainUnionB",
pure PlainUnionB
<*> field "key3" plainUnionKey3 ""
<*> fieldDef "key4" 123 plainUnionKey4 "")
]
test_plain_unions :: Test
test_plain_unions = "test_maps" ~: do
-- simplest case
let json1 = Aeson.object
[ "key1" .= ("abc" :: String)
]
let Result val1 iss1 = parse unjsonPlainUnion json1
assertEqual "No problems" [] iss1
assertEqual "Got expected value" (PlainUnionA "abc" Nothing) val1
-- anyway choose first object on list, because 'key1' is present
let json2 = Aeson.object
[ "key1" .= ("abc" :: String)
, "key3" .= ("abc" :: String)
, "key4" .= ("abc" :: String)
]
let Result val2 iss2 = parse unjsonPlainUnion json2
assertEqual "No problems" [] iss2
assertEqual "Got expected value" (PlainUnionA "abc" Nothing) val2
-- key is present so PlainUnionA will be chosen and then fail
-- because of wrong types
let json3 = Aeson.object
[ "key1" .= (123 :: Int)
]
let Result _val3 iss3 = parse unjsonPlainUnion json3
assertEqual "Cannot parse PlainUnionA" [Anchored (Path [PathElemKey "key1"]) "expected String, encountered Number"] iss3
-- choose PlainUnionB
let json4 = Aeson.object
[ "xx" .= (123 :: Int)
, "key3" .= (15523 :: Int)
, "key4" .= (13 :: Int)
]
let Result val4 iss4 = parse unjsonPlainUnion json4
assertEqual "No issues" [] iss4
assertEqual "Got expected value" (PlainUnionB 15523 13) val4
return ()
data ROTest = ROTest
{ roTestF1 :: Int
, roTestF2 :: String
}
deriving (Eq,Ord,Show)
unjsonROTest :: UnjsonDef ROTest
unjsonROTest = objectOf $
pure (\f2 -> ROTest 444 f2)
<*> field "f2" roTestF2 "f2 is a normal field"
<* fieldReadonly "f1" roTestF1 "f1 is readonly field"
test_readonly_fields :: Test
test_readonly_fields = "test_readonly_fields" ~: do
-- simplest case
let json1 = Aeson.object
[ "f2" .= ("abc" :: String)
, "f1" .= (123 :: Int)
]
let Result val1 iss1 = parse unjsonROTest json1
assertEqual "No problems" [] iss1
assertEqual "Got expected value" (ROTest 444 "abc") val1
let json2 = Aeson.object
[ "f2" .= ("abc" :: String)
]
let Result val2 iss2 = parse unjsonROTest json2
assertEqual "No problems" [] iss2
assertEqual "Got expected value" (ROTest 444 "abc") val2
let json3 = Aeson.object
[ "f2x" .= ("abc" :: String)
, "f1" .= ("should not be inspected" :: String)
]
let Result _val3 iss3 = parse unjsonROTest json3
assertEqual "There are problems" [Anchored (Path [ PathElemKey "f2"
]) "missing key"] iss3
return ()
tests :: Test
tests = test [ test_proper_parse
, test_missing_key
, test_wrong_value_type
, test_tuple_parsing
, test_symmetry_of_serialization
, test_serialize_with_nulls
, test_parse_either_field
, test_enum_field
, test_update_from_serialization
, test_update_from_serialization_with_reset_to_default
, test_array_modes
, test_array_update_by_primary_key
, test_pretty_serialization
, test_semantic_errors_on_values
, test_maps
, test_plain_unions
, test_readonly_fields
]
main :: IO Counts
main = do results <- runTestTT tests
if (errors results + failures results == 0)
then exitWith ExitSuccess
else exitWith (ExitFailure 1)
updateExampleRendering :: IO ()
updateExampleRendering = do
contents <- readFile "src/Data/Unjson.hs"
let (before,exampleAndRest) = break (=="-- Example rendering:") (lines contents)
(_example,after) = break ("render ::" `isPrefixOf`) exampleAndRest
_ <- return $! length after
writeFile "src/Data/Unjson.hs"
(unlines (before ++ ["-- Example rendering:", "--"] ++
(map ((++)"-- > ") $ lines $ filterOutAnsi $ render unjsonKonfig) ++
after))
filterOutAnsi :: String -> String
filterOutAnsi "" = ""
filterOutAnsi ('\ESC' : '[' : rest) = filterOutAnsiTillEndOfMulticharSequence rest
filterOutAnsi ('\ESC' : _ : rest) = filterOutAnsi rest
filterOutAnsi (c : rest) = c : filterOutAnsi rest
filterOutAnsiTillEndOfMulticharSequence :: String -> String
filterOutAnsiTillEndOfMulticharSequence (c : rest) | c >= '@' = filterOutAnsi rest
filterOutAnsiTillEndOfMulticharSequence (_c : rest) =
filterOutAnsiTillEndOfMulticharSequence rest
filterOutAnsiTillEndOfMulticharSequence [] = []
data Example = Example
{ exampleName :: Text.Text,
exampleArray :: [Int],
exampleOptional :: Maybe Bool,
exampleIntAsString :: Int }
unjsonExample :: UnjsonDef Example
unjsonExample = objectOf $ pure Example
<*> field "name"
exampleName
"Name used for example"
<*> fieldDefBy "array_of_ints" []
exampleArray
"Array of integers, optional, defaults to empty list"
(arrayOf unjsonDef)
<*> fieldOpt "optional_bool"
exampleOptional
"Optional boolean"
<*> fieldBy "int_as_string"
exampleIntAsString
"Integer value serialized as a string value in json"
(invmap (read :: String -> Int) (show :: Int -> String) unjsonDef)
newtype Theme = Theme { unTheme :: Int }
unjsonTheme :: UnjsonDef Theme
unjsonTheme = invmap (Theme . read :: String -> Theme) (show . unTheme :: Theme -> String) unjsonDef
instance Unjson Theme where
unjsonDef = unjsonTheme