wireform-proto-0.1.0.0: test-integration/Test/TextFormatParsed.hs
{-# LANGUAGE QuasiQuotes #-}
module Test.TextFormatParsed (textFormatParsedTests) where
import Data.Aeson qualified as Aeson
import Data.Proxy (Proxy (..))
import Proto.Schema (ProtoMessage (..))
import Data.Text qualified as T
import Data.Vector qualified as V
import Test.Tasty
import Test.Tasty.HUnit
import Proto (decodeMessage, MessageDecode (..))
import Proto (encodeMessage)
import Proto.Internal.Encode (MessageEncode (..))
import Proto.TextFormat (textToTyped, typedToTextPretty)
import Proto.TH.QQ (proto)
import Data.Reflection (Given (..))
import Proto.Internal.JSON.Extension (ExtensionRegistry, emptyExtensionRegistry)
-- Generated types for these tests.
[proto|
syntax = "proto3";
package test.textfmt;
enum Color {
COLOR_UNSPECIFIED = 0;
COLOR_RED = 1;
COLOR_GREEN = 2;
COLOR_BLUE = 3;
}
message Point {
int32 x = 1;
int32 y = 2;
string label = 3;
Color color = 4;
bytes data = 5;
repeated int32 values = 6;
}
message Outer {
string name = 1;
Point inner = 2;
}
|]
instance Given ExtensionRegistry where
given = emptyExtensionRegistry
textFormatParsedTests :: TestTree
textFormatParsedTests =
testGroup
"textToTyped"
[ testCase "empty message round-trips" $ do
let msg = defaultPoint
pbtxt = typedToTextPretty (Proxy @Point) msg
case pbtxt of
Nothing -> assertFailure "typedToTextPretty returned Nothing for empty message"
Just t -> case textToTyped (Proxy @Point) t of
Left e -> assertFailure ("textToTyped failed: " <> e)
Right p -> p @?= msg
, testCase "scalar fields round-trip" $ do
let msg = defaultPoint
{ pointX = 42
, pointY = -7
, pointLabel = "hello world"
}
roundTrip (Proxy @Point) msg
, testCase "enum field round-trips" $ do
let msg = defaultPoint { pointColor = Color'ColorRed }
roundTrip (Proxy @Point) msg
, testCase "all enum values round-trip" $
mapM_
(\c -> roundTrip (Proxy @Point) (defaultPoint { pointColor = c }))
[Color'ColorUnspecified, Color'ColorRed, Color'ColorGreen, Color'ColorBlue]
, testCase "nested message round-trips" $ do
let inner = defaultPoint { pointX = 10, pointY = 20, pointLabel = "inner" }
msg = defaultOuter { outerName = "outer", outerInner = Just inner }
roundTrip (Proxy @Outer) msg
, testCase "repeated field round-trips" $ do
let msg = defaultPoint { pointValues = V.fromList [1, 2, 3, 100, -5] }
roundTrip (Proxy @Point) msg
, testCase "parse known text format" $
-- Build text format manually without round-tripping through typedToTextPretty.
-- This tests that the parser itself works for user-provided pbtxt.
case textToTyped (Proxy @Point) "x: 99\ny: -3\nlabel: \"foo\"" of
Left e -> assertFailure ("textToTyped failed: " <> e)
Right p -> do
pointX p @?= 99
pointY p @?= -3
pointLabel p @?= "foo"
, testCase "parse nested message text format" $ do
let src = T.unlines
[ "name: \"test\""
, "inner {"
, " x: 5"
, " y: 10"
, "}"
]
case textToTyped (Proxy @Outer) src of
Left e -> assertFailure ("textToTyped failed: " <> e)
Right o -> do
outerName o @?= "test"
fmap pointX (outerInner o) @?= Just 5
fmap pointY (outerInner o) @?= Just 10
, testCase "wire-format ↔ text format ↔ wire-format consistency" $ do
-- textToTyped ∘ typedToTextPretty ∘ decodeMessage ∘ encodeMessage == id
let msg = defaultPoint { pointX = 7, pointY = -3, pointLabel = "wire", pointColor = Color'ColorBlue }
bytes = encodeMessage msg
case decodeMessage @Point bytes of
Left e -> assertFailure ("decode failed: " <> show e)
Right decoded -> roundTrip (Proxy @Point) decoded
, testCase "empty string returns default" $
case textToTyped (Proxy @Point) "" of
Left e -> assertFailure ("textToTyped empty failed: " <> e)
Right p -> p @?= defaultPoint
, testCase "unknown field names are ignored" $
-- Graceful: unknown names result in fields staying at default,
-- but the parse itself succeeds (fromJSON ignores unknown keys).
case textToTyped (Proxy @Point) "not_a_field: 42\nx: 5" of
Left _ -> pure () -- also acceptable to fail
Right p -> pointX p @?= 5
]
where
roundTrip :: (Given ExtensionRegistry, MessageEncode a, MessageDecode a, ProtoMessage a, Aeson.ToJSON a, Aeson.FromJSON a, Eq a, Show a) => Proxy a -> a -> IO ()
roundTrip p msg =
case typedToTextPretty p msg of
Nothing -> assertFailure "typedToTextPretty returned Nothing"
Just t ->
case textToTyped p t of
Left e -> assertFailure ("textToTyped failed: " <> e)
Right r -> r @?= msg