packages feed

aeson-2.2.0.0: tests/UnitTests/OptionalFields.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

module UnitTests.OptionalFields (optionalFields) where

import GHC.Generics (Generic)
import Data.Maybe (isNothing)
import UnitTests.OptionalFields.Common
import UnitTests.OptionalFields.Generics (omitGenerics)
import UnitTests.OptionalFields.TH (omitTH)
import UnitTests.OptionalFields.Manual (omitManual)

optionalFields :: TestTree
optionalFields = testGroup "optional fields"
  [ omitGenerics
  , omitTH
  , omitManual
  , proofOfConcept
  ]

-- c.f. https://github.com/haskell/aeson/pull/839#issuecomment-782453060
data P = P
  { x :: Nullable Int -- Field is required, but can be null.
  , y :: Undefineable Int -- Field is optional, but cannot be null.
  , z :: NullOrUndefineable Int -- Field is optional, and can be null.
  }
  deriving (Eq, Show, Generic)

instance ToJSON P where
  toJSON = genericToJSON opts
  toEncoding = genericToEncoding opts

instance FromJSON P where
  parseJSON = genericParseJSON opts

newtype Nullable a = Nullable (Maybe a)
  deriving (Eq, Show, Generic)

instance ToJSON a => ToJSON (Nullable a) where
  toJSON = genericToJSON opts
  toEncoding = genericToEncoding opts

instance FromJSON a => FromJSON (Nullable a) where
  parseJSON = genericParseJSON opts

newtype Undefineable a = Undefineable (Maybe a)
  deriving (Eq, Show, Generic)

instance ToJSON a => ToJSON (Undefineable a) where
  toJSON = genericToJSON opts
  toEncoding = genericToEncoding opts
  omitField (Undefineable a) = isNothing a

instance FromJSON a => FromJSON (Undefineable a) where
  parseJSON Null = fail "Undefineable.parseJSON: expected non-null value"
  parseJSON v = genericParseJSON opts v
  omittedField = Just (Undefineable Nothing)

newtype NullOrUndefineable a = NullOrUndefineable (Maybe a)
  deriving (Eq, Show, Generic)

instance ToJSON a => ToJSON (NullOrUndefineable a) where
  toJSON = genericToJSON opts
  toEncoding = genericToEncoding opts
  omitField (NullOrUndefineable a) = isNothing a

instance FromJSON a => FromJSON (NullOrUndefineable a) where
  parseJSON = genericParseJSON opts
  omittedField = Just (NullOrUndefineable Nothing)

opts :: Options
opts = defaultOptions { omitNothingFields = True }

fullP :: P
fullP = P (Nullable $ Just 0) (Undefineable $ Just 0) (NullOrUndefineable $ Just 0)

zero :: Key -> (Key, Value)
zero k = k .= (0 :: Int)

proofOfConcept :: TestTree
proofOfConcept = testGroup "Type-directed optional fields Proof of Concept"
  [ testGroup "toJSON"
    [ testCase "x is not omitted when Nothing" $
        let subject = fullP {x = Nullable Nothing}
            expected = object ["x" .= Null, zero "y", zero "z"]
         in toJSON subject @?= expected

    , testCase "y is omitted when Nothing" $
        let subject = fullP {y = Undefineable Nothing}
            expected = object [zero "x", zero "z"]
         in toJSON subject @?= expected

    , testCase "z is omitted when Nothing" $
        let subject = fullP {z = NullOrUndefineable Nothing}
            expected = object [zero "x", zero "y"]
         in toJSON subject @?= expected
    ]

  , testGroup "parseJSON"
    [ testCase "x can be null" $
        let subject = object ["x" .= Null, zero "y", zero "z"]
            expected = Just fullP {x = Nullable Nothing}
         in decode (encode subject) @?= expected

    , testCase "x cannot be omitted" $
        let subject = object [zero "y", zero "z"]
            expected = Nothing :: Maybe P
         in decode (encode subject) @?= expected

    , testCase "y can be omitted" $
        let subject = object [zero "x", zero "z"]
            expected = Just fullP {y = Undefineable Nothing}
         in decode (encode subject) @?= expected

    , testCase "y cannot be null" $
        let subject = object [zero "x", "y" .= Null, zero "z"]
            expected = Nothing :: Maybe P
         in decode (encode subject) @?= expected

    , testCase "z can be null" $
        let subject = object [zero "x", zero "y", "z" .= Null]
            expected = Just fullP {z = NullOrUndefineable Nothing}
         in decode (encode subject) @?= expected

    , testCase "z can be omitted" $
        let subject = object [zero "x", zero "y"]
            expected = Just fullP {z = NullOrUndefineable Nothing}
         in decode (encode subject) @?= expected
    ]
  ]