Commit c10a1b08 authored by Kenton Varda's avatar Kenton Varda

Implement annotations to control JSON parsing.

Features:
- Rename any field or enum value for JSON purposes.
- Flatten structs/groups into their parent object, possibly with a prefix.
- Assign a special discriminant field for unions, so that union members can be flattened without ambiguity.
parent 0fdad20a
......@@ -22,6 +22,7 @@
#include "json.h"
#include <capnp/test-util.h>
#include <capnp/compat/json.capnp.h>
#include <capnp/compat/json-test.capnp.h>
#include <kj/debug.h>
#include <kj/string.h>
#include <kj/test.h>
......@@ -822,6 +823,104 @@ KJ_TEST("register capability handler") {
json.addTypeHandler(handler);
}
static constexpr kj::StringPtr GOLDEN_ANNOTATED =
R"({ "names-can_contain!anything Really": "foo",
"flatFoo": 123,
"flatBar": "abc",
"renamed-flatBaz": {"hello": true},
"flatQux": "cba",
"pfx.foo": "this is a long string in order to force multi-line pretty printing",
"pfx.renamed-bar": 321,
"pfx.baz": {"hello": true},
"pfx.xfp.qux": "fed",
"union-type": "renamed-bar",
"barMember": 789,
"multiMember": "ghi",
"dependency": {"renamed-foo": "corge"},
"simpleGroup": {"renamed-grault": "garply"},
"enums": ["qux", "renamed-bar", "foo", "renamed-baz"] })"_kj;
static constexpr kj::StringPtr GOLDEN_ANNOTATED_REVERSE =
R"({
"enums": ["qux", "renamed-bar", "foo", "renamed-baz"],
"simpleGroup": { "renamed-grault": "garply" },
"dependency": { "renamed-foo": "corge" },
"multiMember": "ghi",
"barMember": 789,
"union-type": "renamed-bar",
"pfx.xfp.qux": "fed",
"pfx.baz": {"hello": true},
"pfx.renamed-bar": 321,
"pfx.foo": "this is a long string in order to force multi-line pretty printing",
"flatQux": "cba",
"renamed-flatBaz": {"hello": true},
"flatBar": "abc",
"flatFoo": 123,
"names-can_contain!anything Really": "foo"
})"_kj;
KJ_TEST("rename fields") {
JsonCodec json;
json.handleByAnnotation<TestJsonAnnotations>();
json.setPrettyPrint(true);
kj::String goldenText;
{
MallocMessageBuilder message;
auto root = message.getRoot<TestJsonAnnotations>();
root.setSomeField("foo");
auto aGroup = root.getAGroup();
aGroup.setFlatFoo(123);
aGroup.setFlatBar("abc");
aGroup.getFlatBaz().setHello(true);
aGroup.getDoubleFlat().setFlatQux("cba");
auto prefixedGroup = root.getPrefixedGroup();
prefixedGroup.setFoo("this is a long string in order to force multi-line pretty printing");
prefixedGroup.setBar(321);
prefixedGroup.getBaz().setHello(true);
prefixedGroup.getMorePrefix().setQux("fed");
auto unionBar = root.getAUnion().initBar();
unionBar.setBarMember(789);
unionBar.setMultiMember("ghi");
root.initDependency().setFoo("corge");
root.initSimpleGroup().setGrault("garply");
root.setEnums({
TestJsonAnnotatedEnum::QUX,
TestJsonAnnotatedEnum::BAR,
TestJsonAnnotatedEnum::FOO,
TestJsonAnnotatedEnum::BAZ
});
auto encoded = json.encode(root.asReader());
KJ_EXPECT(encoded == GOLDEN_ANNOTATED, encoded);
goldenText = kj::str(root);
}
{
MallocMessageBuilder message;
auto root = message.getRoot<TestJsonAnnotations>();
json.decode(GOLDEN_ANNOTATED, root);
KJ_EXPECT(kj::str(root) == goldenText, root, goldenText);
}
{
// Try parsing in reverse, mostly to test that union tags can come after content.
MallocMessageBuilder message;
auto root = message.getRoot<TestJsonAnnotations>();
json.decode(GOLDEN_ANNOTATED_REVERSE, root);
KJ_EXPECT(kj::str(root) == goldenText, root, goldenText);
}
}
} // namespace
} // namespace _ (private)
} // namespace capnp
# Copyright (c) 2018 Cloudflare, Inc. and contributors
# Licensed under the MIT License:
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
@0xc9d405cf4333e4c9;
using Json = import "json.capnp".JsonAnnotations;
using JsonValue = import "json.capnp".JsonValue;
$import "/capnp/c++.capnp".namespace("capnp");
struct TestJsonAnnotations {
someField @0 :Text $Json.name("names-can_contain!anything Really");
aGroup :group $Json.flatten() {
flatFoo @1 :UInt32;
flatBar @2 :Text;
flatBaz :group $Json.name("renamed-flatBaz") {
hello @3 :Bool;
}
doubleFlat :group $Json.flatten() {
flatQux @4 :Text;
}
}
prefixedGroup :group $Json.flatten(prefix = "pfx.") {
foo @5 :Text;
bar @6 :UInt32 $Json.name("renamed-bar");
baz :group {
hello @7 :Bool;
}
morePrefix :group $Json.flatten(prefix = "xfp.") {
qux @8 :Text;
}
}
aUnion :union $Json.flatten() $Json.discriminator("union-type") {
foo :group $Json.flatten() {
fooMember @9 :Text;
multiMember @10 :UInt32;
}
bar :group $Json.flatten() $Json.name("renamed-bar") {
barMember @11 :UInt32;
multiMember @12 :Text;
}
}
dependency @13 :TestJsonAnnotations2;
# To test that dependencies are loaded even if not flattened.
simpleGroup :group {
# To test that group types are loaded even if not flattened.
grault @14 :Text $Json.name("renamed-grault");
}
enums @15 :List(TestJsonAnnotatedEnum);
}
struct TestJsonAnnotations2 {
foo @0 :Text $Json.name("renamed-foo");
cycle @1 :TestJsonAnnotations;
}
enum TestJsonAnnotatedEnum {
foo @0;
bar @1 $Json.name("renamed-bar");
baz @2 $Json.name("renamed-baz");
qux @3;
}
# TODO(now): enums
This diff is collapsed.
......@@ -56,3 +56,37 @@ struct JsonValue {
params @1 :List(JsonValue);
}
}
struct JsonFlattenFlags {
prefix @0 :Text = "";
# Optional: Adds the given prefix to flattened field names.
}
struct JsonAnnotations {
# These are wrapped in this struct for namespacing purposes. Typical usage:
#
# using Json = import "/capnp/compat/json.capnp".JsonAnnotations;
#
# And then later on:
#
# myField @0 :Text $Json.name("my_field");
annotation name @0xfa5b1fd61c2e7c3d (field, enumerant, method, group, union): Text;
# Define an alternative name to use when encoding the given item in JSON. This can be used, for
# example, to use snake_case names where needed, even though Cap'n Proto uses strictly camelCase.
#
# (However, because JSON is derived from JavaScript, you *should* use camelCase names when
# defining JSON-based APIs. But, when supporting a pre-existing API you may not have a choice.)
annotation flatten @0x82d3e852af0336bf (field, group, union): JsonFlattenFlags;
# Specifies that an aggregate field should be flattened into its parent.
#
# In order to flatten a member of a union, the union (or, for an anonymous union, the parent
# struct type) must have the $jsonDiscribinator annotation.
annotation discriminator @0xcfa794e8d19a0162 (struct, union): Text;
# Specifies that a union's variant will be decided not by which fields are present, but instead
# by a special discriminator field with the given name. The value of the discriminator field is
# a string naming which variant is active. This allows the members of the union to have the
# $jsonFlatten annotation.
}
......@@ -198,6 +198,16 @@ public:
void addFieldHandler(StructSchema::Field field, Handler<T>& handler);
// Matches only the specific field. T can be a dynamic type. T must match the field's type.
void handleByAnnotation(Schema schema);
template <typename T> void handleByAnnotation();
// Inspects the given type (as specified by type parameter or dynamic schema) and all its
// dependencies looking for JSON annotations (see json.capnp), building and registering Handlers
// based on these annotations.
//
// If you'd like to use annotations to control JSON, you must call these functions before you
// start using the codec. They are not loaded "on demand" because that would require mutex
// locking.
// ---------------------------------------------------------------------------
// Hack to support string literal parameters
......@@ -214,6 +224,8 @@ public:
private:
class HandlerBase;
class AnnotatedHandler;
class AnnotatedEnumHandler;
struct Impl;
kj::Own<Impl> impl;
......@@ -224,6 +236,10 @@ private:
void decodeObject(JsonValue::Reader input, StructSchema type, Orphanage orphanage, DynamicStruct::Builder output) const;
void addTypeHandlerImpl(Type type, HandlerBase& handler);
void addFieldHandlerImpl(StructSchema::Field field, Type type, HandlerBase& handler);
AnnotatedHandler& loadAnnotatedHandler(StructSchema schema,
kj::Maybe<kj::StringPtr> discriminator,
kj::Vector<Schema>& dependencies);
};
// =======================================================================================
......@@ -480,4 +496,9 @@ template <> void JsonCodec::addTypeHandler(Handler<DynamicCapability>& handler)
// TODO(someday): Implement support for registering handlers that cover thinsg like "all structs"
// or "all lists". Currently you can only target a specific struct or list type.
template <typename T>
void JsonCodec::handleByAnnotation() {
return handleByAnnotation(Schema::from<T>());
}
} // namespace capnp
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