aboutsummaryrefslogtreecommitdiff
path: root/util/json
diff options
context:
space:
mode:
authorJordan Bayles <jophba@chromium.org>2020-06-05 14:14:54 -0700
committerCommit Bot <commit-bot@chromium.org>2020-06-05 23:27:56 +0000
commit3afe77d7e243959f39568a58c8bff451eea87cbd (patch)
treee6986fedf34a47d1e8946b7b18b16175311c007e /util/json
parent977bb0c2f35a96500ecbd09b236ab39b6ddc55fd (diff)
downloadopenscreen-3afe77d7e243959f39568a58c8bff451eea87cbd.tar.gz
Implement Answer parsing
This patch adds Answer parsing and testing, similar to how Offer messages are currently parsed. As part of this work, the following improvements are also included: 1. To avoid Abseil usage in public APIs, a new Optional type with unit tests is included. 2. message_util.h helpers have been greatly expanded, moved to util/json/parsing_helpers.h, and unit tests added. 3. SimpleFraction has been moved from util/ to platform/base/, so it can be properly used in public APIs. 4. SessionConfig has been cleaned up to follow coding style guidelines. 5. ANSWER message creation (that encapsulates the Answer struct) has been moved to the ReceiverSession. Bug: b/152633271, b/158030843 Change-Id: I59c20a140a5174d45378fb9b647ccbe5e6d23d1b Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/2219571 Commit-Queue: Jordan Bayles <jophba@chromium.org> Reviewed-by: mark a. foltz <mfoltz@chromium.org> Reviewed-by: Ryan Keane <rwkeane@google.com>
Diffstat (limited to 'util/json')
-rw-r--r--util/json/json_helpers.h209
-rw-r--r--util/json/json_helpers_unittest.cc209
2 files changed, 418 insertions, 0 deletions
diff --git a/util/json/json_helpers.h b/util/json/json_helpers.h
new file mode 100644
index 00000000..a4c43479
--- /dev/null
+++ b/util/json/json_helpers.h
@@ -0,0 +1,209 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UTIL_JSON_JSON_HELPERS_H_
+#define UTIL_JSON_JSON_HELPERS_H_
+
+#include <chrono>
+#include <functional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+#include "json/value.h"
+#include "platform/base/error.h"
+#include "util/chrono_helpers.h"
+#include "util/simple_fraction.h"
+
+// This file contains helper methods for parsing JSON, in an attempt to
+// reduce boilerplate code when working with JsonCpp.
+namespace openscreen {
+namespace json {
+
+// TODO(jophba): remove these methods after refactoring offer messaging.
+inline Error CreateParseError(const std::string& type) {
+ return Error(Error::Code::kJsonParseError, "Failed to parse " + type);
+}
+
+inline Error CreateParameterError(const std::string& type) {
+ return Error(Error::Code::kParameterInvalid, "Invalid parameter: " + type);
+}
+
+inline ErrorOr<bool> ParseBool(const Json::Value& parent,
+ const std::string& field) {
+ const Json::Value& value = parent[field];
+ if (!value.isBool()) {
+ return CreateParseError("bool field " + field);
+ }
+ return value.asBool();
+}
+
+inline ErrorOr<int> ParseInt(const Json::Value& parent,
+ const std::string& field) {
+ const Json::Value& value = parent[field];
+ if (!value.isInt()) {
+ return CreateParseError("integer field: " + field);
+ }
+ return value.asInt();
+}
+
+inline ErrorOr<uint32_t> ParseUint(const Json::Value& parent,
+ const std::string& field) {
+ const Json::Value& value = parent[field];
+ if (!value.isUInt()) {
+ return CreateParseError("unsigned integer field: " + field);
+ }
+ return value.asUInt();
+}
+
+inline ErrorOr<std::string> ParseString(const Json::Value& parent,
+ const std::string& field) {
+ const Json::Value& value = parent[field];
+ if (!value.isString()) {
+ return CreateParseError("string field: " + field);
+ }
+ return value.asString();
+}
+
+// TODO(jophba): offer messaging should use these methods instead.
+inline bool ParseBool(const Json::Value& value, bool* out) {
+ if (!value.isBool()) {
+ return false;
+ }
+ *out = value.asBool();
+ return true;
+}
+
+// A general note about parsing primitives. "Validation" in this context
+// generally means ensuring that the values are non-negative. There are
+// currently no cases in our usage of JSON strings where we accept negative
+// values. If this changes in the future, care must be taken to ensure
+// that we don't break anything in existing code.
+inline bool ParseAndValidateDouble(const Json::Value& value, double* out) {
+ if (!value.isDouble()) {
+ return false;
+ }
+ const double d = value.asDouble();
+ if (d < 0) {
+ return false;
+ }
+ *out = d;
+ return true;
+}
+
+inline bool ParseAndValidateInt(const Json::Value& value, int* out) {
+ if (!value.isInt()) {
+ return false;
+ }
+ int i = value.asInt();
+ if (i < 0) {
+ return false;
+ }
+ *out = i;
+ return true;
+}
+
+inline bool ParseAndValidateUint(const Json::Value& value, uint32_t* out) {
+ if (!value.isUInt()) {
+ return false;
+ }
+ *out = value.asUInt();
+ return true;
+}
+
+inline bool ParseAndValidateString(const Json::Value& value, std::string* out) {
+ if (!value.isString()) {
+ return false;
+ }
+ *out = value.asString();
+ return true;
+}
+
+// We want to be more robust when we parse fractions then just
+// allowing strings, this will parse numeral values such as
+// value: 50 as well as value: "50" and value: "100/2".
+inline bool ParseAndValidateSimpleFraction(const Json::Value& value,
+ SimpleFraction* out) {
+ if (value.isInt()) {
+ int parsed = value.asInt();
+ if (parsed < 0) {
+ return false;
+ }
+ *out = SimpleFraction{parsed, 1};
+ return true;
+ }
+
+ if (value.isString()) {
+ auto fraction_or_error = SimpleFraction::FromString(value.asString());
+ if (!fraction_or_error) {
+ return false;
+ }
+
+ if (!fraction_or_error.value().is_positive() ||
+ !fraction_or_error.value().is_defined()) {
+ return false;
+ }
+ *out = std::move(fraction_or_error.value());
+ return true;
+ }
+ return false;
+}
+
+inline bool ParseAndValidateMilliseconds(const Json::Value& value,
+ milliseconds* out) {
+ int out_ms;
+ if (!ParseAndValidateInt(value, &out_ms) || out_ms < 0) {
+ return false;
+ }
+ *out = milliseconds(out_ms);
+ return true;
+}
+
+template <typename T>
+using Parser = std::function<bool(const Json::Value&, T*)>;
+
+// NOTE: array parsing methods reset the output vector to an empty vector in
+// any error case. This is especially useful for optional arrays.
+template <typename T>
+bool ParseAndValidateArray(const Json::Value& value,
+ Parser<T> parser,
+ std::vector<T>* out) {
+ out->clear();
+ if (!value.isArray() || value.empty()) {
+ return false;
+ }
+
+ out->reserve(value.size());
+ for (Json::ArrayIndex i = 0; i < value.size(); ++i) {
+ T v;
+ if (!parser(value[i], &v)) {
+ out->clear();
+ return false;
+ }
+ out->push_back(v);
+ }
+
+ return true;
+}
+
+inline bool ParseAndValidateIntArray(const Json::Value& value,
+ std::vector<int>* out) {
+ return ParseAndValidateArray<int>(value, ParseAndValidateInt, out);
+}
+
+inline bool ParseAndValidateUintArray(const Json::Value& value,
+ std::vector<uint32_t>* out) {
+ return ParseAndValidateArray<uint32_t>(value, ParseAndValidateUint, out);
+}
+
+inline bool ParseAndValidateStringArray(const Json::Value& value,
+ std::vector<std::string>* out) {
+ return ParseAndValidateArray<std::string>(value, ParseAndValidateString, out);
+}
+
+} // namespace json
+} // namespace openscreen
+
+#endif // UTIL_JSON_JSON_HELPERS_H_
diff --git a/util/json/json_helpers_unittest.cc b/util/json/json_helpers_unittest.cc
new file mode 100644
index 00000000..fdac1897
--- /dev/null
+++ b/util/json/json_helpers_unittest.cc
@@ -0,0 +1,209 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "util/json/json_helpers.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "util/chrono_helpers.h"
+
+namespace openscreen {
+namespace json {
+namespace {
+
+using ::testing::ElementsAre;
+
+const Json::Value kNone;
+const Json::Value kEmptyString = "";
+const Json::Value kEmptyArray(Json::arrayValue);
+
+struct Dummy {
+ int value;
+
+ constexpr bool operator==(const Dummy& other) const {
+ return other.value == value;
+ }
+};
+
+bool ParseAndValidateDummy(const Json::Value& value, Dummy* out) {
+ int value_out;
+ if (!ParseAndValidateInt(value, &value_out)) {
+ return false;
+ }
+ *out = Dummy{value_out};
+ return true;
+}
+
+} // namespace
+
+TEST(ParsingHelpersTest, ParseAndValidateDouble) {
+ const Json::Value kValid = 13.37;
+ const Json::Value kNotDouble = "coffee beans";
+ const Json::Value kNegativeDouble = -4.2;
+ const Json::Value kZeroDouble = 0.0;
+
+ double out;
+ EXPECT_TRUE(ParseAndValidateDouble(kValid, &out));
+ EXPECT_DOUBLE_EQ(13.37, out);
+ EXPECT_TRUE(ParseAndValidateDouble(kZeroDouble, &out));
+ EXPECT_DOUBLE_EQ(0.0, out);
+ EXPECT_FALSE(ParseAndValidateDouble(kNotDouble, &out));
+ EXPECT_FALSE(ParseAndValidateDouble(kNegativeDouble, &out));
+ EXPECT_FALSE(ParseAndValidateDouble(kNone, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateInt) {
+ const Json::Value kValid = 1337;
+ const Json::Value kNotInt = "cold brew";
+ const Json::Value kNegativeInt = -42;
+ const Json::Value kZeroInt = 0;
+
+ int out;
+ EXPECT_TRUE(ParseAndValidateInt(kValid, &out));
+ EXPECT_EQ(1337, out);
+ EXPECT_TRUE(ParseAndValidateInt(kZeroInt, &out));
+ EXPECT_EQ(0, out);
+ EXPECT_FALSE(ParseAndValidateInt(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateInt(kNotInt, &out));
+ EXPECT_FALSE(ParseAndValidateInt(kNegativeInt, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateUint) {
+ const Json::Value kValid = 1337u;
+ const Json::Value kNotUint = "espresso";
+ const Json::Value kZeroUint = 0u;
+
+ uint32_t out;
+ EXPECT_TRUE(ParseAndValidateUint(kValid, &out));
+ EXPECT_EQ(1337u, out);
+ EXPECT_TRUE(ParseAndValidateUint(kZeroUint, &out));
+ EXPECT_EQ(0u, out);
+ EXPECT_FALSE(ParseAndValidateUint(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateUint(kNotUint, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateString) {
+ const Json::Value kValid = "macchiato";
+ const Json::Value kNotString = 42;
+
+ std::string out;
+ EXPECT_TRUE(ParseAndValidateString(kValid, &out));
+ EXPECT_EQ("macchiato", out);
+ EXPECT_TRUE(ParseAndValidateString(kEmptyString, &out));
+ EXPECT_EQ("", out);
+ EXPECT_FALSE(ParseAndValidateString(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateString(kNotString, &out));
+}
+
+// Simple fraction validity is tested extensively in its unit tests, so we
+// just check the major cases here.
+TEST(ParsingHelpersTest, ParseAndValidateSimpleFraction) {
+ const Json::Value kValid = "42/30";
+ const Json::Value kValidNumber = "42";
+ const Json::Value kUndefined = "5/0";
+ const Json::Value kNegative = "10/-2";
+ const Json::Value kInvalidNumber = "-1";
+ const Json::Value kNotSimpleFraction = "latte";
+
+ SimpleFraction out;
+ EXPECT_TRUE(ParseAndValidateSimpleFraction(kValid, &out));
+ EXPECT_EQ((SimpleFraction{42, 30}), out);
+ EXPECT_TRUE(ParseAndValidateSimpleFraction(kValidNumber, &out));
+ EXPECT_EQ((SimpleFraction{42, 1}), out);
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kUndefined, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kNegative, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kInvalidNumber, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kNotSimpleFraction, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kEmptyString, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateMilliseconds) {
+ const Json::Value kValid = 1000;
+ const Json::Value kValidFloat = 500.0;
+ const Json::Value kNegativeNumber = -120;
+ const Json::Value kZeroNumber = 0;
+ const Json::Value kNotNumber = "affogato";
+
+ milliseconds out;
+ EXPECT_TRUE(ParseAndValidateMilliseconds(kValid, &out));
+ EXPECT_EQ(milliseconds(1000), out);
+ EXPECT_TRUE(ParseAndValidateMilliseconds(kValidFloat, &out));
+ EXPECT_EQ(milliseconds(500), out);
+ EXPECT_TRUE(ParseAndValidateMilliseconds(kZeroNumber, &out));
+ EXPECT_EQ(milliseconds(0), out);
+ EXPECT_FALSE(ParseAndValidateMilliseconds(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateMilliseconds(kNegativeNumber, &out));
+ EXPECT_FALSE(ParseAndValidateMilliseconds(kNotNumber, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateArray) {
+ Json::Value valid_dummy_array;
+ valid_dummy_array[0] = 123;
+ valid_dummy_array[1] = 456;
+
+ Json::Value invalid_dummy_array;
+ invalid_dummy_array[0] = "iced coffee";
+ invalid_dummy_array[1] = 456;
+
+ std::vector<Dummy> out;
+ EXPECT_TRUE(ParseAndValidateArray<Dummy>(valid_dummy_array,
+ ParseAndValidateDummy, &out));
+ EXPECT_THAT(out, ElementsAre(Dummy{123}, Dummy{456}));
+ EXPECT_FALSE(ParseAndValidateArray<Dummy>(invalid_dummy_array,
+ ParseAndValidateDummy, &out));
+ EXPECT_FALSE(
+ ParseAndValidateArray<Dummy>(kEmptyArray, ParseAndValidateDummy, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateIntArray) {
+ Json::Value valid_int_array;
+ valid_int_array[0] = 123;
+ valid_int_array[1] = 456;
+
+ Json::Value invalid_int_array;
+ invalid_int_array[0] = "iced coffee";
+ invalid_int_array[1] = 456;
+
+ std::vector<int> out;
+ EXPECT_TRUE(ParseAndValidateIntArray(valid_int_array, &out));
+ EXPECT_THAT(out, ElementsAre(123, 456));
+ EXPECT_FALSE(ParseAndValidateIntArray(invalid_int_array, &out));
+ EXPECT_FALSE(ParseAndValidateIntArray(kEmptyArray, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateUintArray) {
+ Json::Value valid_uint_array;
+ valid_uint_array[0] = 123u;
+ valid_uint_array[1] = 456u;
+
+ Json::Value invalid_uint_array;
+ invalid_uint_array[0] = "breve";
+ invalid_uint_array[1] = 456u;
+
+ std::vector<uint32_t> out;
+ EXPECT_TRUE(ParseAndValidateUintArray(valid_uint_array, &out));
+ EXPECT_THAT(out, ElementsAre(123u, 456u));
+ EXPECT_FALSE(ParseAndValidateUintArray(invalid_uint_array, &out));
+ EXPECT_FALSE(ParseAndValidateUintArray(kEmptyArray, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateStringArray) {
+ Json::Value valid_string_array;
+ valid_string_array[0] = "nitro cold brew";
+ valid_string_array[1] = "doppio espresso";
+
+ Json::Value invalid_string_array;
+ invalid_string_array[0] = "mocha latte";
+ invalid_string_array[1] = 456;
+
+ std::vector<std::string> out;
+ EXPECT_TRUE(ParseAndValidateStringArray(valid_string_array, &out));
+ EXPECT_THAT(out, ElementsAre("nitro cold brew", "doppio espresso"));
+ EXPECT_FALSE(ParseAndValidateStringArray(invalid_string_array, &out));
+ EXPECT_FALSE(ParseAndValidateStringArray(kEmptyArray, &out));
+}
+
+} // namespace json
+} // namespace openscreen