packages feed

generic-override-aeson-0.4.0.0: test/Test.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
module Main where

import Data.Aeson (FromJSON(parseJSON), Result(Success), ToJSON(toJSON), Value, fromJSON)
import Data.Aeson.QQ.Simple (aesonQQ)
import Data.Override (Override(Override), As, At, With)
import Data.Override.Aeson (AesonOption(..), WithAesonOptions(..))
import Data.Text (Text)
import GHC.Generics (Generic)
import LispCaseAeson (LispCase(LispCase))
import Test.Hspec
import Text.Read (readMaybe)

import qualified Data.Text as Text

main :: IO ()
main = hspec do
  describe "Override ToJSON machinery" do
    it "Rec1" testRec1
    it "Rec2" testRec2
    it "Rec3" testRec3
    it "Rec4" testRec4
    it "Rec5" testRec5
    it "Rec6" testRec6
    it "Rec7" testRec6
    it "Sum1" testSum1
    it "Options1" testOptions1

newtype Uptext = Uptext { unUptext :: Text }

instance ToJSON Uptext where
  toJSON = toJSON . Text.toUpper . unUptext

newtype Shown a = Shown { unShown :: a }

instance (Show a) => ToJSON (Shown a) where
  toJSON = toJSON . show . unShown
instance (Read a) => FromJSON (Shown a) where
  parseJSON v = do
    s <- parseJSON v
    case readMaybe s of
      Nothing -> fail "read: no parse"
      Just a -> pure $ Shown a

newtype CharArray s = CharArray { unCharArray :: s }
  deriving stock (Functor)

instance ToJSON (CharArray String) where
  toJSON = toJSON . map (:[]) . unCharArray

instance FromJSON (CharArray String) where
  parseJSON v = do
    cs :: [String] <- parseJSON v
    pure $ CharArray $ concat cs

instance ToJSON (CharArray Text) where
  toJSON = toJSON . fmap Text.unpack

-- | Overriding instances by type.
data Rec1 = Rec1
  { foo :: Int
  , bar :: String
  , baz :: Text
  } deriving stock (Show, Eq, Generic)
    deriving (ToJSON)
      via Override Rec1
            '[ Text `As` Uptext
             , Int `As` Shown Int
             ]

testRec1 :: IO ()
testRec1 = do
  toJSON Rec1 { foo = 12, bar = "hi", baz = "bye" }
    `shouldBe` [aesonQQ|
    {
      "foo": "12",
      "bar": "hi",
      "baz": "BYE"
    }
  |]

-- | Overriding instances by field name.
data Rec2 = Rec2
  { foo :: Int
  , bar :: Text
  , baz :: Text
  } deriving stock (Show, Eq, Generic)
    deriving (ToJSON)
      via Override Rec2
            '[ "baz" `As` CharArray Text
             , "bar" `As` Uptext
             ]

testRec2 :: IO ()
testRec2 = do
  toJSON Rec2 { foo = 12, bar = "hi", baz = "bye" }
    `shouldBe` [aesonQQ|
    {
      "foo": 12,
      "bar": "HI",
      "baz": ["b", "y", "e"]
    }
  |]

-- | Overriding instances by type and field name.
data Rec3 = Rec3
  { foo :: Int
  , bar :: String
  , baz :: Text
  } deriving stock (Show, Eq, Generic)
    deriving (ToJSON)
      via Override Rec3
            '[ String `As` CharArray String
             , "foo" `As` Shown Int
             , Text `As` Uptext
             ]

testRec3 :: IO ()
testRec3 = do
  toJSON Rec3 { foo = 12, bar = "hi", baz = "bye" }
    `shouldBe` [aesonQQ|
    {
      "foo": "12",
      "bar": ["h", "i"],
      "baz": "BYE"
    }
  |]

-- | Overriding instance by type and field name; first match wins.
-- In this case, 'foo' and 'bar' use 'CharArray Text' and 'baz' uses 'Uptext'.
data Rec4 = Rec4
  { foo :: Text
  , bar :: Text
  , baz :: Text
  } deriving stock (Show, Eq, Generic)
    deriving (ToJSON)
      via Override Rec4
            '[ "baz" `As` Uptext
             , Text `As` CharArray Text
             ]

testRec4 :: IO ()
testRec4 = do
  toJSON Rec4 { foo = "go", bar = "hi", baz = "bye" }
    `shouldBe` [aesonQQ|
    {
      "foo": ["g", "o"],
      "bar": ["h", "i"],
      "baz": "BYE"
    }
  |]

-- We can compose an 'Override' with another deriving-via newtype, 'LispCase'.
data Rec5 = Rec5
  { fooBar :: Int
  , baz :: Text
  , quuxSpamEggs :: String
  } deriving stock (Show, Eq, Generic)
    deriving (ToJSON)
      via LispCase (Override Rec5
            '[ "fooBar" `As` Shown Int
             , Text `As` Uptext
             , "quuxSpamEggs" `As` CharArray String
             ])

