OSDN Git Service

Add libjsonpbverify.
authorYifan Hong <elsk@google.com>
Wed, 13 Feb 2019 22:29:33 +0000 (14:29 -0800)
committerYifan Hong <elsk@google.com>
Fri, 15 Feb 2019 00:17:33 +0000 (16:17 -0800)
- libjsonpbverify is a helper library that a gtest can link against
  when checking JSON files against their protobuf schema.

- Add unittests to ensure these validation functions are correct.
  (libjsonpbverify_test)

- Add tests to mitigate discrepancies of behavior between libjsoncpp
  and libprocessgroup. (libjsonpbverify_test)

- Add tests to impose additional restrictions on proto3 files when
  using them as JSON schema.

Test: libjsonpbverify_test

Bug: 123664216
Change-Id: I1e6257c9a25fd8d10fad103f3b74f7e1c6fad8a6

libjsonpb/README.md
libjsonpb/TEST_MAPPING [new file with mode: 0644]
libjsonpb/verify/Android.bp [new file with mode: 0644]
libjsonpb/verify/include/jsonpb/json_schema_test.h [new file with mode: 0644]
libjsonpb/verify/include/jsonpb/verify.h [new file with mode: 0644]
libjsonpb/verify/test.cpp [new file with mode: 0644]
libjsonpb/verify/test.proto [new file with mode: 0644]
libjsonpb/verify/verify.cpp [new file with mode: 0644]

index d8bf6e2..5562c8f 100644 (file)
@@ -44,3 +44,64 @@ logic of `libjsoncpp` and `libprotobuf` when parsing JSON files.
 Once `libprotobuf` in the source tree is updated to a higher version and
 `libjsonpbparse` is updated to ignore unknown fields in JSON files, all parsing
 code must be converted to use `libjsonpbparse` for consistency.
+
+# `libjsonpbverify`
+
+This library provides functions and tests to examine a JSON file and validate
+it against a Protobuf message definition.
+
+In addition to a sanity check that `libprotobuf` can convert the JSON file to a
+Protobuf message (using `libjsonpbparse`), it also checks the following:
+
+- Whether there are fields unknown to the schema. All fields in the JSON file
+  must be well defined in the schema.
+- Whether the Protobuf file defines JSON keys clearly. The JSON keys must be
+  the `json_name` option of a Protobuf field, or name of a Protobuf field if
+  `json_name` is not defined. `lowerCamelCase` supported by `libprotobuf` is
+  explicitly disallowed (unless explicitly used in `json_name`). For example,
+  in the following Protobuf file, only keys `foo_bar` and `barBaz` are allowed
+  in the JSON file:
+  ```
+  message Foo {
+      string foo_bar = 1;
+      string bar_baz = 2 [json_name = "barBaz"];
+  }
+  ```
+- Whether `json == convert_to_json(convert_to_pb(json))`, using `libprotobuf`.
+  This imposes additional restrictions including:
+  - Enum values must be present as names (not integer values) in the JSON file.
+  - 64-bit integers and special floating point values (infinity, NaN) must
+    always be strings.
+
+## Defining a JSON schema using Protobuf
+
+Check [JSON Mapping](https://developers.google.com/protocol-buffers/docs/proto3#json)
+before defining a Protobuf object as a JSON schema. In general:
+
+- **Use proto3**. `libjsonverify` does not support proto2.
+- JSON booleans should be `bool`.
+- JSON numbers should be `(s|fixed|u|)int32`, `float`, or `double` in the schema
+- JSON strings are generally `string`s, but if you want to impose more
+  restrictions on the string, you can also use `Timestamp`, `bytes`,
+  **`float`** or **`double`** (if NaN and infinity are valid values),
+  enumerations, etc.
+  - If a custom enumeration is used, parser code should **NOT** error when the
+    enumeration value name is unknown, as enumeration definitions may be
+    extended in the future.
+- JSON arrays should be repeated fields.
+- JSON objects should be a well-defined `message`, unless you have a good reason
+  to use `map<string, T>`.
+- Don't use `Any`; it defeats the purpose of having the schema.
+
+## Validating a JSON file against a Protobuf definition
+
+Example:
+```c++
+#include <jsonpb/verify.h>
+using namespace ::android::jsonpb;
+std::unique_ptr<JsonSchemaTestConfig> CreateCgroupsParam() {
+
+}
+INSTANTIATE_TEST_SUITE_P(LibProcessgroupProto, JsonSchemaTest,
+                         ::testing::Values(MakeTestParam<Cgroups>("cgroups.json")));
+```
diff --git a/libjsonpb/TEST_MAPPING b/libjsonpb/TEST_MAPPING
new file mode 100644 (file)
index 0000000..69e5a25
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "presubmit": [
+    {
+      "name": "libjsonpbverify_test",
+      "host": true
+    }
+  ]
+}
diff --git a/libjsonpb/verify/Android.bp b/libjsonpb/verify/Android.bp
new file mode 100644 (file)
index 0000000..b32b9b4
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This static library defines parameterized tests that enforce additional restrictions when
+// using Protobuf as schema for JSON files. The reason is that the JSON parser that
+// libprotobuf-cpp-full provides is relatively relaxed.
+cc_library_static {
+    name: "libjsonpbverify",
+    host_supported: true,
+    srcs: [
+        "verify.cpp",
+    ],
+    shared_libs: [
+        "libbase",
+        "libprotobuf-cpp-full",
+        "libjsoncpp",
+    ],
+    static_libs: [
+        "libjsonpbparse",
+    ],
+    export_static_lib_headers: [
+        "libjsonpbparse",
+    ],
+    export_include_dirs: [
+        "include",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+}
+
+cc_test_host {
+    name: "libjsonpbverify_test",
+    srcs: [
+        "test.cpp",
+        "test.proto",
+    ],
+    static_libs: [
+        "libbase",
+        "liblog",
+        "libgmock",
+        "libjsoncpp",
+        "libjsonpbparse",
+        "libjsonpbverify",
+    ],
+    shared_libs: [
+        "libprotobuf-cpp-full",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+}
diff --git a/libjsonpb/verify/include/jsonpb/json_schema_test.h b/libjsonpb/verify/include/jsonpb/json_schema_test.h
new file mode 100644 (file)
index 0000000..9a62ea9
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+#include <json/reader.h>
+#include <json/writer.h>
+#include <jsonpb/jsonpb.h>
+#include <jsonpb/verify.h>
+
+// JsonSchemaTest test that a given JSON file conforms to a given schema.
+// This includes:
+// - libprotobuf can parse the given JSON file using the given Prototype class
+// - Additional checks on field names of the JSON file, and types of values.
+
+namespace android {
+namespace jsonpb {
+
+class JsonSchemaTestConfig {
+ public:
+  virtual ~JsonSchemaTestConfig() = default;
+  virtual std::unique_ptr<google::protobuf::Message> CreateMessage() const = 0;
+  virtual std::string file_path() const = 0;
+  virtual std::string GetFileContent() const {
+    std::string content;
+    if (!android::base::ReadFileToString(file_path(), &content)) {
+      return "";
+    }
+    return content;
+  }
+};
+using JsonSchemaTestConfigFactory =
+    std::function<std::unique_ptr<JsonSchemaTestConfig>()>;
+
+template <typename T>
+class AbstractJsonSchemaTestConfig : public JsonSchemaTestConfig {
+ public:
+  AbstractJsonSchemaTestConfig(const std::string& path) : file_path_(path){};
+  std::unique_ptr<google::protobuf::Message> CreateMessage() const override {
+    return std::make_unique<T>();
+  }
+  std::string file_path() const override { return file_path_; }
+
+ private:
+  std::string file_path_;
+};
+
+template <typename T>
+JsonSchemaTestConfigFactory MakeTestParam(const std::string& path) {
+  return [path]() {
+    return std::make_unique<AbstractJsonSchemaTestConfig<T>>(path);
+  };
+}
+
+class JsonSchemaTest
+    : public ::testing::TestWithParam<JsonSchemaTestConfigFactory> {
+ public:
+  void SetUp() override {
+    auto&& config =
+        ::testing::TestWithParam<JsonSchemaTestConfigFactory>::GetParam()();
+    file_path_ = config->file_path();
+    json_ = config->GetFileContent();
+    ASSERT_FALSE(json_.empty()) << "Cannot read " << config->file_path();
+    object_ = config->CreateMessage();
+    auto res = internal::JsonStringToMessage(json_, object_.get());
+    ASSERT_TRUE(res.ok()) << "Invalid format of file " << config->file_path()
+                          << ": " << res.error();
+  }
+  google::protobuf::Message* message() const {
+    return object_.get();
+  }
+  std::string file_path_;
+  std::string json_;
+  std::unique_ptr<google::protobuf::Message> object_;
+};
+
+// Test that the JSON file has no fields unknown by the schema. See
+// AllFieldsAreKnown() for more details.
+TEST_P(JsonSchemaTest, NoUnknownFields) {
+  std::string error;
+  EXPECT_TRUE(AllFieldsAreKnown(*object_, json_, &error))
+      << "File: " << file_path_ << ": " << error;
+}
+
+TEST_P(JsonSchemaTest, EqReformattedJson) {
+  std::string error;
+  EXPECT_TRUE(EqReformattedJson(json_, object_.get(), &error))
+      << "File: " << file_path_ << ": " << error;
+}
+
+}  // namespace jsonpb
+}  // namespace android
diff --git a/libjsonpb/verify/include/jsonpb/verify.h b/libjsonpb/verify/include/jsonpb/verify.h
new file mode 100644 (file)
index 0000000..c05b13d
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#pragma once
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <google/protobuf/message.h>
+#include <json/reader.h>
+#include <json/value.h>
+#include <jsonpb/jsonpb.h>
+
+namespace android {
+namespace jsonpb {
+
+// Ensure that the JSON file has no unknown fields that is not defined in proto.
+// Because we want forwards compatibility, the parser of JSON files must ignore
+// unknown fields. This is achievable with libprotobuf version > 3.0-beta.
+// - <= 3.0-beta: we have to check unknown fields manually, and parser cannot
+// use libprotobuf
+//   to parse JSON files.
+// - < 3.5: libprotobuf discards all unknown fields. We can still check unknown
+// fields manually, but
+//   an easier way to check is `json == FormatJson(json)` (schematically)
+// - >= 3.5: Unknown fields are preserved, so FormatJson() may contain these
+// unknown fields. We can
+//   still check fields manually, or use reflection mechanism.
+//
+// For example, if a new field "foo" is added to cgroups.json but not to
+// cgroups.proto, libprocessgroup could technically read the value of "foo" by
+// using other libraries that parse JSON strings, effectively working around the
+// schema.
+//
+// This test also ensures that the parser does not use alternative key names.
+// For example, if the proto file states: message Foo { string foo_bar = 1;
+// string bar_baz = 2 [json_name = "BarBaz"]; } Then the parser accepts
+// "foo_bar" "fooBar", "bar_baz", "BarBaz" as valid key names. Here, we enforce
+// that the JSON file must use "foo_bar" and "BarBaz".
+//
+// Requiring this avoids surprises like:
+//     message Foo { string FooBar = 1; }
+//     { "fooBar" : "s" }
+// conforms with the schema, because libprotobuf accept "fooBar" as a valid key.
+// The correct schema should be:
+//     message Foo { string foo_bar = 1 [json_name="fooBar"]; }
+//
+// Params:
+//    path: path to navigate inside JSON tree. For example, {"foo", "bar"} for
+//    the value "string" in
+//          {"foo": {"bar" : "string"}}
+bool AllFieldsAreKnown(const google::protobuf::Message& message,
+                       const std::string& json, std::string* error);
+
+// Format the given JSON string according to Prototype T. This will serialize
+// the JSON string to a Prototype message, then re-print the message as JSON. By
+// reformatting the JSON string, we effectively enforces that the JSON source
+// file uses conventions of Protobuf's JSON writer; e.g. 64-bit integers /
+// special floating point numbers (inf, NaN, etc.) in strings, enum values in
+// names, etc.
+//
+// Params:
+//   scratch_space: The scratch space to use to store the Protobuf message. It
+//   must be a pointer
+//                  to the schema that the JSON string conforms to.
+bool EqReformattedJson(const std::string& json,
+                       google::protobuf::Message* scratch_space,
+                       std::string* error);
+
+namespace internal {
+// See EqReformattedJson().
+ErrorOr<std::string> FormatJson(const std::string& json,
+                                google::protobuf::Message* scratch_space);
+
+}  // namespace internal
+
+}  // namespace jsonpb
+}  // namespace android
diff --git a/libjsonpb/verify/test.cpp b/libjsonpb/verify/test.cpp
new file mode 100644 (file)
index 0000000..2ffc923
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#include <limits>
+
+#include <sstream>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <json/writer.h>
+#include <jsonpb/jsonpb.h>
+#include <jsonpb/verify.h>
+
+#include "test.pb.h"
+
+using ::android::jsonpb::internal::FormatJson;
+using ::testing::ElementsAre;
+using ::testing::HasSubstr;
+
+namespace android {
+namespace jsonpb {
+
+// Unit tests for libjsonpbverify.
+
+class LibJsonpbVerifyTest : public ::testing::Test {};
+
+class JsonKeyTest : public LibJsonpbVerifyTest {
+ public:
+  template <typename T>
+  std::string GetFieldJsonName(const std::string& field_name) {
+    return T{}.GetDescriptor()->FindFieldByName(field_name)->json_name();
+  }
+
+  template <typename T>
+  void TestParseOkWithUnknownKey(const std::string& field_name,
+                                 const std::string& json_key) {
+    std::string json = "{\"" + json_key + "\": \"test\"}";
+    auto object = JsonStringToMessage<T>(json);
+    ASSERT_TRUE(object.ok()) << object.error();
+    EXPECT_EQ(
+        "test",
+        object->GetReflection()->GetString(
+            *object, object->GetDescriptor()->FindFieldByName(field_name)));
+    std::string error;
+    ASSERT_FALSE(AllFieldsAreKnown(*object, json, &error))
+        << "AllFieldsAreKnown should return false";
+    EXPECT_THAT(error, HasSubstr("unknown keys"));
+    EXPECT_THAT(error, HasSubstr(json_key));
+  }
+};
+
+TEST_F(JsonKeyTest, WithJsonNameOk) {
+  std::string json =
+      "{\n"
+      "    \"FOOBAR\": \"foo_bar\",\n"
+      "    \"BarBaz\": \"barBaz\",\n"
+      "    \"baz_qux\": \"BazQux\",\n"
+      "    \"quxQuux\": \"QUX_QUUX\"\n"
+      "\n}";
+  auto object = JsonStringToMessage<WithJsonName>(json);
+  ASSERT_TRUE(object.ok()) << object.error();
+
+  EXPECT_EQ("foo_bar", object->foo_bar());
+  EXPECT_EQ("barBaz", object->barbaz());
+  EXPECT_EQ("BazQux", object->bazqux());
+  EXPECT_EQ("QUX_QUUX", object->qux_quux());
+
+  std::string error;
+  EXPECT_TRUE(AllFieldsAreKnown(*object, json, &error)) << error;
+}
+
+// If Prototype field name as keys while json_name is present, AllFieldsAreKnown
+// should return false.
+TEST_F(JsonKeyTest, WithJsonNameFooBar) {
+  TestParseOkWithUnknownKey<WithJsonName>("foo_bar", "foo_bar");
+}
+
+TEST_F(JsonKeyTest, WithJsonNameBarBaz) {
+  TestParseOkWithUnknownKey<WithJsonName>("barBaz", "barBaz");
+}
+
+TEST_F(JsonKeyTest, WithJsonNameBazQux) {
+  TestParseOkWithUnknownKey<WithJsonName>("BazQux", "BazQux");
+}
+
+TEST_F(JsonKeyTest, WithJsonNameQuxQuux) {
+  TestParseOkWithUnknownKey<WithJsonName>("QUX_QUUX", "QUX_QUUX");
+}
+
+// JSON field name matches Proto field name
+TEST_F(JsonKeyTest, NoJsonNameOk) {
+  std::string json =
+      "{\n"
+      "    \"foo_bar\": \"foo_bar\",\n"
+      "    \"barBaz\": \"barBaz\",\n"
+      "    \"BazQux\": \"BazQux\",\n"
+      "    \"QUX_QUUX\": \"QUX_QUUX\"\n"
+      "\n}";
+  auto object = JsonStringToMessage<NoJsonName>(json);
+  ASSERT_TRUE(object.ok()) << object.error();
+
+  EXPECT_EQ("foo_bar", object->foo_bar());
+  EXPECT_EQ("barBaz", object->barbaz());
+  EXPECT_EQ("BazQux", object->bazqux());
+  EXPECT_EQ("QUX_QUUX", object->qux_quux());
+
+  std::string error;
+  EXPECT_TRUE(AllFieldsAreKnown(*object, json, &error)) << error;
+}
+
+// JSON field name is lowerCamelCase of Proto field name;
+// AllFieldsAreKnown should return false. Although the lowerCamelCase name is a
+// valid key accepted by Protobuf's JSON parser, we explicitly disallow the
+// behavior.
+TEST_F(JsonKeyTest, NoJsonNameFooBar) {
+  EXPECT_EQ("fooBar", GetFieldJsonName<NoJsonName>("foo_bar"));
+  TestParseOkWithUnknownKey<NoJsonName>("foo_bar", "fooBar");
+}
+
+TEST_F(JsonKeyTest, NoJsonNameBarBaz) {
+  EXPECT_EQ("barBaz", GetFieldJsonName<NoJsonName>("barBaz"));
+  // No test for barBaz because its JSON name is the same as field_name
+}
+
+TEST_F(JsonKeyTest, NoJsonNameBazQux) {
+  EXPECT_EQ("bazQux", GetFieldJsonName<NoJsonName>("BazQux"));
+  TestParseOkWithUnknownKey<NoJsonName>("BazQux", "bazQux");
+}
+
+TEST_F(JsonKeyTest, NoJsonNameQuxQuux) {
+  EXPECT_EQ("qUXQUUX", GetFieldJsonName<NoJsonName>("QUX_QUUX"));
+  TestParseOkWithUnknownKey<NoJsonName>("QUX_QUUX", "qUXQUUX");
+}
+
+class EmbeddedJsonKeyTest : public LibJsonpbVerifyTest {
+ public:
+  ErrorOr<Parent> TestEmbeddedError(const std::string& json,
+                                    const std::string& unknown_key) {
+    auto object = JsonStringToMessage<Parent>(json);
+    if (!object.ok()) return object;
+    std::string error;
+    EXPECT_FALSE(AllFieldsAreKnown(*object, json, &error))
+        << "AllFieldsAreKnown should return false";
+    EXPECT_THAT(error, HasSubstr("unknown keys"));
+    EXPECT_THAT(error, HasSubstr(unknown_key));
+    return object;
+  }
+};
+
+TEST_F(EmbeddedJsonKeyTest, Ok) {
+  std::string json =
+      "{"
+      "    \"with_json_name\": {\"FOOBAR\": \"foo_bar\"},\n"
+      "    \"repeated_with_json_name\": [{\"BarBaz\": \"barBaz\"}],\n"
+      "    \"no_json_name\": {\"BazQux\": \"BazQux\"},\n"
+      "    \"repeated_no_json_name\": [{\"QUX_QUUX\": \"QUX_QUUX\"}]\n"
+      "}";
+  auto object = JsonStringToMessage<Parent>(json);
+  ASSERT_TRUE(object.ok()) << object.error();
+
+  EXPECT_EQ("foo_bar", object->with_json_name().foo_bar());
+  ASSERT_EQ(1u, object->repeated_with_json_name().size());
+  EXPECT_EQ("barBaz", object->repeated_with_json_name().begin()->barbaz());
+  EXPECT_EQ("BazQux", object->no_json_name().bazqux());
+  ASSERT_EQ(1u, object->repeated_no_json_name().size());
+  EXPECT_EQ("QUX_QUUX", object->repeated_no_json_name().begin()->qux_quux());
+
+  std::string error;
+  EXPECT_TRUE(AllFieldsAreKnown(*object, json, &error)) << error;
+}
+
+TEST_F(EmbeddedJsonKeyTest, FooBar) {
+  auto object = TestEmbeddedError(
+      "{\"with_json_name\": {\"foo_bar\": \"test\"}}", "foo_bar");
+  ASSERT_TRUE(object.ok()) << object.error();
+  EXPECT_EQ("test", object->with_json_name().foo_bar());
+}
+
+TEST_F(EmbeddedJsonKeyTest, BarBaz) {
+  auto object = TestEmbeddedError(
+      "{\"repeated_with_json_name\": [{\"barBaz\": \"test\"}]}", "barBaz");
+  ASSERT_TRUE(object.ok()) << object.error();
+  ASSERT_EQ(1u, object->repeated_with_json_name().size());
+  EXPECT_EQ("test", object->repeated_with_json_name().begin()->barbaz());
+}
+
+TEST_F(EmbeddedJsonKeyTest, BazQux) {
+  auto object =
+      TestEmbeddedError("{\"no_json_name\": {\"bazQux\": \"test\"}}", "bazQux");
+  ASSERT_TRUE(object.ok()) << object.error();
+  EXPECT_EQ("test", object->no_json_name().bazqux());
+}
+
+TEST_F(EmbeddedJsonKeyTest, QuxQuux) {
+  auto object = TestEmbeddedError(
+      "{\"repeated_no_json_name\": [{\"qUXQUUX\": \"test\"}]}", "qUXQUUX");
+  ASSERT_TRUE(object.ok()) << object.error();
+  ASSERT_EQ(1u, object->repeated_no_json_name().size());
+  EXPECT_EQ("test", object->repeated_no_json_name().begin()->qux_quux());
+}
+
+class ScalarTest : public LibJsonpbVerifyTest {
+ public:
+  ::testing::AssertionResult IsJsonEq(const std::string& l,
+                                      const std::string& r) {
+    Json::Reader reader;
+    Json::Value lvalue;
+    if (!reader.parse(l, lvalue))
+      return ::testing::AssertionFailure()
+             << reader.getFormattedErrorMessages();
+    Json::Value rvalue;
+    if (!reader.parse(r, rvalue))
+      return ::testing::AssertionFailure()
+             << reader.getFormattedErrorMessages();
+    Json::StyledWriter writer;
+    return lvalue == rvalue
+               ? (::testing::AssertionSuccess() << "Both are \n"
+                                                << writer.write(lvalue))
+               : (::testing::AssertionFailure()
+                  << writer.write(lvalue) << "\n does not equal \n"
+                  << writer.write(rvalue));
+  }
+
+  bool EqReformattedJson(const std::string& json, std::string* error) {
+    return android::jsonpb::EqReformattedJson(json, &scalar_, error);
+  }
+
+  Scalar scalar_;
+  std::string error_;
+};
+
+TEST_F(ScalarTest, Ok) {
+  std::string json =
+      "{\n"
+      "    \"i32\": 1,\n"
+      "    \"si32\": 1,\n"
+      "    \"i64\": \"1\",\n"
+      "    \"si64\": \"1\",\n"
+      "    \"f\": 1.5,\n"
+      "    \"d\": 1.5,\n"
+      "    \"e\": \"FOO\"\n"
+      "}";
+  auto formatted = FormatJson(json, &scalar_);
+  ASSERT_TRUE(formatted.ok()) << formatted.error();
+  EXPECT_TRUE(IsJsonEq(json, *formatted));
+
+  EXPECT_TRUE(EqReformattedJson(json, &error_)) << error_;
+}
+
+using ScalarTestErrorParam = std::tuple<const char*, const char*>;
+class ScalarTestError
+    : public ScalarTest,
+      public ::testing::WithParamInterface<ScalarTestErrorParam> {};
+
+TEST_P(ScalarTestError, Test) {
+  std::string json;
+  std::string message;
+  std::tie(json, message) = GetParam();
+  auto formatted = FormatJson(json, &scalar_);
+  ASSERT_TRUE(formatted.ok()) << formatted.error();
+  EXPECT_FALSE(IsJsonEq(json, *formatted)) << message;
+  EXPECT_FALSE(EqReformattedJson(json, &error_))
+      << "EqReformattedJson should return false";
+}
+
+static const std::vector<ScalarTestErrorParam> gScalarTestErrorParams = {
+    {"{\"i32\": \"1\"}", "Should not allow int32 values to be quoted"},
+    {"{\"si32\": \"1\"}", "Should not allow sint32 values to be quoted"},
+    {"{\"i64\": 1}", "Should require int64 values to be quoted"},
+    {"{\"si64\": 1}", "Should require sint64 values to be quoted"},
+    {"{\"f\": \"1.5\"}", "Should not allow float values to be quoted"},
+    {"{\"d\": \"1.5\"}", "Should not allow double values to be quoted"},
+    {"{\"e\": 1}", "Should not allow integers for enums"},
+};
+
+INSTANTIATE_TEST_SUITE_P(, ScalarTestError,
+                         ::testing::ValuesIn(gScalarTestErrorParams));
+
+int main(int argc, char** argv) {
+  using ::testing::AddGlobalTestEnvironment;
+  using ::testing::InitGoogleTest;
+
+  InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
+
+}  // namespace jsonpb
+}  // namespace android
diff --git a/libjsonpb/verify/test.proto b/libjsonpb/verify/test.proto
new file mode 100644 (file)
index 0000000..29ec8b1
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+package android.jsonpb;
+
+// Note: this file explicitly uses names that does NOT follow the Protobuf Style
+// Guide for testing purposes. When writing a .proto file as a JSON schema, you
+// should:
+// - Follow the Protobuf Style Guide for field names / enum value names
+// - If the JSON file is going to have field names that does not conform to the
+//   Protobuf Style Guide (a.k.a lower_snake_case), use json_name option to
+//   indicate an alternative name.
+// - If the JSON file is going to have enum value names that does not conform to
+//   the Protobuf Style Guide (a.k.a CAPITALIZED_SNAKE_CASE), use strings.
+
+message WithJsonName {
+  string foo_bar = 1 [json_name = "FOOBAR"];
+  string barBaz = 2 [json_name = "BarBaz"];
+  string BazQux = 3 [json_name = "baz_qux"];
+  string QUX_QUUX = 4 [json_name = "quxQuux"];
+}
+
+message NoJsonName {
+  string foo_bar = 1;
+  string barBaz = 2;
+  string BazQux = 3;
+  string QUX_QUUX = 4;
+}
+
+message Parent {
+  repeated WithJsonName repeated_with_json_name = 1;
+  WithJsonName with_json_name = 2;
+  repeated NoJsonName repeated_no_json_name = 3;
+  NoJsonName no_json_name = 4;
+}
+
+message Scalar {
+  int32 i32 = 1;
+  sint32 si32 = 2;
+  int64 i64 = 3;
+  sint64 si64 = 4;
+  float f = 5;
+  double d = 6;
+
+  enum Enum {
+    DEFAULT = 0;
+    FOO = 1;
+  }
+  Enum e = 7;
+}
diff --git a/libjsonpb/verify/verify.cpp b/libjsonpb/verify/verify.cpp
new file mode 100644 (file)
index 0000000..c411de8
--- /dev/null
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#include <jsonpb/verify.h>
+
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <string>
+
+#include <android-base/strings.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/message.h>
+#include <google/protobuf/reflection.h>
+#include <json/reader.h>
+#include <json/writer.h>
+#include <jsonpb/jsonpb.h>
+
+namespace android {
+namespace jsonpb {
+
+using google::protobuf::FieldDescriptor;
+using google::protobuf::FieldDescriptorProto;
+using google::protobuf::Message;
+
+// Return json_name of the field. If it is not set, return the name of the
+// field.
+const std::string& GetJsonName(const FieldDescriptor& field_descriptor) {
+  // The current version of libprotobuf does not define
+  // FieldDescriptor::has_json_name() yet. Use a workaround.
+  // TODO: use field_descriptor.has_json_name() when libprotobuf version is
+  // bumped.
+  FieldDescriptorProto proto;
+  field_descriptor.CopyTo(&proto);
+  return proto.has_json_name() ? field_descriptor.json_name()
+                               : field_descriptor.name();
+}
+
+bool AllFieldsAreKnown(const Message& message, const Json::Value& json,
+                       std::vector<std::string>* path,
+                       std::stringstream* error) {
+  if (!json.isObject()) {
+    *error << base::Join(*path, ".") << ": Not a JSON object\n";
+    return false;
+  }
+  auto&& descriptor = message.GetDescriptor();
+
+  auto json_members = json.getMemberNames();
+  std::set<std::string> json_keys{json_members.begin(), json_members.end()};
+
+  std::set<std::string> known_keys;
+  for (int i = 0; i < descriptor->field_count(); ++i) {
+    known_keys.insert(GetJsonName(*descriptor->field(i)));
+  }
+
+  std::set<std::string> unknown_keys;
+  std::set_difference(json_keys.begin(), json_keys.end(), known_keys.begin(),
+                      known_keys.end(),
+                      std::inserter(unknown_keys, unknown_keys.begin()));
+
+  if (!unknown_keys.empty()) {
+    *error << base::Join(*path, ".") << ": contains unknown keys: ["
+           << base::Join(unknown_keys, ", ")
+           << "]. Keys must be a known field name of "
+           << descriptor->full_name() << "(or its json_name option if set): ["
+           << base::Join(known_keys, ", ") << "]\n";
+    return false;
+  }
+
+  bool success = true;
+
+  // Check message fields.
+  auto&& reflection = message.GetReflection();
+  std::vector<const FieldDescriptor*> set_field_descriptors;
+  reflection->ListFields(message, &set_field_descriptors);
+  for (auto&& field_descriptor : set_field_descriptors) {
+    if (field_descriptor->cpp_type() !=
+        FieldDescriptor::CppType::CPPTYPE_MESSAGE) {
+      continue;
+    }
+    if (field_descriptor->is_map()) {
+      continue;
+    }
+
+    const std::string& json_name = GetJsonName(*field_descriptor);
+    const Json::Value& json_value = json[json_name];
+
+    if (field_descriptor->is_repeated()) {
+      auto&& fields =
+          reflection->GetRepeatedFieldRef<Message>(message, field_descriptor);
+
+      if (json_value.type() != Json::ValueType::arrayValue) {
+        *error << base::Join(*path, ".")
+               << ": not a JSON list. This should not happen.\n";
+        success = false;
+        continue;
+      }
+
+      if (json_value.size() != static_cast<size_t>(fields.size())) {
+        *error << base::Join(*path, ".") << ": JSON list has size "
+               << json_value.size() << " but message has size " << fields.size()
+               << ". This should not happen.\n";
+        success = false;
+        continue;
+      }
+
+      std::unique_ptr<Message> scratch_space(fields.NewMessage());
+      for (int i = 0; i < fields.size(); ++i) {
+        path->push_back(json_name + "[" + std::to_string(i) + "]");
+        auto res = AllFieldsAreKnown(fields.Get(i, scratch_space.get()),
+                                     json_value[i], path, error);
+        path->pop_back();
+        if (!res) {
+          success = false;
+        }
+      }
+    } else {
+      auto&& field = reflection->GetMessage(message, field_descriptor);
+      path->push_back(json_name);
+      auto res = AllFieldsAreKnown(field, json_value, path, error);
+      path->pop_back();
+      if (!res) {
+        success = false;
+      }
+    }
+  }
+  return success;
+}
+
+bool AllFieldsAreKnown(const google::protobuf::Message& message,
+                       const std::string& json, std::string* error) {
+  Json::Reader reader;
+  Json::Value value;
+  if (!reader.parse(json, value)) {
+    *error = reader.getFormattedErrorMessages();
+    return false;
+  }
+
+  std::stringstream errorss;
+  std::vector<std::string> json_tree_path{"<root>"};
+  if (!AllFieldsAreKnown(message, value, &json_tree_path, &errorss)) {
+    *error = errorss.str();
+    return false;
+  }
+  return true;
+}
+
+bool EqReformattedJson(const std::string& json,
+                       google::protobuf::Message* scratch_space,
+                       std::string* error) {
+  Json::Reader reader;
+  Json::Value old_json;
+  if (!reader.parse(json, old_json)) {
+    *error = reader.getFormattedErrorMessages();
+    return false;
+  }
+
+  auto new_json_string = internal::FormatJson(json, scratch_space);
+  if (!new_json_string.ok()) {
+    *error = new_json_string.error();
+    return false;
+  }
+  Json::Value new_json;
+  if (!reader.parse(*new_json_string, new_json)) {
+    *error = reader.getFormattedErrorMessages();
+    return false;
+  }
+
+  if (old_json != new_json) {
+    std::stringstream ss;
+    ss << "Formatted JSON tree does not match source. Possible reasons "
+          "include: \n"
+          "- JSON Integers (without quotes) are matched against 64-bit "
+          "integers in Prototype\n"
+          "  (Reformatted integers will now have quotes.) Quote these integers "
+          "in source\n"
+          "  JSON or use 32-bit integers instead.\n"
+          "- Enum values are stored as integers in source JSON file. Use enum "
+          "value name \n"
+          "  string instead, or change schema field to string / integers.\n"
+          "- JSON keys are re-formatted to be lowerCamelCase. To fix, define "
+          "json_name "
+          "option\n"
+          "  for appropriate fields.\n"
+          "\n"
+          "Reformatted JSON is printed below.\n"
+       << Json::StyledWriter().write(new_json);
+    *error = ss.str();
+    return false;
+  }
+  return true;
+}
+
+namespace internal {
+ErrorOr<std::string> FormatJson(const std::string& json,
+                                google::protobuf::Message* scratch_space) {
+  auto res = internal::JsonStringToMessage(json, scratch_space);
+  if (!res.ok()) {
+    return MakeError<std::string>(res.error());
+  }
+  return MessageToJsonString(*scratch_space);
+}
+}  // namespace internal
+
+}  // namespace jsonpb
+}  // namespace android