From 3afe77d7e243959f39568a58c8bff451eea87cbd Mon Sep 17 00:00:00 2001 From: Jordan Bayles Date: Fri, 5 Jun 2020 14:14:54 -0700 Subject: 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 Reviewed-by: mark a. foltz Reviewed-by: Ryan Keane --- util/json/json_helpers.h | 209 +++++++++++++++++++++++++++++++++++++ util/json/json_helpers_unittest.cc | 209 +++++++++++++++++++++++++++++++++++++ 2 files changed, 418 insertions(+) create mode 100644 util/json/json_helpers.h create mode 100644 util/json/json_helpers_unittest.cc (limited to 'util/json') 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 +#include +#include +#include +#include + +#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 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 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 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 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 +using Parser = std::function; + +// NOTE: array parsing methods reset the output vector to an empty vector in +// any error case. This is especially useful for optional arrays. +template +bool ParseAndValidateArray(const Json::Value& value, + Parser parser, + std::vector* 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* out) { + return ParseAndValidateArray(value, ParseAndValidateInt, out); +} + +inline bool ParseAndValidateUintArray(const Json::Value& value, + std::vector* out) { + return ParseAndValidateArray(value, ParseAndValidateUint, out); +} + +inline bool ParseAndValidateStringArray(const Json::Value& value, + std::vector* out) { + return ParseAndValidateArray(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 out; + EXPECT_TRUE(ParseAndValidateArray(valid_dummy_array, + ParseAndValidateDummy, &out)); + EXPECT_THAT(out, ElementsAre(Dummy{123}, Dummy{456})); + EXPECT_FALSE(ParseAndValidateArray(invalid_dummy_array, + ParseAndValidateDummy, &out)); + EXPECT_FALSE( + ParseAndValidateArray(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 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 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 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 -- cgit v1.2.3