Commit 6b895a0c authored by Kenton Varda's avatar Kenton Varda

Corrently handle JSON discriminated unions when no value field is present.

In particular, it should not be necessary to specify a void union member's value explicitly. The discriminator should be sufficient. It should also be permitted to omit default values as long as the descriminator specifies the variant.
parent 4e75505a
...@@ -845,10 +845,12 @@ R"({ "names-can_contain!anything Really": "foo", ...@@ -845,10 +845,12 @@ R"({ "names-can_contain!anything Really": "foo",
"testHex": "706c756768", "testHex": "706c756768",
"bUnion": "renamed-bar", "bUnion": "renamed-bar",
"bValue": 678, "bValue": 678,
"externalUnion": {"type": "bar", "value": "cba"} })"_kj; "externalUnion": {"type": "bar", "value": "cba"},
"unionWithVoid": {"type": "voidValue"} })"_kj;
static constexpr kj::StringPtr GOLDEN_ANNOTATED_REVERSE = static constexpr kj::StringPtr GOLDEN_ANNOTATED_REVERSE =
R"({ R"({
"unionWithVoid": {"type": "voidValue"},
"externalUnion": {"type": "bar", "value": "cba"}, "externalUnion": {"type": "bar", "value": "cba"},
"bValue": 678, "bValue": 678,
"bUnion": "renamed-bar", "bUnion": "renamed-bar",
...@@ -944,6 +946,8 @@ KJ_TEST("rename fields") { ...@@ -944,6 +946,8 @@ KJ_TEST("rename fields") {
root.initExternalUnion().initBar().setValue("cba"); root.initExternalUnion().initBar().setValue("cba");
root.initUnionWithVoid().setVoidValue();
auto encoded = json.encode(root.asReader()); auto encoded = json.encode(root.asReader());
KJ_EXPECT(encoded == GOLDEN_ANNOTATED, encoded); KJ_EXPECT(encoded == GOLDEN_ANNOTATED, encoded);
......
...@@ -84,6 +84,12 @@ struct TestJsonAnnotations { ...@@ -84,6 +84,12 @@ struct TestJsonAnnotations {
} }
externalUnion @22 :TestJsonAnnotations3; externalUnion @22 :TestJsonAnnotations3;
unionWithVoid :union $Json.discriminator(name = "type") {
intValue @23 :UInt32;
voidValue @24 :Void;
textValue @25 :Text;
}
} }
struct TestJsonAnnotations2 { struct TestJsonAnnotations2 {
......
...@@ -1116,7 +1116,7 @@ public: ...@@ -1116,7 +1116,7 @@ public:
void decode(const JsonCodec& codec, JsonValue::Reader input, void decode(const JsonCodec& codec, JsonValue::Reader input,
DynamicStruct::Builder output) const override { DynamicStruct::Builder output) const override {
KJ_REQUIRE(input.isObject()); KJ_REQUIRE(input.isObject());
kj::HashMap<const void*, StructSchema::Field> unionsSeen; kj::HashSet<const void*> unionsSeen;
kj::Vector<JsonValue::Field::Reader> retries; kj::Vector<JsonValue::Field::Reader> retries;
for (auto field: input.getObject()) { for (auto field: input.getObject()) {
if (!decodeField(codec, field.getName(), field.getValue(), output, unionsSeen)) { if (!decodeField(codec, field.getName(), field.getValue(), output, unionsSeen)) {
...@@ -1244,15 +1244,19 @@ private: ...@@ -1244,15 +1244,19 @@ private:
KJ_IF_MAYBE(handler, info.flattenHandler) { KJ_IF_MAYBE(handler, info.flattenHandler) {
handler->gatherForEncode(codec, reader.get(*which), prefix, info.prefix, flattenedFields); handler->gatherForEncode(codec, reader.get(*which), prefix, info.prefix, flattenedFields);
} else { } else {
flattenedFields.add(FlattenedField { auto type = which->getType();
prefix, info.name, which->getType(), reader.get(*which) }); if (type.which() == schema::Type::VOID && unionTagName != nullptr) {
// When we have an explicit union discriminant, we don't need to encode void fields.
} else {
flattenedFields.add(FlattenedField {
prefix, info.name, which->getType(), reader.get(*which) });
}
} }
} }
} }
bool decodeField(const JsonCodec& codec, kj::StringPtr name, JsonValue::Reader value, bool decodeField(const JsonCodec& codec, kj::StringPtr name, JsonValue::Reader value,
DynamicStruct::Builder output, DynamicStruct::Builder output, kj::HashSet<const void*>& unionsSeen) const {
kj::HashMap<const void*, StructSchema::Field>& unionsSeen) const {
KJ_ASSERT(output.getSchema() == schema); KJ_ASSERT(output.getSchema() == schema);
KJ_IF_MAYBE(info, fieldsByName.find(name)) { KJ_IF_MAYBE(info, fieldsByName.find(name)) {
...@@ -1273,20 +1277,20 @@ private: ...@@ -1273,20 +1277,20 @@ private:
// Mark that we've seen a union tag for this struct. // Mark that we've seen a union tag for this struct.
const void* ptr = getUnionInstanceIdentifier(output); const void* ptr = getUnionInstanceIdentifier(output);
KJ_IF_MAYBE(field, unionTagValues.find(value.getString())) { KJ_IF_MAYBE(field, unionTagValues.find(value.getString())) {
unionsSeen.insert(ptr, *field); // clear() has the side-effect of activating this member of the union, without
// allocating any objects.
output.clear(*field);
unionsSeen.insert(ptr);
} }
return true; return true;
} }
case FieldNameInfo::FLATTENED_FROM_UNION: { case FieldNameInfo::FLATTENED_FROM_UNION: {
const void* ptr = getUnionInstanceIdentifier(output); const void* ptr = getUnionInstanceIdentifier(output);
KJ_IF_MAYBE(variant, unionsSeen.find(ptr)) { if (unionsSeen.contains(ptr)) {
bool alreadyInitialized = output.which() auto variant = KJ_ASSERT_NONNULL(output.which());
.map([&](auto f) { return f == *variant; }) return KJ_ASSERT_NONNULL(fields[variant.getIndex()].flattenHandler)
.orDefault(false);
auto child = alreadyInitialized ? output.get(*variant) : output.init(*variant);
return KJ_ASSERT_NONNULL(fields[variant->getIndex()].flattenHandler)
.decodeField(codec, name.slice(info->prefixLength), value, .decodeField(codec, name.slice(info->prefixLength), value,
child.as<DynamicStruct>(), unionsSeen); output.get(variant).as<DynamicStruct>(), unionsSeen);
} else { } else {
// We haven't seen the union tag yet, so we can't parse this field yet. Try again later. // We haven't seen the union tag yet, so we can't parse this field yet. Try again later.
return false; return false;
...@@ -1294,8 +1298,9 @@ private: ...@@ -1294,8 +1298,9 @@ private:
} }
case FieldNameInfo::UNION_VALUE: { case FieldNameInfo::UNION_VALUE: {
const void* ptr = getUnionInstanceIdentifier(output); const void* ptr = getUnionInstanceIdentifier(output);
KJ_IF_MAYBE(variant, unionsSeen.find(ptr)) { if (unionsSeen.contains(ptr)) {
codec.decodeField(*variant, value, Orphanage::getForMessageContaining(output), output); auto variant = KJ_ASSERT_NONNULL(output.which());
codec.decodeField(variant, value, Orphanage::getForMessageContaining(output), output);
return true; return true;
} else { } else {
// We haven't seen the union tag yet, so we can't parse this field yet. Try again later. // We haven't seen the union tag yet, so we can't parse this field yet. Try again later.
......
...@@ -18,4 +18,5 @@ ...@@ -18,4 +18,5 @@
"testHex": "706c756768", "testHex": "706c756768",
"bUnion": "renamed-bar", "bUnion": "renamed-bar",
"bValue": 678, "bValue": 678,
"externalUnion": {"type": "bar", "value": "cba"} } "externalUnion": {"type": "bar", "value": "cba"},
"unionWithVoid": {"type": "voidValue"} }
...@@ -262,6 +262,11 @@ class HashSet: public Table<Element, HashIndex<_::HashSetCallbacks>> { ...@@ -262,6 +262,11 @@ class HashSet: public Table<Element, HashIndex<_::HashSetCallbacks>> {
public: public:
// Everything is inherited. // Everything is inherited.
template <typename... Params>
inline bool contains(Params&&... params) const {
return this->find(kj::fwd<Params>(params)...) != nullptr;
}
}; };
template <typename Element> template <typename Element>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment