packages feed

registry-aeson-0.2.3.0: test/Test/Data/Registry/Aeson/EncoderSpec.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}

module Test.Data.Registry.Aeson.EncoderSpec where

import Data.Aeson hiding (encode)
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BL (fromStrict, toStrict)
import Data.Registry
import Data.Registry.Aeson.Encoder
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Time
import Protolude
import Test.Data.Registry.Aeson.DataTypes
import Test.Data.Registry.Aeson.SimilarDataTypes qualified as SimilarDataTypes
import Test.Tasty.Hedgehogx hiding (string)

test_encode = test "encode" $ do
  checkEncodings (Identifier 123) "123"
  checkEncodings email1 "{'_email':'me@here.com'}"
  checkEncodings delivery0 "{'tag':'NoDelivery'}"
  checkEncodings delivery1 "{'tag':'ByEmail','contents':{'_email':'me@here.com'}}"
  checkEncodings person1 "{'email':{'_email':'me@here.com'},'identifier':123}"
  checkEncodings delivery2 "{'tag':'InPerson','contents':[{'email':{'_email':'me@here.com'},'identifier':123},{'_datetime':'2022-04-18T00:00:12Z'}]}"

test_all_nullary_to_string_tag = test "allNullaryToStringTag" $ do
  checkEncodingsWith allNullaryOptions AllNullary1 "'AllNullary1'"
  checkEncodingsWith allNullaryOptions AllNullary2 "'AllNullary2'"

test_field_modifier = test "fieldLabelModifier" $ do
  checkEncodingsWith fieldLabelModifierOptions (FieldLabelModifier1 123) "{'tag':'FieldLabelModifier1','__field1':123}"

test_constructor_modifier = test "constructorTagModifier" $ do
  checkEncodingsWith constructorTagModifierOptions (ConstructorTagModifier1 123) "{'tag':'__ConstructorTagModifier1','ctField1':123}"

test_omit_nothing_fields = test "omitNothingFields" $ do
  checkEncodingsWith (defaultOptions {omitNothingFields = True}) (Identifier 123) "123"
  checkEncodingsWith omitNothingFieldsOptions (OmitNothingFields1 Nothing 123) "{'tag':'OmitNothingFields1','onField2':123}"

test_unwrap_unary_records = test "unwrapUnaryRecords" $ do
  checkEncodingsWith unwrapUnaryRecordsOptions (UnwrapUnaryRecords1 123) "123"

test_tag_single_constructors = test "TagSingleConstructors" $ do
  checkEncodingsWith tagSingleConstructorsOptions (TagSingleConstructors1 123) "{'tag':'TagSingleConstructors1', 'tsField1':123}"

test_untagged_values_sum_encoding = test "UntaggedValueSumEncoding" $ do
  checkEncodingsWith untaggedValueOptions (UntaggedValueSumEncoding1 123) "{'uvField1':123}"

test_object_with_single_field_sum_encoding = test "ObjectWithSingleFieldSumEncoding" $ do
  checkEncodingsWith objectWithSingleFieldSumEncodingOptions (ObjectWithSingleFieldSumEncoding1 123) "{'ObjectWithSingleFieldSumEncoding1':{'owsfField1':123}}"

test_two_elem_array_sum_encoding = test "TwoElemArray" $ do
  checkEncodingsWith twoElemArraySumEncodingOptions (TwoElemArraySumEncoding1 123) "['TwoElemArraySumEncoding1',{'teaField1':123}]"

-- * HELPERS

encoders :: Registry _ _
encoders =
  $(makeEncoder ''Delivery)
      -- test that it is possible to generate an Encoder when there are name clashes
    <: $(makeEncoderQualifiedLast ''SimilarDataTypes.Person)
    <: $(makeEncoderQualifiedLast ''SimilarDataTypes.Email)
    <: $(makeEncoderQualifiedLast ''SimilarDataTypes.Identifier)
    <: $(makeEncoderQualifiedLast ''SimilarDataTypes.DateTime)
    <: $(makeEncoder ''Person)
    <: $(makeEncoder ''Email)
    <: $(makeEncoder ''Identifier)
    <: $(makeEncoder ''AllNullary)
    <: $(makeEncoder ''FieldLabelModifier)
    <: $(makeEncoder ''ConstructorTagModifier)
    <: $(makeEncoder ''OmitNothingFields)
    <: $(makeEncoder ''UnwrapUnaryRecords)
    <: $(makeEncoder ''TagSingleConstructors)
    <: $(makeEncoder ''UntaggedValueSumEncoding)
    <: $(makeEncoder ''ObjectWithSingleFieldSumEncoding)
    <: $(makeEncoder ''TwoElemArraySumEncoding)
    <: fun datetimeEncoder
    <: fun utcTimeEncoder
    <: encodeMaybeOf @Int
    <: jsonEncoder @Text
    <: jsonEncoder @Int
    <: defaultEncoderOptions

-- | This Encoder for DateTime reproduces the default generic one
datetimeEncoder :: Encoder DateTime
datetimeEncoder = fromValue $ \(DateTime dt) -> do
  let formatted = toS $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" dt
  Object [("_datetime", String formatted)]

-- | This Encoder for DateTime reproduces the default generic one
utcTimeEncoder :: Encoder UTCTime
utcTimeEncoder = fromValue $ String . toS . formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ"

-- | Check that the encoding performed with the registry and the one performed with a Generic instance are the same
--   This helps validating the encoding algorithm based on the various Options values
--   Additionally we check that the bytestring created with an Encoding (with encodeByteString)
--   is the same as the one produced from a Value (with encodeValue)
checkEncodings :: forall a. (ToJSON a, Typeable a) => a -> Text -> PropertyT IO ()
checkEncodings a t = withFrozenCallStack $ checkEncodingsWith defaultOptions a t

checkEncodingsWith :: forall a. (ToJSON a, Typeable a) => Options -> a -> Text -> PropertyT IO ()
checkEncodingsWith options a expectShort = withFrozenCallStack $ do
  let expected = T.replace "'" "\"" expectShort
  let encoder = make @(Encoder a) (val options <: encoders)
  let asValue = BL.toStrict . A.encode $ encodeValue encoder a
  let asEncoding = encodeByteString encoder a
  let asGeneric = BL.toStrict $ A.encode a

  annotate "the encoded Value must be the same as the generic one"
  checkValue asValue asGeneric

  annotate "the encoded Value must be the expected value"
  checkValue asValue (T.encodeUtf8 expected)

  annotate "the encoded Value must be the same as the one using a direct encoding"
  checkValue asValue asEncoding

checkValue :: ByteString -> ByteString -> PropertyT IO ()
checkValue actual expected = withFrozenCallStack $ do
  case (decode @Value $ BL.fromStrict actual, decode $ BL.fromStrict expected) of
    (Just a, Just e) -> if a == e then success else actual === expected
    (actualValue, expectedValue) -> annotateShow ("cannot decode values", actual, expected, actualValue, expectedValue) >> failure