packages feed

wireform-proto-0.2.0.0: test-integration/Test/WellKnown.hs

module Test.WellKnown (wellKnownTests) where

import Data.Aeson qualified as Aeson
import Data.ByteString qualified as BS
import Data.Hashable (hash, hashWithSalt)
import Data.Proxy (Proxy (..))
import Data.Vector qualified as V
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Proto.Decode
import Proto.Encode
import Proto.Google.Protobuf.Any
import Proto.Google.Protobuf.Any.Util
import Proto.Google.Protobuf.Duration
import Proto.Google.Protobuf.Empty
import Proto.Google.Protobuf.FieldMask
import Proto.Google.Protobuf.SourceContext
import Proto.Google.Protobuf.Struct
import Proto.Google.Protobuf.Timestamp
import Proto.Google.Protobuf.Wrappers
import Proto.Registry (TypeRegistry, emptyRegistry, lookupCodec, lookupDecoder, registerMessage)
import Test.Syd
import Test.Syd.Hedgehog ()


wellKnownTests :: Spec
wellKnownTests =
  describe
    "Well-Known Types"
    $ sequence_
      [ describe
          "Timestamp"
          $ sequence_
            [ it "roundtrip" $ property $ do
                s <- forAll $ Gen.int64 (Range.linear (-1000000000) 1000000000)
                n <- forAll $ Gen.int32 (Range.linear 0 999999999)
                let msg = defaultTimestamp {timestampSeconds = s, timestampNanos = n}
                    encoded = encodeMessage msg
                decodeMessage encoded === Right msg
            , it "default is zero-length" $ do
                let encoded = encodeMessage defaultTimestamp
                BS.length encoded `shouldBe` 0
            , it "sized encoding matches" $ do
                let msg = defaultTimestamp {timestampSeconds = 1234567890, timestampNanos = 123456789}
                BS.length (encodeMessage msg) `shouldBe` messageSize msg
            , it "JSON canonical RFC 3339" $ do
                let msg = defaultTimestamp {timestampSeconds = 1708000000, timestampNanos = 0}
                Aeson.toJSON msg `shouldBe` Aeson.String "2024-02-15T12:26:40Z"
            , it "JSON with nanos" $ do
                let msg = defaultTimestamp {timestampSeconds = 0, timestampNanos = 123456789}
                Aeson.toJSON msg `shouldBe` Aeson.String "1970-01-01T00:00:00.123456789Z"
            , it "JSON nanos trailing zeros trimmed" $ do
                let msg = defaultTimestamp {timestampSeconds = 0, timestampNanos = 100000000}
                Aeson.toJSON msg `shouldBe` Aeson.String "1970-01-01T00:00:00.1Z"
            ]
      , describe
          "Duration"
          $ sequence_
            [ it "roundtrip" $ property $ do
                s <- forAll $ Gen.int64 (Range.linear (-315576000000) 315576000000)
                n <- forAll $ Gen.int32 (Range.linear (-999999999) 999999999)
                let msg = defaultDuration {durationSeconds = s, durationNanos = n}
                    encoded = encodeMessage msg
                decodeMessage encoded === Right msg
            , it "JSON canonical seconds" $ do
                let msg = defaultDuration {durationSeconds = 3600, durationNanos = 0}
                Aeson.toJSON msg `shouldBe` Aeson.String "3600s"
            , it "JSON with nanos" $ do
                let msg = defaultDuration {durationSeconds = 1, durationNanos = 500000000}
                Aeson.toJSON msg `shouldBe` Aeson.String "1.5s"
            , it "JSON negative" $ do
                let msg = defaultDuration {durationSeconds = -1, durationNanos = -500000000}
                Aeson.toJSON msg `shouldBe` Aeson.String "-1.5s"
            ]
      , describe
          "Any"
          $ sequence_
            [ it "raw Any roundtrip" $ property $ do
                tu <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum
                v <- forAll $ Gen.bytes (Range.linear 0 200)
                let msg = defaultAny {anyTypeUrl = tu, anyValue = v}
                    encoded = encodeMessage msg
                decodeMessage encoded === Right msg
            , it "packAny Timestamp roundtrip" $ property $ do
                s <- forAll $ Gen.int64 (Range.linear 0 2000000000)
                n <- forAll $ Gen.int32 (Range.linear 0 999999999)
                let ts = defaultTimestamp {timestampSeconds = s, timestampNanos = n}
                    packed = packAny ts
                anyTypeUrl packed === "type.googleapis.com/google.protobuf.Timestamp"
                case unpackAny packed of
                  Just (Right decoded) -> decoded === ts
                  Just (Left err) -> do annotate (show err); failure
                  Nothing -> do annotate "type mismatch"; failure
            , it "packAny Duration roundtrip" $ property $ do
                s <- forAll $ Gen.int64 (Range.linear 0 1000000)
                n <- forAll $ Gen.int32 (Range.linear 0 999999999)
                let dur = defaultDuration {durationSeconds = s, durationNanos = n}
                    packed = packAny dur
                case unpackAny packed of
                  Just (Right decoded) -> decoded === dur
                  _ -> failure
            , it "packAny Empty" $ do
                let packed = packAny defaultEmpty
                anyTypeUrl packed `shouldBe` "type.googleapis.com/google.protobuf.Empty"
                case unpackAny packed :: Maybe (Either DecodeError Empty) of
                  Just (Right _) -> pure ()
                  other -> expectationFailure ("Expected Just (Right Empty), got " <> show other)
            , it "unpackAny type mismatch returns Nothing" $ do
                let packed = packAny (defaultTimestamp {timestampSeconds = 100})
                case unpackAny packed :: Maybe (Either DecodeError Duration) of
                  Nothing -> pure ()
                  Just _ -> expectationFailure "Should not match Duration"
            , it "isMessageType" $ do
                let packed = packAny (defaultDuration {durationSeconds = 60})
                isMessageType (Proxy :: Proxy Duration) packed `shouldBe` True
                isMessageType (Proxy :: Proxy Timestamp) packed `shouldBe` False
            , it "typeNameFromUrl strips prefix" $ do
                typeNameFromUrl "type.googleapis.com/google.protobuf.Timestamp"
                  `shouldBe` "google.protobuf.Timestamp"
                typeNameFromUrl "mycompany.com/types/example.Foo"
                  `shouldBe` "example.Foo"
                typeNameFromUrl "google.protobuf.Timestamp"
                  `shouldBe` "google.protobuf.Timestamp"
            , it "packAnyWithPrefix custom prefix" $ do
                let packed = packAnyWithPrefix "myhost/" (defaultTimestamp {timestampSeconds = 1})
                anyTypeUrl packed `shouldBe` "myhost/google.protobuf.Timestamp"
                case unpackAny packed of
                  Just (Right ts) -> ts `shouldBe` defaultTimestamp {timestampSeconds = 1}
                  _ -> expectationFailure "Should unpack with any prefix"
            , it "Any wire roundtrip preserves content" $ do
                let ts = defaultTimestamp {timestampSeconds = 1234567890, timestampNanos = 500000000}
                    packed = packAny ts
                    encodedAny = encodeMessage packed
                case decodeMessage encodedAny of
                  Left err -> expectationFailure (show err)
                  Right decodedAny -> case unpackAny decodedAny of
                    Just (Right (decoded :: Timestamp)) -> decoded `shouldBe` ts
                    _ -> expectationFailure "Should unpack after wire roundtrip"
            , it "TypeRegistry codec lookup" $ do
                let registry =
                      registerMessage (Proxy :: Proxy Timestamp)
                        . registerMessage (Proxy :: Proxy Duration)
                        . registerMessage (Proxy :: Proxy Empty)
                        $ emptyRegistry
                case lookupDecoder @Timestamp "google.protobuf.Timestamp" registry of
                  Just decode' -> case decode' (encodeMessage (defaultTimestamp {timestampSeconds = 42})) of
                    Right ts -> timestampSeconds ts `shouldBe` 42
                    Left e -> expectationFailure (show e)
                  Nothing -> expectationFailure "Should find Timestamp decoder"
            , it "TypeRegistry unknown type returns Nothing" $ do
                let registry = registerMessage (Proxy :: Proxy Timestamp) emptyRegistry
                case lookupCodec "unknown.Type" registry of
                  Nothing -> pure ()
                  Just _ -> expectationFailure "Should return Nothing for unknown type"
            ]
      , describe
          "Empty"
          $ sequence_
            [ it "empty roundtrip" $ do
                let encoded = encodeMessage defaultEmpty
                BS.length encoded `shouldBe` 0
                case decodeMessage encoded :: Either DecodeError Empty of
                  Right _ -> pure ()
                  Left e -> expectationFailure (show e)
            ]
      , describe
          "Wrappers"
          $ sequence_
            [ it "Int64Value roundtrip" $ property $ do
                v <- forAll $ Gen.int64 Range.linearBounded
                let msg = defaultInt64Value {int64ValueValue = v}
                decodeMessage (encodeMessage msg) === Right msg
            , it "UInt64Value roundtrip" $ property $ do
                v <- forAll $ Gen.word64 (Range.linear 0 maxBound)
                let msg = defaultUInt64Value {uInt64ValueValue = v}
                decodeMessage (encodeMessage msg) === Right msg
            , it "Int32Value roundtrip" $ property $ do
                v <- forAll $ Gen.int32 Range.linearBounded
                let msg = defaultInt32Value {int32ValueValue = v}
                decodeMessage (encodeMessage msg) === Right msg
            , it "BoolValue roundtrip" $ property $ do
                v <- forAll Gen.bool
                let msg = defaultBoolValue {boolValueValue = v}
                decodeMessage (encodeMessage msg) === Right msg
            , it "StringValue roundtrip" $ property $ do
                v <- forAll $ Gen.text (Range.linear 0 100) Gen.unicode
                let msg = defaultStringValue {stringValueValue = v}
                decodeMessage (encodeMessage msg) === Right msg
            , it "BytesValue roundtrip" $ property $ do
                v <- forAll $ Gen.bytes (Range.linear 0 200)
                let msg = defaultBytesValue {bytesValueValue = v}
                decodeMessage (encodeMessage msg) === Right msg
            , it "DoubleValue roundtrip" $ property $ do
                v <- forAll $ Gen.double (Range.linearFrac (-1e100) 1e100)
                let msg = defaultDoubleValue {doubleValueValue = v}
                decodeMessage (encodeMessage msg) === Right msg
            , it "FloatValue roundtrip" $ property $ do
                v <- forAll $ Gen.float (Range.linearFrac (-1e30) 1e30)
                let msg = defaultFloatValue {floatValueValue = v}
                decodeMessage (encodeMessage msg) === Right msg
            ]
      , describe
          "FieldMask"
          $ sequence_
            [ it "roundtrip" $ do
                let msg = defaultFieldMask {fieldMaskPaths = V.fromList ["foo.bar", "baz"]}
                    encoded = encodeMessage msg
                decodeMessage encoded `shouldBe` Right msg
            ]
      , describe
          "SourceContext"
          $ sequence_
            [ it "roundtrip" $ property $ do
                fn <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum
                let msg = defaultSourceContext {sourceContextFileName = fn}
                decodeMessage (encodeMessage msg) === Right msg
            ]
      , describe
          "Struct"
          $ sequence_
            [ it "empty struct roundtrip" $ do
                let msg = defaultStruct
                decodeMessage (encodeMessage msg) `shouldBe` Right msg
            , it "value null roundtrip" $ do
                let v = defaultValue {valueKind = Just (Value'Kind'NullValue NullValue'NullValue)}
                decodeMessage (encodeMessage v) `shouldBe` Right v
            , it "value number roundtrip" $ do
                let v = defaultValue {valueKind = Just (Value'Kind'NumberValue 3.14)}
                decodeMessage (encodeMessage v) `shouldBe` Right v
            , it "value string roundtrip" $ do
                let v = defaultValue {valueKind = Just (Value'Kind'StringValue "hello")}
                decodeMessage (encodeMessage v) `shouldBe` Right v
            , it "value bool roundtrip" $ do
                let v = defaultValue {valueKind = Just (Value'Kind'BoolValue True)}
                decodeMessage (encodeMessage v) `shouldBe` Right v
            , it "list value roundtrip" $ do
                let lv =
                      defaultListValue
                        { listValueValues =
                            V.fromList
                              [ defaultValue {valueKind = Just (Value'Kind'NumberValue 1)}
                              , defaultValue {valueKind = Just (Value'Kind'StringValue "two")}
                              , defaultValue {valueKind = Just (Value'Kind'BoolValue False)}
                              ]
                        }
                decodeMessage (encodeMessage lv) `shouldBe` Right lv
            ]
      , describe
          "Exact-size encoding"
          $ sequence_
            [ it "encodeMessageSized matches encodeMessage for Timestamp" $ property $ do
                s <- forAll $ Gen.int64 (Range.linear 0 1000000)
                n <- forAll $ Gen.int32 (Range.linear 0 999999)
                let msg = defaultTimestamp {timestampSeconds = s, timestampNanos = n}
                encodeMessageSized msg === encodeMessage msg
            , it "encodeMessageSized matches encodeMessage for Duration" $ property $ do
                s <- forAll $ Gen.int64 (Range.linear 0 1000000)
                n <- forAll $ Gen.int32 (Range.linear 0 999999)
                let msg = defaultDuration {durationSeconds = s, durationNanos = n}
                encodeMessageSized msg === encodeMessage msg
            ]
      , describe
          "Hashable instances"
          $ sequence_
            [ it "Timestamp: equal values have equal hashes" $ property $ do
                s <- forAll $ Gen.int64 (Range.linear 0 1000000)
                n <- forAll $ Gen.int32 (Range.linear 0 999999)
                let msg = defaultTimestamp {timestampSeconds = s, timestampNanos = n}
                hash msg === hash msg
            , it "Timestamp: different values have different hashes" $ do
                let m1 = defaultTimestamp {timestampSeconds = 100}
                    m2 = defaultTimestamp {timestampSeconds = 200}
                (hash m1 /= hash m2) `shouldBe` True
            , it "Duration: hashWithSalt works" $ do
                let msg = defaultDuration {durationSeconds = 42, durationNanos = 123}
                hashWithSalt 0 msg `seq` pure () :: IO ()
            , it "Empty: hashable" $ do
                hash defaultEmpty `seq` pure () :: IO ()
            , it "FieldMask: hashable with vector field" $ property $ do
                ps <- forAll $ Gen.list (Range.linear 0 5) (Gen.text (Range.linear 1 10) Gen.alphaNum)
                let msg = defaultFieldMask {fieldMaskPaths = V.fromList ps}
                hash msg `seq` pure ()
            , it "Struct: hashable with map field" $ property $ do
                hash defaultStruct `seq` pure ()
            ]
      ]