testRec5 :: IO ()
testRec5 = do
  toJSON Rec5 { fooBar = 1, baz = "hi", quuxSpamEggs = "bye" }
    `shouldBe` [aesonQQ|
      {
        "foo-bar": "1",
        "baz": "HI",
        "quux-spam-eggs": ["b", "y", "e"]
      }
    |]

-- Test 'Override' for both 'ToJSON' and 'FromJSON'.
data Rec6 = Rec6
  { foo :: Int
  , bar :: String
  , baz :: Text
  } deriving stock (Show, Eq, Generic)
    deriving (ToJSON, FromJSON)
      via Override Rec6
            '[ "foo" `As` Shown Int
             , String `As` CharArray String
             ]

testRec6 :: IO ()
testRec6 = do
  Rec6 { foo = 1, bar = "hi", baz = "bye" }
    `shouldRoundtripAs` [aesonQQ|
      {
        "foo": "1",
        "bar": ["h", "i"],
        "baz": "bye"
      }
    |]

-- Test 'Override' for both 'ToJSON' and 'FromJSON'.
data Rec7 = Rec7
  { foo :: Int
  , bar :: String
  , baz :: Text
  } deriving stock (Show, Eq, Generic)
    deriving (ToJSON, FromJSON)
      via Override Rec7
            '[ "foo" `With` Shown
             , String `With` CharArray
             ]

testRec7 :: IO ()
testRec7 = do
  Rec7 { foo = 1, bar = "hi", baz = "bye" }
    `shouldRoundtripAs` [aesonQQ|
      {
        "foo": "1",
        "bar": ["h", "i"],
        "baz": "bye"
      }
    |]

newtype Reverse a = Reverse [a]

instance (ToJSON a) => ToJSON (Reverse a) where
  toJSON (Reverse xs) = toJSON $ reverse xs

instance (FromJSON a) => FromJSON (Reverse a) where
  parseJSON = fmap (Reverse . reverse) . parseJSON

newtype Not = Not Bool

instance ToJSON Not where
  toJSON (Not b) = toJSON $ not b

instance FromJSON Not where
  parseJSON = fmap (Not . not) . parseJSON

data Sum1 a =
    Sum1List [a]
  | Sum1Trip a Char Bool
  | Sum1Null
  deriving stock (Show, Eq, Generic)
  deriving (ToJSON, FromJSON)
    via Override (Sum1 a)
          '[ At "Sum1List" 0 (Reverse a)
           , At "Sum1Trip" 2 Not
           ]

testSum1 :: IO ()
testSum1 = do
  Sum1List ['a', 'b'] `shouldRoundtripAs` [aesonQQ|
    {
      "tag": "Sum1List",
      "contents": "ba"
    }
  |]
  Sum1Trip 'a' 'b' True `shouldRoundtripAs` [aesonQQ|
    {
      "tag": "Sum1Trip",
      "contents": ["a", "b", false]
    }
  |]
  Sum1Null @Char `shouldRoundtripAs` [aesonQQ|
    {
      "tag": "Sum1Null"
    }
  |]


data Options1 =
    Options1A { foo :: Maybe Int, bar :: String }
  | Options1B Options1BBody
  | Options1C
    deriving stock (Eq, Show, Generic)
    deriving (FromJSON, ToJSON)
      via Override Options1
            '[ "foo" `As` Maybe (Shown Int)
             ] `WithAesonOptions`
            '[ 'OmitNothingFields
             , 'SumEncodingTaggedObject "type" "data"
             ]

data Options1BBody = Options1BBody { baz :: Int }
  deriving stock (Eq, Show, Generic)
  deriving anyclass (FromJSON, ToJSON)

testOptions1 :: IO ()
testOptions1 = do
  Options1A { foo = Nothing, bar = "boo" }
    `shouldRoundtripAs` [aesonQQ| { "type": "Options1A", "bar": "boo" } |]
  Options1A { foo = Just 1, bar = "boo" }
    `shouldRoundtripAs` [aesonQQ| { "type": "Options1A", "foo": "1", "bar": "boo" } |]
  Options1B Options1BBody { baz = 2 }
    `shouldRoundtripAs` [aesonQQ| { "type": "Options1B", "data": { "baz": 2 } } |]
  Options1C
    `shouldRoundtripAs` [aesonQQ| { "type": "Options1C" } |]

shouldRoundtripAs
  :: (ToJSON a, FromJSON a, Eq a, Show a, HasCallStack)
  => a -> Value -> IO ()
shouldRoundtripAs x j = do
  toJSON x `shouldBe` j
  fromJSON j `shouldBe` Success x