diff options
Diffstat (limited to 'util/json')
-rw-r--r-- | util/json/json_helpers.h | 209 | ||||
-rw-r--r-- | util/json/json_helpers_unittest.cc | 209 |
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 |