From eedf08756178a826168b3c9390ba05ca7025beca Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 22:55:08 +0000 Subject: [PATCH] Support alloy open + discriminated unions in union serde Implement alloy's @discriminated and @jsonUnknown (open union) wire encodings in SerdeGenerator.writeUnion: - @discriminated("key") unions serialize the engaged member's fields inline with the discriminator spliced into the same object ({"key": "smol", ...fields}) instead of a single-key tagged wrapper, and deserialize by switching on the discriminator value. - A @jsonUnknown member retains the entire wire object when the tag or discriminator value matches no known member, for both tagged and discriminated unions; closed unions keep the strict exactly-one-member behavior byte-for-byte. Un-exclude the three PizzaAdminService conformance cases (OpenUnionsUnknownTaggedUnionCase, OpenUnionsKnownDiscriminatedUnionCase, OpenUnionsUnknownDiscriminatedUnionCase) and regenerate the simplerestjson suite: 12 new conformance tests (client+server, request+response) plus a smoke round-trip, all green. Closes #27 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01SyQAo21Pv6GYhHrkbQj8xQ --- .../io/smithycpp/codegen/SerdeGenerator.java | 122 +++++++++++++++++- .../codegen/protocol-test-exclusions.txt | 5 - docs/generated-types.md | 4 + .../simplerestjson/generated/src/serde.cc | 62 ++++----- .../generated/tests/request_tests.cc | 74 ++++++++++- .../generated/tests/response_tests.cc | 68 +++++++++- .../generated/tests/server_request_tests.cc | 80 +++++++++++- .../generated/tests/server_response_tests.cc | 83 +++++++++++- 8 files changed, 431 insertions(+), 67 deletions(-) diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/SerdeGenerator.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/SerdeGenerator.java index bfea8f7b..20786fef 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/SerdeGenerator.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/SerdeGenerator.java @@ -1,7 +1,10 @@ package io.smithycpp.codegen; +import alloy.DiscriminatedUnionTrait; +import alloy.JsonUnknownTrait; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.TopologicalIndex; @@ -203,13 +206,24 @@ private void writeStructure(CppWriter w, StructureShape shape) { } private void writeUnion(CppWriter w, UnionShape shape) { + if (shape.hasTrait(DiscriminatedUnionTrait.class)) { + writeDiscriminatedUnion(w, shape); + return; + } String suffix = SerdeCodeGen.serdeFunctionSuffix(context, shape); String type = valueType(shape); + // alloy's @jsonUnknown member (open unions) carries the entire wire object + // for unrecognized tags; it has no tag key of its own. + Optional unknown = jsonUnknownMember(shape); w.openBlock("smithy::Document Serialize$L(const $L& value) {", suffix, type); w.write("smithy::DocumentMap map;"); for (MemberShape member : shape.members()) { String name = context.cppSymbols().toMemberName(member); + if (isJsonUnknown(member)) { + w.write("if (value.is_$L()) return value.as_$L();", name, name); + continue; + } w.openBlock("if (value.is_$L()) {", name); w.write( "map.emplace($S, $L);", @@ -228,11 +242,19 @@ private void writeUnion(CppWriter w, UnionShape shape) { // Unions are exactly one member on the wire; extra known or unknown // members are malformed (the malformed-request suite pins this), but a // "__type" hint is ignored (clients must tolerate it in responses). - w.write( - "if (doc.as_map().size() - (doc.Find(\"__type\") != nullptr ? 1 : 0) != 1) " - + "return smithy::Error::Serialization($S);", - type + ": expected exactly one union member"); + // Open unions route anything else to the @jsonUnknown member instead. + if (unknown.isPresent()) { + w.openBlock("if (doc.as_map().size() - (doc.Find(\"__type\") != nullptr ? 1 : 0) == 1) {"); + } else { + w.write( + "if (doc.as_map().size() - (doc.Find(\"__type\") != nullptr ? 1 : 0) != 1) " + + "return smithy::Error::Serialization($S);", + type + ": expected exactly one union member"); + } for (MemberShape member : shape.members()) { + if (isJsonUnknown(member)) { + continue; + } String wireName = wireName(member); Symbol targetType = context.cppSymbols().toSymbol(context.model().expectShape(member.getTarget())); @@ -248,11 +270,101 @@ private void writeUnion(CppWriter w, UnionShape shape) { pascal(context.cppSymbols().toMemberName(member))); w.closeBlock("}"); } - w.write("return smithy::Error::Serialization($S);", type + ": unknown or missing union member"); + if (unknown.isPresent()) { + w.closeBlock("}"); + w.write( + "return $L::From$L(doc);", + type, + pascal(context.cppSymbols().toMemberName(unknown.get()))); + } else { + w.write( + "return smithy::Error::Serialization($S);", type + ": unknown or missing union member"); + } w.closeBlock("}"); w.write(""); } + /** + * alloy @discriminated unions: the wire form is the engaged member's own object with the + * discriminator field spliced in ({"key": "smol", ...member fields...}), not a single-key tagged + * wrapper. A @jsonUnknown member (open union) keeps the whole object — discriminator included — + * when the discriminator value matches no known member. + */ + private void writeDiscriminatedUnion(CppWriter w, UnionShape shape) { + String suffix = SerdeCodeGen.serdeFunctionSuffix(context, shape); + String type = valueType(shape); + String discriminator = shape.expectTrait(DiscriminatedUnionTrait.class).getValue(); + Optional unknown = jsonUnknownMember(shape); + + w.openBlock("smithy::Document Serialize$L(const $L& value) {", suffix, type); + for (MemberShape member : shape.members()) { + String name = context.cppSymbols().toMemberName(member); + if (isJsonUnknown(member)) { + w.write("if (value.is_$L()) return value.as_$L();", name, name); + continue; + } + w.openBlock("if (value.is_$L()) {", name); + w.write( + "smithy::Document member_doc = $L;", + serde.serializeExpression(member, "value.as_" + name + "()")); + w.write( + "member_doc.as_map().insert_or_assign($S, smithy::Document(std::string($S)));", + discriminator, + wireName(member)); + w.write("return member_doc;"); + w.closeBlock("}"); + } + w.write("return smithy::Document(smithy::DocumentMap{});"); + w.closeBlock("}"); + w.write(""); + + w.openBlock("smithy::Outcome<$L> Deserialize$L(const smithy::Document& doc) {", type, suffix); + w.write( + "if (!doc.is_map()) return smithy::Error::Serialization($S);", + type + ": expected a map on the wire"); + w.openBlock( + "if (const smithy::Document* discriminator = doc.Find($S);" + + " discriminator != nullptr && discriminator->is_string()) {", + discriminator); + for (MemberShape member : shape.members()) { + if (isJsonUnknown(member)) { + continue; + } + Symbol targetType = + context.cppSymbols().toSymbol(context.model().expectShape(member.getTarget())); + w.openBlock("if (discriminator->as_string() == $S) {", wireName(member)); + w.write("const smithy::Document* member = &doc;"); + w.write("$L parsed_member{};", targetType.getName()); + serde.writeDeserializeInto( + w, member, "member", "parsed_member", type + "." + wireName(member)); + w.write( + "return $L::From$L(std::move(parsed_member));", + type, + pascal(context.cppSymbols().toMemberName(member))); + w.closeBlock("}"); + } + w.closeBlock("}"); + if (unknown.isPresent()) { + w.write( + "return $L::From$L(doc);", + type, + pascal(context.cppSymbols().toMemberName(unknown.get()))); + } else { + w.write( + "return smithy::Error::Serialization($S);", type + ": unknown or missing union member"); + } + w.closeBlock("}"); + w.write(""); + } + + private static Optional jsonUnknownMember(UnionShape shape) { + return shape.members().stream().filter(SerdeGenerator::isJsonUnknown).findFirst(); + } + + private static boolean isJsonUnknown(MemberShape member) { + return member.hasTrait(JsonUnknownTrait.class); + } + private void writeList(CppWriter w, ListShape shape) { String suffix = SerdeCodeGen.serdeFunctionSuffix(context, shape); String type = valueType(shape); diff --git a/codegen/smithy-cpp-codegen/src/main/resources/io/smithycpp/codegen/protocol-test-exclusions.txt b/codegen/smithy-cpp-codegen/src/main/resources/io/smithycpp/codegen/protocol-test-exclusions.txt index 4daabfad..7d3f2506 100644 --- a/codegen/smithy-cpp-codegen/src/main/resources/io/smithycpp/codegen/protocol-test-exclusions.txt +++ b/codegen/smithy-cpp-codegen/src/main/resources/io/smithycpp/codegen/protocol-test-exclusions.txt @@ -45,11 +45,6 @@ smithy.protocoltests.rpcv2Cbor#RpcV2Protocol server-request RpcV2CborSupportsNaN # simpleRestJson (alloy) conformance exclusions. -# alloy @discriminated / @untagged / open-union encodings are not implemented. -alloy.test#PizzaAdminService any OpenUnionsUnknownTaggedUnionCase alloy open/discriminated unions are not implemented -alloy.test#PizzaAdminService any OpenUnionsKnownDiscriminatedUnionCase alloy open/discriminated unions are not implemented -alloy.test#PizzaAdminService any OpenUnionsUnknownDiscriminatedUnionCase alloy open/discriminated unions are not implemented - # @httpResponseCode with a 3xx value: the client treats only 2xx as success. alloy.test#PizzaAdminService response CustomCodeOutput 3xx @httpResponseCode is not treated as a success status diff --git a/docs/generated-types.md b/docs/generated-types.md index e0e41335..800075ee 100644 --- a/docs/generated-types.md +++ b/docs/generated-types.md @@ -90,6 +90,10 @@ smithy::Outcome DeserializeOrderCoffeeInput(const smithy::Docu `smithy::ErrorKind::kSerialization` error naming the member. - **Sparse** lists/maps serialize `std::nullopt` as explicit nulls; timestamps honor `@timestampFormat` with the protocol default applied where unspecified. +- **alloy unions**: `@discriminated("key")` unions put the engaged member's fields inline with + the discriminator spliced into the same object (`{"key": "smol", ...fields}`); a + `@jsonUnknown` member (open unions, tagged or discriminated) retains the entire wire object + when the tag or discriminator value matches no known member. ## Clients (Phase 3) diff --git a/protocol-tests/simplerestjson/generated/src/serde.cc b/protocol-tests/simplerestjson/generated/src/serde.cc index a784026d..60243c68 100644 --- a/protocol-tests/simplerestjson/generated/src/serde.cc +++ b/protocol-tests/simplerestjson/generated/src/serde.cc @@ -856,34 +856,30 @@ smithy::Outcome DeserializeSmallStruct(const smithy::Document& doc) } smithy::Document SerializeOpenDiscriminatedUnion(const OpenDiscriminatedUnion& value) { - smithy::DocumentMap map; if (value.is_smol()) { - map.emplace("smol", SerializeSmallStruct(value.as_smol())); - } - if (value.is_other()) { - map.emplace("other", value.as_other()); + smithy::Document member_doc = SerializeSmallStruct(value.as_smol()); + member_doc.as_map().insert_or_assign("key", smithy::Document(std::string("smol"))); + return member_doc; } - return smithy::Document(std::move(map)); + if (value.is_other()) return value.as_other(); + return smithy::Document(smithy::DocumentMap{}); } smithy::Outcome DeserializeOpenDiscriminatedUnion(const smithy::Document& doc) { if (!doc.is_map()) return smithy::Error::Serialization("OpenDiscriminatedUnion: expected a map on the wire"); - if (doc.as_map().size() - (doc.Find("__type") != nullptr ? 1 : 0) != 1) return smithy::Error::Serialization("OpenDiscriminatedUnion: expected exactly one union member"); - if (const smithy::Document* member = doc.Find("smol"); member != nullptr && !member->is_null()) { - SmallStruct parsed_member{}; - { - auto parsed = DeserializeSmallStruct(*member); - if (!parsed) return std::move(parsed).error(); - parsed_member = std::move(*parsed); + if (const smithy::Document* discriminator = doc.Find("key"); discriminator != nullptr && discriminator->is_string()) { + if (discriminator->as_string() == "smol") { + const smithy::Document* member = &doc; + SmallStruct parsed_member{}; + { + auto parsed = DeserializeSmallStruct(*member); + if (!parsed) return std::move(parsed).error(); + parsed_member = std::move(*parsed); + } + return OpenDiscriminatedUnion::FromSmol(std::move(parsed_member)); } - return OpenDiscriminatedUnion::FromSmol(std::move(parsed_member)); } - if (const smithy::Document* member = doc.Find("other"); member != nullptr && !member->is_null()) { - smithy::Document parsed_member{}; - parsed_member = *member; - return OpenDiscriminatedUnion::FromOther(std::move(parsed_member)); - } - return smithy::Error::Serialization("OpenDiscriminatedUnion: unknown or missing union member"); + return OpenDiscriminatedUnion::FromOther(doc); } smithy::Document SerializeOpenTaggedUnion(const OpenTaggedUnion& value) { @@ -891,27 +887,21 @@ smithy::Document SerializeOpenTaggedUnion(const OpenTaggedUnion& value) { if (value.is_str()) { map.emplace("str", smithy::Document(value.as_str())); } - if (value.is_other()) { - map.emplace("other", value.as_other()); - } + if (value.is_other()) return value.as_other(); return smithy::Document(std::move(map)); } smithy::Outcome DeserializeOpenTaggedUnion(const smithy::Document& doc) { if (!doc.is_map()) return smithy::Error::Serialization("OpenTaggedUnion: expected a map on the wire"); - if (doc.as_map().size() - (doc.Find("__type") != nullptr ? 1 : 0) != 1) return smithy::Error::Serialization("OpenTaggedUnion: expected exactly one union member"); - if (const smithy::Document* member = doc.Find("str"); member != nullptr && !member->is_null()) { - std::string parsed_member{}; - if (!member->is_string()) return smithy::Error::Serialization("OpenTaggedUnion.str: unexpected type on the wire"); - parsed_member = member->as_string(); - return OpenTaggedUnion::FromStr(std::move(parsed_member)); - } - if (const smithy::Document* member = doc.Find("other"); member != nullptr && !member->is_null()) { - smithy::Document parsed_member{}; - parsed_member = *member; - return OpenTaggedUnion::FromOther(std::move(parsed_member)); - } - return smithy::Error::Serialization("OpenTaggedUnion: unknown or missing union member"); + if (doc.as_map().size() - (doc.Find("__type") != nullptr ? 1 : 0) == 1) { + if (const smithy::Document* member = doc.Find("str"); member != nullptr && !member->is_null()) { + std::string parsed_member{}; + if (!member->is_string()) return smithy::Error::Serialization("OpenTaggedUnion.str: unexpected type on the wire"); + parsed_member = member->as_string(); + return OpenTaggedUnion::FromStr(std::move(parsed_member)); + } + } + return OpenTaggedUnion::FromOther(doc); } smithy::Document SerializeOpenUnionsPayload(const OpenUnionsPayload& value) { diff --git a/protocol-tests/simplerestjson/generated/tests/request_tests.cc b/protocol-tests/simplerestjson/generated/tests/request_tests.cc index 97c630e7..206dcd3b 100644 --- a/protocol-tests/simplerestjson/generated/tests/request_tests.cc +++ b/protocol-tests/simplerestjson/generated/tests/request_tests.cc @@ -14,12 +14,6 @@ namespace smithy::protocoltests::simplerestjson { // Generated from smithy.test#httpRequestTests (client cases). -// -// Excluded cases (protocol-test-exclusions.txt; the list must only shrink): -// OpenUnionsUnknownTaggedUnionCase (request) — alloy open/discriminated unions are not implemented -// OpenUnionsKnownDiscriminatedUnionCase (request) — alloy open/discriminated unions are not implemented -// OpenUnionsUnknownDiscriminatedUnionCase (request) — alloy open/discriminated unions are not implemented - namespace { struct Fixture { @@ -239,6 +233,74 @@ TEST(PizzaAdminServiceRequestTest, OpenUnionsKnownTaggedUnionCase) { EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\"tagged\": {\"str\": \"string value\"}}", request.body)); } +// Pass an unknown tagged union value in an open union +TEST(PizzaAdminServiceRequestTest, OpenUnionsUnknownTaggedUnionCase) { + Fixture fixture = MakeFixture(); + const OpenUnionsInput input = [] { + OpenUnionsInput v{}; + v.data = OpenUnionsPayload::FromTagged(OpenTaggedUnion::FromOther([] { + smithy::DocumentMap map; + map.emplace("whatisthis", [] { + smithy::DocumentMap map; + map.emplace("nested", smithy::Document(std::string("something different"))); + return smithy::Document(std::move(map)); +}()); + return smithy::Document(std::move(map)); +}())); + return v; +}(); + (void)fixture.client.OpenUnions(input); + const smithy::http::HttpRequest& request = fixture.transport->last_request; + EXPECT_EQ(request.method, "PUT"); + EXPECT_EQ(smithy::testing::UriPath(request.target), "/openUnions"); + EXPECT_EQ(request.headers.Get("Content-Type").value_or(""), "application/json"); + EXPECT_TRUE(request.headers.Has("Content-Length")); + EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\"tagged\": {\"whatisthis\": {\"nested\": \"something different\"}}}", request.body)); +} + +// Pass a known discriminated union value in an open union +TEST(PizzaAdminServiceRequestTest, OpenUnionsKnownDiscriminatedUnionCase) { + Fixture fixture = MakeFixture(); + const OpenUnionsInput input = [] { + OpenUnionsInput v{}; + v.data = OpenUnionsPayload::FromDiscriminated(OpenDiscriminatedUnion::FromSmol([] { + SmallStruct v{}; + v.content = "some string"; + return v; +}())); + return v; +}(); + (void)fixture.client.OpenUnions(input); + const smithy::http::HttpRequest& request = fixture.transport->last_request; + EXPECT_EQ(request.method, "PUT"); + EXPECT_EQ(smithy::testing::UriPath(request.target), "/openUnions"); + EXPECT_EQ(request.headers.Get("Content-Type").value_or(""), "application/json"); + EXPECT_TRUE(request.headers.Has("Content-Length")); + EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\"discriminated\": {\"key\": \"smol\", \"content\": \"some string\"}}", request.body)); +} + +// Pass an unknown discriminated union value in an open union +TEST(PizzaAdminServiceRequestTest, OpenUnionsUnknownDiscriminatedUnionCase) { + Fixture fixture = MakeFixture(); + const OpenUnionsInput input = [] { + OpenUnionsInput v{}; + v.data = OpenUnionsPayload::FromDiscriminated(OpenDiscriminatedUnion::FromOther([] { + smithy::DocumentMap map; + map.emplace("key", smithy::Document(std::string("mysterious_and_important"))); + map.emplace("extras", smithy::Document(std::int64_t{42})); + return smithy::Document(std::move(map)); +}())); + return v; +}(); + (void)fixture.client.OpenUnions(input); + const smithy::http::HttpRequest& request = fixture.transport->last_request; + EXPECT_EQ(request.method, "PUT"); + EXPECT_EQ(smithy::testing::UriPath(request.target), "/openUnions"); + EXPECT_EQ(request.headers.Get("Content-Type").value_or(""), "application/json"); + EXPECT_TRUE(request.headers.Has("Content-Length")); + EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\"discriminated\": {\"key\": \"mysterious_and_important\", \"extras\": 42}}", request.body)); +} + TEST(PizzaAdminServiceRequestTest, RoundTripRequest) { Fixture fixture = MakeFixture(); const RoundTripInput input = [] { diff --git a/protocol-tests/simplerestjson/generated/tests/response_tests.cc b/protocol-tests/simplerestjson/generated/tests/response_tests.cc index 269de4c0..0821698a 100644 --- a/protocol-tests/simplerestjson/generated/tests/response_tests.cc +++ b/protocol-tests/simplerestjson/generated/tests/response_tests.cc @@ -18,9 +18,6 @@ namespace smithy::protocoltests::simplerestjson { // // Excluded cases (protocol-test-exclusions.txt; the list must only shrink): // CustomCodeOutput (response) — 3xx @httpResponseCode is not treated as a success status -// OpenUnionsUnknownTaggedUnionCase (response) — alloy open/discriminated unions are not implemented -// OpenUnionsKnownDiscriminatedUnionCase (response) — alloy open/discriminated unions are not implemented -// OpenUnionsUnknownDiscriminatedUnionCase (response) — alloy open/discriminated unions are not implemented namespace { @@ -213,6 +210,71 @@ TEST(PizzaAdminServiceResponseTest, OpenUnionsKnownTaggedUnionCase) { EXPECT_EQ(*outcome, expected); } +// Return an unknown tagged union value in an open union +TEST(PizzaAdminServiceResponseTest, OpenUnionsUnknownTaggedUnionCase) { + Fixture fixture = MakeFixture(); + fixture.transport->next_response.status = 200; + fixture.transport->next_response.headers.Set("Content-Type", "application/json"); + fixture.transport->next_response.body = "{\"tagged\": {\"whatisthis\": {\"nested\": \"something different\"}}}"; + const auto outcome = fixture.client.OpenUnions(OpenUnionsInput{}); + ASSERT_TRUE(outcome.ok()) << outcome.error().message(); + const OpenUnionsOutput expected = [] { + OpenUnionsOutput v{}; + v.data = OpenUnionsPayload::FromTagged(OpenTaggedUnion::FromOther([] { + smithy::DocumentMap map; + map.emplace("whatisthis", [] { + smithy::DocumentMap map; + map.emplace("nested", smithy::Document(std::string("something different"))); + return smithy::Document(std::move(map)); +}()); + return smithy::Document(std::move(map)); +}())); + return v; +}(); + EXPECT_EQ(*outcome, expected); +} + +// Return a known discriminated union value in an open union +TEST(PizzaAdminServiceResponseTest, OpenUnionsKnownDiscriminatedUnionCase) { + Fixture fixture = MakeFixture(); + fixture.transport->next_response.status = 200; + fixture.transport->next_response.headers.Set("Content-Type", "application/json"); + fixture.transport->next_response.body = "{\"discriminated\": {\"key\": \"smol\", \"content\": \"some string\"}}"; + const auto outcome = fixture.client.OpenUnions(OpenUnionsInput{}); + ASSERT_TRUE(outcome.ok()) << outcome.error().message(); + const OpenUnionsOutput expected = [] { + OpenUnionsOutput v{}; + v.data = OpenUnionsPayload::FromDiscriminated(OpenDiscriminatedUnion::FromSmol([] { + SmallStruct v{}; + v.content = "some string"; + return v; +}())); + return v; +}(); + EXPECT_EQ(*outcome, expected); +} + +// Return an unknown discriminated union value in an open union +TEST(PizzaAdminServiceResponseTest, OpenUnionsUnknownDiscriminatedUnionCase) { + Fixture fixture = MakeFixture(); + fixture.transport->next_response.status = 200; + fixture.transport->next_response.headers.Set("Content-Type", "application/json"); + fixture.transport->next_response.body = "{\"discriminated\": {\"key\": \"mysterious_and_important\", \"extras\": 42}}"; + const auto outcome = fixture.client.OpenUnions(OpenUnionsInput{}); + ASSERT_TRUE(outcome.ok()) << outcome.error().message(); + const OpenUnionsOutput expected = [] { + OpenUnionsOutput v{}; + v.data = OpenUnionsPayload::FromDiscriminated(OpenDiscriminatedUnion::FromOther([] { + smithy::DocumentMap map; + map.emplace("key", smithy::Document(std::string("mysterious_and_important"))); + map.emplace("extras", smithy::Document(std::int64_t{42})); + return smithy::Document(std::move(map)); +}())); + return v; +}(); + EXPECT_EQ(*outcome, expected); +} + TEST(PizzaAdminServiceResponseTest, RoundTripDataResponse) { Fixture fixture = MakeFixture(); fixture.transport->next_response.status = 200; diff --git a/protocol-tests/simplerestjson/generated/tests/server_request_tests.cc b/protocol-tests/simplerestjson/generated/tests/server_request_tests.cc index ec4a6a5c..28689f38 100644 --- a/protocol-tests/simplerestjson/generated/tests/server_request_tests.cc +++ b/protocol-tests/simplerestjson/generated/tests/server_request_tests.cc @@ -17,12 +17,6 @@ namespace smithy::protocoltests::simplerestjson { // Generated from smithy.test#httpRequestTests (server cases): the wire // request is routed into the generated server and the parsed input is // compared against the expected params. -// -// Excluded cases (protocol-test-exclusions.txt; the list must only shrink): -// OpenUnionsUnknownTaggedUnionCase (server-request) — alloy open/discriminated unions are not implemented -// OpenUnionsKnownDiscriminatedUnionCase (server-request) — alloy open/discriminated unions are not implemented -// OpenUnionsUnknownDiscriminatedUnionCase (server-request) — alloy open/discriminated unions are not implemented - namespace { AddMenuItemOutput MinimalAddMenuItemOutput() { @@ -404,6 +398,80 @@ TEST(PizzaAdminServiceServerRequestTest, OpenUnionsKnownTaggedUnionCase) { EXPECT_EQ(*handler->lastOpenUnions, expected); } +// Pass an unknown tagged union value in an open union +TEST(PizzaAdminServiceServerRequestTest, OpenUnionsUnknownTaggedUnionCase) { + auto handler = std::make_shared(); + PizzaAdminServiceServer server(handler); + smithy::http::HttpRequest request; + request.method = "PUT"; + request.target = "/openUnions"; + request.headers.Set("Content-Type", "application/json"); + request.body = "{\"tagged\": {\"whatisthis\": {\"nested\": \"something different\"}}}"; + const smithy::http::HttpResponse response = server.Handler()(request); + ASSERT_TRUE(handler->lastOpenUnions.has_value()) << response.status << " " << response.body; + const OpenUnionsInput expected = [] { + OpenUnionsInput v{}; + v.data = OpenUnionsPayload::FromTagged(OpenTaggedUnion::FromOther([] { + smithy::DocumentMap map; + map.emplace("whatisthis", [] { + smithy::DocumentMap map; + map.emplace("nested", smithy::Document(std::string("something different"))); + return smithy::Document(std::move(map)); +}()); + return smithy::Document(std::move(map)); +}())); + return v; +}(); + EXPECT_EQ(*handler->lastOpenUnions, expected); +} + +// Pass a known discriminated union value in an open union +TEST(PizzaAdminServiceServerRequestTest, OpenUnionsKnownDiscriminatedUnionCase) { + auto handler = std::make_shared(); + PizzaAdminServiceServer server(handler); + smithy::http::HttpRequest request; + request.method = "PUT"; + request.target = "/openUnions"; + request.headers.Set("Content-Type", "application/json"); + request.body = "{\"discriminated\": {\"key\": \"smol\", \"content\": \"some string\"}}"; + const smithy::http::HttpResponse response = server.Handler()(request); + ASSERT_TRUE(handler->lastOpenUnions.has_value()) << response.status << " " << response.body; + const OpenUnionsInput expected = [] { + OpenUnionsInput v{}; + v.data = OpenUnionsPayload::FromDiscriminated(OpenDiscriminatedUnion::FromSmol([] { + SmallStruct v{}; + v.content = "some string"; + return v; +}())); + return v; +}(); + EXPECT_EQ(*handler->lastOpenUnions, expected); +} + +// Pass an unknown discriminated union value in an open union +TEST(PizzaAdminServiceServerRequestTest, OpenUnionsUnknownDiscriminatedUnionCase) { + auto handler = std::make_shared(); + PizzaAdminServiceServer server(handler); + smithy::http::HttpRequest request; + request.method = "PUT"; + request.target = "/openUnions"; + request.headers.Set("Content-Type", "application/json"); + request.body = "{\"discriminated\": {\"key\": \"mysterious_and_important\", \"extras\": 42}}"; + const smithy::http::HttpResponse response = server.Handler()(request); + ASSERT_TRUE(handler->lastOpenUnions.has_value()) << response.status << " " << response.body; + const OpenUnionsInput expected = [] { + OpenUnionsInput v{}; + v.data = OpenUnionsPayload::FromDiscriminated(OpenDiscriminatedUnion::FromOther([] { + smithy::DocumentMap map; + map.emplace("key", smithy::Document(std::string("mysterious_and_important"))); + map.emplace("extras", smithy::Document(std::int64_t{42})); + return smithy::Document(std::move(map)); +}())); + return v; +}(); + EXPECT_EQ(*handler->lastOpenUnions, expected); +} + TEST(PizzaAdminServiceServerRequestTest, RoundTripRequest) { auto handler = std::make_shared(); PizzaAdminServiceServer server(handler); diff --git a/protocol-tests/simplerestjson/generated/tests/server_response_tests.cc b/protocol-tests/simplerestjson/generated/tests/server_response_tests.cc index 1a2ccfb9..f8fbca2d 100644 --- a/protocol-tests/simplerestjson/generated/tests/server_response_tests.cc +++ b/protocol-tests/simplerestjson/generated/tests/server_response_tests.cc @@ -17,12 +17,6 @@ namespace smithy::protocoltests::simplerestjson { // Generated from smithy.test#httpResponseTests (server cases): a stub // handler returns the expected params and the wire response the server // produced is compared against the test definition. -// -// Excluded cases (protocol-test-exclusions.txt; the list must only shrink): -// OpenUnionsUnknownTaggedUnionCase (server-response) — alloy open/discriminated unions are not implemented -// OpenUnionsKnownDiscriminatedUnionCase (server-response) — alloy open/discriminated unions are not implemented -// OpenUnionsUnknownDiscriminatedUnionCase (server-response) — alloy open/discriminated unions are not implemented - namespace { AddMenuItemOutput MinimalAddMenuItemOutput() { @@ -571,6 +565,83 @@ TEST(PizzaAdminServiceServerResponseTest, OpenUnionsKnownTaggedUnionCase) { EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\"tagged\": {\"str\": \"string value\"}}", response.body)); } +// Return an unknown tagged union value in an open union +TEST(PizzaAdminServiceServerResponseTest, OpenUnionsUnknownTaggedUnionCase) { + class Handler final : public RecordingHandler { + public: + smithy::Outcome OpenUnions(const OpenUnionsInput& input) override { + (void)input; + return [] { + OpenUnionsOutput v{}; + v.data = OpenUnionsPayload::FromTagged(OpenTaggedUnion::FromOther([] { + smithy::DocumentMap map; + map.emplace("whatisthis", [] { + smithy::DocumentMap map; + map.emplace("nested", smithy::Document(std::string("something different"))); + return smithy::Document(std::move(map)); +}()); + return smithy::Document(std::move(map)); +}())); + return v; +}(); + } + }; + PizzaAdminServiceServer server(std::make_shared()); + const smithy::http::HttpResponse response = server.Handler()(MinimalRequestForOpenUnions()); + EXPECT_EQ(response.status, 200); + EXPECT_EQ(response.headers.Get("Content-Type").value_or(""), "application/json"); + EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\"tagged\": {\"whatisthis\": {\"nested\": \"something different\"}}}", response.body)); +} + +// Return a known discriminated union value in an open union +TEST(PizzaAdminServiceServerResponseTest, OpenUnionsKnownDiscriminatedUnionCase) { + class Handler final : public RecordingHandler { + public: + smithy::Outcome OpenUnions(const OpenUnionsInput& input) override { + (void)input; + return [] { + OpenUnionsOutput v{}; + v.data = OpenUnionsPayload::FromDiscriminated(OpenDiscriminatedUnion::FromSmol([] { + SmallStruct v{}; + v.content = "some string"; + return v; +}())); + return v; +}(); + } + }; + PizzaAdminServiceServer server(std::make_shared()); + const smithy::http::HttpResponse response = server.Handler()(MinimalRequestForOpenUnions()); + EXPECT_EQ(response.status, 200); + EXPECT_EQ(response.headers.Get("Content-Type").value_or(""), "application/json"); + EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\"discriminated\": {\"key\": \"smol\", \"content\": \"some string\"}}", response.body)); +} + +// Return an unknown discriminated union value in an open union +TEST(PizzaAdminServiceServerResponseTest, OpenUnionsUnknownDiscriminatedUnionCase) { + class Handler final : public RecordingHandler { + public: + smithy::Outcome OpenUnions(const OpenUnionsInput& input) override { + (void)input; + return [] { + OpenUnionsOutput v{}; + v.data = OpenUnionsPayload::FromDiscriminated(OpenDiscriminatedUnion::FromOther([] { + smithy::DocumentMap map; + map.emplace("key", smithy::Document(std::string("mysterious_and_important"))); + map.emplace("extras", smithy::Document(std::int64_t{42})); + return smithy::Document(std::move(map)); +}())); + return v; +}(); + } + }; + PizzaAdminServiceServer server(std::make_shared()); + const smithy::http::HttpResponse response = server.Handler()(MinimalRequestForOpenUnions()); + EXPECT_EQ(response.status, 200); + EXPECT_EQ(response.headers.Get("Content-Type").value_or(""), "application/json"); + EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\"discriminated\": {\"key\": \"mysterious_and_important\", \"extras\": 42}}", response.body)); +} + TEST(PizzaAdminServiceServerResponseTest, RoundTripDataResponse) { class Handler final : public RecordingHandler { public: