packages feed

jordan-0.2.0.0: test/Jordan/RoundTripSpec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Jordan.RoundTripSpec where

import Data.ByteString.Lazy (ByteString, fromStrict, toStrict)
import qualified Data.ByteString.Lazy as LBS
import Data.Functor.Contravariant (Contravariant (..))
import Data.Text (Text, unpack)
import qualified Data.Text as Text
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Typeable (Proxy (..), Typeable)
import GHC.Generics
import Jordan (parseViaAttoparsec, toJSONViaBuilder)
import Jordan.FromJSON.Class (FromJSON (..), GFromJSON (..), JSONParser (..))
import Jordan.FromJSON.UnboxedReporting (parseOrReport)
import Jordan.Generic.Options
import Jordan.ToJSON.Class (GToJSON (..), JSONSerializer (..), ToJSON (..))
import Test.Hspec (Spec, describe)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
import Test.QuickCheck.Utf8

data PropertyResult exp = MkPropertyResult
  { expectedValue :: exp,
    actualValue :: Either String exp,
    jsonStringTested :: Text
  }
  deriving (Show, Read)

roundtripProperty ::
  (ToJSON a, FromJSON a, Arbitrary a, Show a, Eq a, Eq err, Show err) =>
  Proxy a ->
  (ByteString -> Either err a) ->
  (err -> String) ->
  Property
roundtripProperty (Proxy :: Proxy a) parser mapErr =
  forAllShrink (arbitrary @a) (shrink @a) $ \a ->
    let built = toJSONViaBuilder a
     in counterexample (show built) $ parser (toJSONViaBuilder a) === Right a

makeResult ::
  (ToJSON a, FromJSON a, Arbitrary a, Show a, Eq a) =>
  Proxy a ->
  (a -> b) ->
  (b -> String) ->
  (b -> Either String a) ->
  Property
makeResult (Proxy :: Proxy a) convForward convString convBack =
  forAllShow (arbitrary @a) showResult convert
  where
    showResult a =
      show a
        <> "\n"
        <> convString (convForward a)
        <> "\n"
        <> showError (convBack $ convForward a)
    convert a = convBack (convForward a) == pure a
    showError :: Either String a -> String
    showError (Left err) = "Error\n" <> err
    showError (Right a) = "Success: " <> show a

newtype ExtremelyBasic = ExtremelyBasic {getExtremelyBasic :: ()}
  deriving (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON)

instance Arbitrary ExtremelyBasic where
  arbitrary = ExtremelyBasic <$> arbitrary

data TwoFieldsRec = TwoFieldsRec {firstField :: Int, secondField :: Int}
  deriving (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON)

instance Arbitrary TwoFieldsRec where
  arbitrary = TwoFieldsRec <$> arbitrary <*> arbitrary

data RLMSum
  = Mike
  | Jay
  | Rich
  | Jack
  | Josh
  deriving (Eq, Show, Generic, Bounded, Enum)
  deriving anyclass (ToJSON, FromJSON)

instance Arbitrary RLMSum where
  arbitrary = arbitraryBoundedEnum

data TwoChoices
  = ChoseFirst {getFirst :: Int}
  | ChoseSecond {getSecondA :: Int, getSecondB :: Int}
  deriving (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON)

instance Arbitrary TwoChoices where
  arbitrary =
    oneof
      [ ChoseFirst <$> arbitrary,
        ChoseSecond <$> arbitrary <*> arbitrary
      ]

data FakePerson = FakePerson
  { age :: Int,
    name :: String,
    cool :: Bool
  }
  deriving (Eq, Show, Generic)
  deriving anyclass (FromJSON, ToJSON)

instance Arbitrary FakePerson where
  arbitrary =
    FakePerson
      <$> arbitrary
      <*> fmap unpack genValidUtf8
      <*> arbitrary

newtype OnlyText = OnlyText {getText :: Text}
  deriving (Show, Eq, Generic)

shrinkText :: Text -> [Text]
shrinkText t
  | Text.length t == 0 = []
  | Text.length t == 1 = [""]
  | otherwise = Text.singleton <$> Text.unpack t

instance ToJSON OnlyText where
  toJSON = contramap getText serializeText

instance FromJSON OnlyText where
  fromJSON = OnlyText <$> parseText

instance Arbitrary OnlyText where
  arbitrary = OnlyText <$> genValidUtf8
  shrink (OnlyText t) = OnlyText <$> shrinkText t

data EnumyObject
  = EnumA
  | EnumB
  | EnumC
  | EnumObject {enumValue :: Text}
  | EnumOther OnlyText
  deriving (Show, Eq, Generic)
  deriving anyclass (ToJSON, FromJSON)

