Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<MemberShape> 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);",
Expand All @@ -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()));
Expand All @@ -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<MemberShape> 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<MemberShape> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

4 changes: 4 additions & 0 deletions docs/generated-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ smithy::Outcome<OrderCoffeeInput> 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)

Expand Down
62 changes: 26 additions & 36 deletions protocol-tests/simplerestjson/generated/src/serde.cc
Original file line number Diff line number Diff line change
Expand Up @@ -856,62 +856,52 @@ smithy::Outcome<SmallStruct> 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<OpenDiscriminatedUnion> 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) {
smithy::DocumentMap map;
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<OpenTaggedUnion> 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) {
Expand Down
74 changes: 68 additions & 6 deletions protocol-tests/simplerestjson/generated/tests/request_tests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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("<missing>"), "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("<missing>"), "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("<missing>"), "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 = [] {
Expand Down
Loading
Loading