instance Arbitrary EnumyObject where
  arbitrary =
    oneof
      [ pure EnumA,
        pure EnumB,
        pure EnumC,
        EnumObject <$> genValidUtf8,
        EnumOther <$> arbitrary
      ]
  shrink (EnumObject o) = EnumObject <$> shrinkText o
  shrink (EnumOther ot) = EnumOther <$> shrink ot
  shrink _ = []

data AllTogether = AllTogether
  { extremelyBasic :: ExtremelyBasic,
    twoFieldsRec :: TwoFieldsRec,
    rlmSum :: RLMSum,
    fakePerson :: FakePerson,
    onlyText :: OnlyText,
    enumyObject :: EnumyObject
  }
  deriving (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON)

instance Arbitrary AllTogether where
  arbitrary =
    AllTogether
      <$> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
  shrink (AllTogether ub tfr rs fp ot eo) =
    AllTogether
      <$> shrink ub
      <*> shrink tfr
      <*> shrink rs
      <*> shrink fp
      <*> shrink ot
      <*> shrink eo

data AnnoyinglyOptional = AnnoyinglyOptional
  { annoyingFirst :: Maybe Int,
    annoyingSecond :: Maybe Int,
    annoyingThird :: Maybe Int
  }
  deriving (Show, Read, Eq, Ord, Generic)
  deriving anyclass (ToJSON, FromJSON)

instance Arbitrary AnnoyinglyOptional where
  arbitrary =
    pure $ AnnoyinglyOptional Nothing Nothing Nothing

-- <$> arbitrary <*> arbitrary <*> arbitrary

data JustNulls = JustNulls
  { jna :: (),
    jnb :: (),
    jnc :: (),
    jnd :: (),
    jne :: (),
    jnf :: ()
  }
  deriving (Show, Read, Eq, Ord, Generic)
  deriving anyclass (ToJSON, FromJSON)

instance Arbitrary JustNulls where
  arbitrary =
    pure $
      JustNulls () () () () () ()

showViaText :: (ToJSON a) => a -> String
showViaText = unpack . decodeUtf8 . LBS.toStrict . toJSONViaBuilder

showViaBuilder :: (ToJSON a) => a -> String
showViaBuilder = unpack . decodeUtf8 . toStrict . toJSONViaBuilder

showLeft :: (Show a) => Either a b -> Either [Char] b
showLeft = \case
  Left a -> Left (show a)
  Right b -> Right b

instance Arbitrary a => Arbitrary (WithOptions opts a) where
  arbitrary = WithOptions <$> arbitrary
  shrink (WithOptions a) = WithOptions <$> shrink a

roundtrips' ::
  (Arbitrary a, Show a, Eq a, ToJSON a, FromJSON a) =>
  Proxy a ->
  Spec
roundtrips' p = do
  describe "when serializing via a builder" $ do
    prop "roundtrips back via attoparsec" $
      roundtripProperty p (parseViaAttoparsec . toStrict) show
    prop "roundstrips back via unboxed reporting" $
      roundtripProperty p (parseOrReport . toStrict) show

roundtrips ::
  ( Arbitrary a,
    Typeable a,
    Show a,
    Eq a,
    ToJSON a,
    FromJSON a,
    Generic a,
    GToJSON (Rep a),
    GFromJSON (Rep a)
  ) =>
  String ->
  Proxy a ->
  Spec
roundtrips n p@(Proxy :: Proxy a) = describe ("round-tripping " <> n) $ do
  describe "default roundtrips" $
    roundtrips' p
  describe "roundtrips with no options" $
    roundtrips' (Proxy @(WithOptions '[] a))
  describe "Roundtrips omitting nothing fields" $
    roundtrips' (Proxy @(WithOptions '[OmitNothingFields] a))
  describe "Roundtrips keeping nothing fields" $
    roundtrips' (Proxy @(WithOptions '[KeepNothingFields] a))

spec :: Spec
spec = describe "round-tripping generic values" $ do
  roundtrips "a newtype around ()" (Proxy @ExtremelyBasic)
  roundtrips "a record with two int fields" (Proxy @TwoFieldsRec)
  roundtrips "a sum enum type" (Proxy @RLMSum)
  roundtrips "a type with some text" (Proxy @FakePerson)
  roundtrips "just text" (Proxy @OnlyText)
  roundtrips "an object with some constructors as enums" (Proxy @EnumyObject)
  roundtrips "an object composed of all rountrip'd objects" (Proxy @AllTogether)
  roundtrips "an object with optional fields" (Proxy @AnnoyinglyOptional)
  roundtrips "an object that is jut a bunch of nulls" (Proxy @JustNulls)