Read the [JsonUtils Spec] for more details. This pull request introduces the next version of JsonUtils. It is in a separate file for ease of review and testing. JsonUtilsNew will be renamed in a subsequent commit that rewrites our JSON deserializers. ### Implementer's Notes I went with telescoping exceptions for the key parsing code, because it's totally possible that you can be five keys deep and encounter a type error. This lets us encode information about all failures in the chain instead of just the topmost one. The original JsonUtilsNew code changed to use `decay` everywhere because the tests wouldn't compile. We want to treat `GetValue<const guid>` _the same as_ `GetValue<guid>`, and this lets us do so. `decay` is awesome. I've been developing this with a shim that redirects `JsonUtils.h` to `JsonUtilsNew.h`. I am not comfortable deleting the original until we've moved off of it, and that _will_ be the subject of a followup PR. ## Validation Steps Performed So many tests. [JsonUtils Spec]: https://github.com/microsoft/terminal/blob/master/doc/cascadia/Json-Utility-API.md Refs #2550
This commit is contained in:
parent
6485a2b440
commit
10bc1a6532
|
@ -2255,6 +2255,7 @@ targetnametoken
|
|||
targetver
|
||||
taskbar
|
||||
tbar
|
||||
TBase
|
||||
tbc
|
||||
tbi
|
||||
Tbl
|
||||
|
@ -2297,6 +2298,7 @@ testtestabc
|
|||
testtesttesttesttest
|
||||
TEXCOORD
|
||||
texel
|
||||
TExpected
|
||||
textattribute
|
||||
TEXTATTRIBUTEID
|
||||
Textbox
|
||||
|
@ -2321,6 +2323,7 @@ tilunittests
|
|||
Timeline
|
||||
titlebar
|
||||
TITLEISLINKNAME
|
||||
TJson
|
||||
tl
|
||||
TLEN
|
||||
Tlg
|
||||
|
@ -2343,6 +2346,7 @@ tooltip
|
|||
TOPDOWNDIB
|
||||
TOPLEFT
|
||||
TOPRIGHT
|
||||
TOpt
|
||||
tosign
|
||||
touchpad
|
||||
towlower
|
||||
|
@ -2385,6 +2389,7 @@ Txtev
|
|||
typechecked
|
||||
typechecking
|
||||
typedef
|
||||
typeid
|
||||
typeinfo
|
||||
typelib
|
||||
typename
|
||||
|
|
471
src/cascadia/TerminalApp/JsonUtilsNew.h
Normal file
471
src/cascadia/TerminalApp/JsonUtilsNew.h
Normal file
|
@ -0,0 +1,471 @@
|
|||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- JsonUtils.h
|
||||
|
||||
Abstract:
|
||||
- Helpers for the TerminalApp project
|
||||
Author(s):
|
||||
- Mike Griese - August 2019
|
||||
- Dustin Howett - January 2020
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <json.h>
|
||||
|
||||
#include "../types/inc/utils.hpp"
|
||||
|
||||
namespace winrt
|
||||
{
|
||||
// If we don't use winrt, nobody will include the ConversionTrait for winrt::guid.
|
||||
// If nobody includes it, this forward declaration will suffice.
|
||||
struct guid;
|
||||
}
|
||||
|
||||
namespace TerminalApp::JsonUtils
|
||||
{
|
||||
namespace Detail
|
||||
{
|
||||
// Function Description:
|
||||
// - Returns a string_view to a Json::Value's internal string storage,
|
||||
// hopefully without copying it.
|
||||
__declspec(noinline) inline const std::string_view GetStringView(const Json::Value& json)
|
||||
{
|
||||
const char* begin{ nullptr };
|
||||
const char* end{ nullptr };
|
||||
json.getString(&begin, &end);
|
||||
const std::string_view zeroCopyString{ begin, gsl::narrow_cast<size_t>(end - begin) };
|
||||
return zeroCopyString;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
struct DeduceOptional
|
||||
{
|
||||
using Type = typename std::decay<T>::type;
|
||||
};
|
||||
|
||||
template<typename TOpt>
|
||||
struct DeduceOptional<std::optional<TOpt>>
|
||||
{
|
||||
using Type = typename std::decay<TOpt>::type;
|
||||
};
|
||||
}
|
||||
|
||||
// These exceptions cannot use localized messages, as we do not have
|
||||
// guaranteed access to the resource loader.
|
||||
class TypeMismatchException : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
TypeMismatchException() :
|
||||
runtime_error("unexpected data type") {}
|
||||
};
|
||||
|
||||
class KeyedException : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
KeyedException(const std::string_view key, std::exception_ptr exception) :
|
||||
runtime_error(fmt::format("error parsing \"{0}\"", key).c_str()),
|
||||
_key{ key },
|
||||
_innerException{ std::move(exception) } {}
|
||||
|
||||
std::string GetKey() const
|
||||
{
|
||||
return _key;
|
||||
}
|
||||
|
||||
[[noreturn]] void RethrowInner() const
|
||||
{
|
||||
std::rethrow_exception(_innerException);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _key;
|
||||
std::exception_ptr _innerException;
|
||||
};
|
||||
|
||||
class UnexpectedValueException : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
UnexpectedValueException(const std::string_view value) :
|
||||
runtime_error(fmt::format("unexpected value \"{0}\"", value).c_str()),
|
||||
_value{ value } {}
|
||||
|
||||
std::string GetValue() const
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _value;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct ConversionTrait
|
||||
{
|
||||
// FromJson, CanConvert are not defined so as to cause a compile error (which forces a specialization)
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<std::string>
|
||||
{
|
||||
std::string FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asString();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isString();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<std::wstring>
|
||||
{
|
||||
std::wstring FromJson(const Json::Value& json)
|
||||
{
|
||||
return til::u8u16(Detail::GetStringView(json));
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isString();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<bool>
|
||||
{
|
||||
bool FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asBool();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isBool();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<int>
|
||||
{
|
||||
int FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asInt();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isInt();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<unsigned int>
|
||||
{
|
||||
unsigned int FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asUInt();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isUInt();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<float>
|
||||
{
|
||||
float FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asFloat();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isNumeric();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<double>
|
||||
{
|
||||
double FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asDouble();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isNumeric();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<GUID>
|
||||
{
|
||||
GUID FromJson(const Json::Value& json)
|
||||
{
|
||||
return ::Microsoft::Console::Utils::GuidFromString(til::u8u16(Detail::GetStringView(json)));
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
if (!json.isString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto string{ Detail::GetStringView(json) };
|
||||
return string.length() == 38 && string.front() == '{' && string.back() == '}';
|
||||
}
|
||||
};
|
||||
|
||||
// (GUID and winrt::guid are mutually convertible!)
|
||||
template<>
|
||||
struct ConversionTrait<winrt::guid> : public ConversionTrait<GUID>
|
||||
{
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<til::color>
|
||||
{
|
||||
til::color FromJson(const Json::Value& json)
|
||||
{
|
||||
return ::Microsoft::Console::Utils::ColorFromHexString(Detail::GetStringView(json));
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
if (!json.isString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto string{ Detail::GetStringView(json) };
|
||||
return (string.length() == 7 || string.length() == 3) && string.front() == '#';
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, typename TBase>
|
||||
struct EnumMapper
|
||||
{
|
||||
using pair_type = std::pair<std::string_view, T>;
|
||||
T FromJson(const Json::Value& json)
|
||||
{
|
||||
const auto name{ Detail::GetStringView(json) };
|
||||
for (const auto& pair : TBase::mappings)
|
||||
{
|
||||
if (pair.first == name)
|
||||
{
|
||||
return pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
throw UnexpectedValueException{ name };
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isString();
|
||||
}
|
||||
};
|
||||
|
||||
// FlagMapper is EnumMapper, but it works for bitfields.
|
||||
// It supports a string (single flag) or an array of strings.
|
||||
// Does an O(n*m) search; meant for small search spaces!
|
||||
//
|
||||
// Cleverly leverage EnumMapper to do the heavy lifting.
|
||||
template<typename T, typename TBase>
|
||||
struct FlagMapper : public EnumMapper<T, TBase>
|
||||
{
|
||||
static constexpr T AllSet{ static_cast<T>(~0u) };
|
||||
static constexpr T AllClear{ static_cast<T>(0u) };
|
||||
|
||||
T FromJson(const Json::Value& json)
|
||||
{
|
||||
if (json.isString())
|
||||
{
|
||||
return EnumMapper::FromJson(json);
|
||||
}
|
||||
else if (json.isArray())
|
||||
{
|
||||
unsigned int seen{ 0 };
|
||||
T value{};
|
||||
for (const auto& element : json)
|
||||
{
|
||||
const auto newFlag{ EnumMapper::FromJson(element) };
|
||||
if (++seen > 1 &&
|
||||
((newFlag == AllClear && value != AllClear) ||
|
||||
(value == AllClear && newFlag != AllClear)))
|
||||
{
|
||||
// attempt to combine AllClear (explicitly) with anything else
|
||||
throw UnexpectedValueException{ element.asString() };
|
||||
}
|
||||
value |= newFlag;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// We'll only get here if CanConvert has failed us.
|
||||
return AllClear;
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return EnumMapper::CanConvert(json) || json.isArray();
|
||||
}
|
||||
};
|
||||
|
||||
// Method Description:
|
||||
// - Helper that will populate a reference with a value converted from a json object.
|
||||
// Arguments:
|
||||
// - json: the json object to convert
|
||||
// - target: the value to populate with the converted result
|
||||
// Return Value:
|
||||
// - a boolean indicating whether the value existed (in this case, was non-null)
|
||||
//
|
||||
// GetValue, type-deduced, manual converter
|
||||
template<typename T, typename Converter>
|
||||
bool GetValue(const Json::Value& json, T& target, Converter&& conv)
|
||||
{
|
||||
if (json)
|
||||
{
|
||||
if (!conv.CanConvert(json))
|
||||
{
|
||||
throw TypeMismatchException{};
|
||||
}
|
||||
|
||||
target = conv.FromJson(json);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Overload on GetValue that will populate a std::optional with a value converted from json
|
||||
// - If the json value doesn't exist we'll leave the target object unmodified.
|
||||
// - If the json object is set to `null`, then
|
||||
// we'll instead set the target back to nullopt.
|
||||
// Arguments:
|
||||
// - json: the json object to convert
|
||||
// - target: the value to populate with the converted result
|
||||
// Return Value:
|
||||
// - a boolean indicating whether the optional was changed
|
||||
//
|
||||
// GetValue, type-deduced for optional, manual converter
|
||||
template<typename TOpt, typename Converter>
|
||||
bool GetValue(const Json::Value& json, std::optional<TOpt>& target, Converter&& conv)
|
||||
{
|
||||
if (json.isNull())
|
||||
{
|
||||
target = std::nullopt;
|
||||
return true; // null is valid for optionals
|
||||
}
|
||||
|
||||
std::decay_t<TOpt> local{};
|
||||
if (GetValue(json, local, std::forward<Converter>(conv)))
|
||||
{
|
||||
target = std::move(local);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetValue, forced return type, manual converter
|
||||
template<typename T, typename Converter>
|
||||
std::decay_t<T> GetValue(const Json::Value& json, Converter&& conv)
|
||||
{
|
||||
std::decay_t<T> local{};
|
||||
GetValue(json, local, std::forward<Converter>(conv));
|
||||
return local; // returns zero-initialized or value
|
||||
}
|
||||
|
||||
// GetValueForKey, type-deduced, manual converter
|
||||
template<typename T, typename Converter>
|
||||
bool GetValueForKey(const Json::Value& json, std::string_view key, T& target, Converter&& conv)
|
||||
{
|
||||
if (auto found{ json.find(&*key.cbegin(), (&*key.cbegin()) + key.size()) })
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetValue(*found, target, std::forward<Converter>(conv));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Wrap any caught exceptions in one that preserves context.
|
||||
throw KeyedException(key, std::current_exception());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetValueForKey, forced return type, manual converter
|
||||
template<typename T, typename Converter>
|
||||
std::decay_t<T> GetValueForKey(const Json::Value& json, std::string_view key, Converter&& conv)
|
||||
{
|
||||
std::decay_t<T> local{};
|
||||
GetValueForKey(json, key, local, std::forward<Converter>(conv));
|
||||
return local; // returns zero-initialized?
|
||||
}
|
||||
|
||||
// GetValue, type-deduced, with automatic converter
|
||||
template<typename T>
|
||||
bool GetValue(const Json::Value& json, T& target)
|
||||
{
|
||||
return GetValue(json, target, ConversionTrait<typename Detail::DeduceOptional<T>::Type>{});
|
||||
}
|
||||
|
||||
// GetValue, forced return type, with automatic converter
|
||||
template<typename T>
|
||||
std::decay_t<T> GetValue(const Json::Value& json)
|
||||
{
|
||||
std::decay_t<T> local{};
|
||||
GetValue(json, local, ConversionTrait<typename Detail::DeduceOptional<T>::Type>{});
|
||||
return local; // returns zero-initialized or value
|
||||
}
|
||||
|
||||
// GetValueForKey, type-deduced, with automatic converter
|
||||
template<typename T>
|
||||
bool GetValueForKey(const Json::Value& json, std::string_view key, T& target)
|
||||
{
|
||||
return GetValueForKey(json, key, target, ConversionTrait<typename Detail::DeduceOptional<T>::Type>{});
|
||||
}
|
||||
|
||||
// GetValueForKey, forced return type, with automatic converter
|
||||
template<typename T>
|
||||
std::decay_t<T> GetValueForKey(const Json::Value& json, std::string_view key)
|
||||
{
|
||||
return GetValueForKey<T>(json, key, ConversionTrait<typename Detail::DeduceOptional<T>::Type>{});
|
||||
}
|
||||
|
||||
// Get multiple values for keys (json, k, &v, k, &v, k, &v, ...).
|
||||
// Uses the default converter for each v.
|
||||
// Careful: this can cause a template explosion.
|
||||
constexpr void GetValuesForKeys(const Json::Value& /*json*/) {}
|
||||
|
||||
template<typename T, typename... Args>
|
||||
void GetValuesForKeys(const Json::Value& json, std::string_view key1, T&& val1, Args&&... args)
|
||||
{
|
||||
GetValueForKey(json, key1, val1);
|
||||
GetValuesForKeys(json, std::forward<Args>(args)...);
|
||||
}
|
||||
};
|
||||
|
||||
#define JSON_ENUM_MAPPER(...) \
|
||||
template<> \
|
||||
struct ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__> : \
|
||||
public ::TerminalApp::JsonUtils::EnumMapper<__VA_ARGS__, ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__>>
|
||||
|
||||
#define JSON_FLAG_MAPPER(...) \
|
||||
template<> \
|
||||
struct ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__> : \
|
||||
public ::TerminalApp::JsonUtils::FlagMapper<__VA_ARGS__, ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__>>
|
||||
|
||||
#define JSON_MAPPINGS(Count) \
|
||||
static constexpr std::array<pair_type, Count> mappings
|
439
src/cascadia/ut_app/JsonUtilsTests.cpp
Normal file
439
src/cascadia/ut_app/JsonUtilsTests.cpp
Normal file
|
@ -0,0 +1,439 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
#include "../TerminalApp/JsonUtilsNew.h"
|
||||
|
||||
using namespace Microsoft::Console;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
using namespace WEX::Common;
|
||||
using namespace TerminalApp::JsonUtils;
|
||||
|
||||
struct StructWithConverterSpecialization
|
||||
{
|
||||
int value;
|
||||
|
||||
bool operator==(const StructWithConverterSpecialization& other) const
|
||||
{
|
||||
return value == other.value;
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<StructWithConverterSpecialization>
|
||||
{
|
||||
StructWithConverterSpecialization FromJson(const Json::Value& value)
|
||||
{
|
||||
return StructWithConverterSpecialization{ value.asInt() };
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& value)
|
||||
{
|
||||
return value.isInt();
|
||||
}
|
||||
};
|
||||
|
||||
// Converts a JSON string value to an int and multiplies it by a specified factor
|
||||
struct CustomConverter
|
||||
{
|
||||
int factor{ 1 };
|
||||
|
||||
int FromJson(const Json::Value& value)
|
||||
{
|
||||
return std::stoi(value.asString()) * factor;
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& /*value*/)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
enum class JsonTestEnum : int
|
||||
{
|
||||
First = 0,
|
||||
Second,
|
||||
Third,
|
||||
Fourth,
|
||||
Fifth
|
||||
};
|
||||
|
||||
JSON_ENUM_MAPPER(JsonTestEnum)
|
||||
{
|
||||
JSON_MAPPINGS(5) = {
|
||||
pair_type{ "first", JsonTestEnum::First }, // DEFAULT
|
||||
pair_type{ "second", JsonTestEnum::Second },
|
||||
pair_type{ "third", JsonTestEnum::Third },
|
||||
pair_type{ "fourth", JsonTestEnum::Fourth },
|
||||
pair_type{ "fifth", JsonTestEnum::Fifth },
|
||||
};
|
||||
};
|
||||
|
||||
enum class JsonTestFlags : int
|
||||
{
|
||||
None = 0,
|
||||
First = 1 << 0,
|
||||
Second = 1 << 1,
|
||||
Third = 1 << 2,
|
||||
Fourth = 1 << 3,
|
||||
Fifth = 1 << 4,
|
||||
All = ~0,
|
||||
};
|
||||
|
||||
DEFINE_ENUM_FLAG_OPERATORS(JsonTestFlags);
|
||||
|
||||
JSON_FLAG_MAPPER(JsonTestFlags)
|
||||
{
|
||||
JSON_MAPPINGS(7) = {
|
||||
pair_type{ "none", AllClear },
|
||||
pair_type{ "first", JsonTestFlags::First },
|
||||
pair_type{ "second", JsonTestFlags::Second },
|
||||
pair_type{ "third", JsonTestFlags::Third },
|
||||
pair_type{ "fourth", JsonTestFlags::Fourth },
|
||||
pair_type{ "fifth", JsonTestFlags::Fifth },
|
||||
pair_type{ "all", AllSet },
|
||||
};
|
||||
};
|
||||
|
||||
namespace TerminalAppUnitTests
|
||||
{
|
||||
class JsonUtilsTests
|
||||
{
|
||||
TEST_CLASS(JsonUtilsTests);
|
||||
|
||||
TEST_METHOD(DocumentedBehaviors_GetValue_Returning);
|
||||
TEST_METHOD(DocumentedBehaviors_GetValue_Filling);
|
||||
TEST_METHOD(DocumentedBehaviors_GetValueForKey_Returning);
|
||||
TEST_METHOD(DocumentedBehaviors_GetValueForKey_Filling);
|
||||
|
||||
TEST_METHOD(BasicTypeConversion);
|
||||
TEST_METHOD(BasicTypeWithCustomConverter);
|
||||
TEST_METHOD(CustomTypeWithConverterSpecialization);
|
||||
TEST_METHOD(EnumMapper);
|
||||
TEST_METHOD(FlagMapper);
|
||||
|
||||
TEST_METHOD(NestedExceptionDuringKeyParse);
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
static bool _ReturnTrueForException(T&& /*exception*/)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void JsonUtilsTests::DocumentedBehaviors_GetValue_Returning()
|
||||
{
|
||||
std::string expected{ "correct" };
|
||||
Json::Value object{ expected };
|
||||
|
||||
//// 1. Bare Value ////
|
||||
//// 1.a. Type Invalid - Exception ////
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<int>(object), TypeMismatchException, _ReturnTrueForException);
|
||||
|
||||
//// 1.b. JSON NULL - Zero Value ////
|
||||
std::string zeroValueString{};
|
||||
VERIFY_ARE_EQUAL(zeroValueString, GetValue<std::string>(Json::Value::nullSingleton()));
|
||||
|
||||
//// 1.c. Valid - Valid ////
|
||||
VERIFY_ARE_EQUAL(expected, GetValue<std::string>(object));
|
||||
|
||||
//// 2. Optional ////
|
||||
//// 2.a. Type Invalid - Exception ////
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<std::optional<int>>(object), TypeMismatchException, _ReturnTrueForException);
|
||||
|
||||
//// 2.b. JSON NULL - nullopt ////
|
||||
VERIFY_ARE_EQUAL(std::nullopt, GetValue<std::optional<std::string>>(Json::Value::nullSingleton()));
|
||||
|
||||
//// 2.c. Valid - Valid ////
|
||||
VERIFY_ARE_EQUAL(expected, GetValue<std::optional<std::string>>(object));
|
||||
}
|
||||
|
||||
void JsonUtilsTests::DocumentedBehaviors_GetValue_Filling()
|
||||
{
|
||||
std::string expected{ "correct" };
|
||||
Json::Value object{ expected };
|
||||
|
||||
//// 1. Bare Value ////
|
||||
int outputRedHerring{ 5 }; // explicitly not the zero value
|
||||
std::string output{ "sentinel" }; // explicitly not the zero value
|
||||
|
||||
//// 1.a. Type Invalid - Exception ////
|
||||
VERIFY_THROWS_SPECIFIC(GetValue(object, outputRedHerring), TypeMismatchException, _ReturnTrueForException);
|
||||
VERIFY_ARE_EQUAL(5, outputRedHerring); // unchanged
|
||||
|
||||
//// 1.b. JSON NULL - Unchanged ////
|
||||
VERIFY_IS_FALSE(GetValue(Json::Value::nullSingleton(), output)); // FALSE = storage not modified!
|
||||
VERIFY_ARE_EQUAL("sentinel", output); // unchanged
|
||||
|
||||
//// 1.c. Valid ////
|
||||
VERIFY_IS_TRUE(GetValue(object, output));
|
||||
VERIFY_ARE_EQUAL(expected, output);
|
||||
|
||||
//// 2. Optional ////
|
||||
std::optional<int> optionalOutputRedHerring{ 6 }; // explicitly not nullopt
|
||||
std::optional<std::string> optionalOutput{ "sentinel2" }; // explicitly not nullopt
|
||||
|
||||
//// 2.a. Type Invalid - Exception ////
|
||||
VERIFY_THROWS_SPECIFIC(GetValue(object, optionalOutputRedHerring), TypeMismatchException, _ReturnTrueForException);
|
||||
VERIFY_ARE_EQUAL(6, optionalOutputRedHerring); // unchanged
|
||||
|
||||
//// 2.b. JSON NULL - nullopt ////
|
||||
VERIFY_IS_TRUE(GetValue(Json::Value::nullSingleton(), optionalOutput)); // TRUE = storage modified!
|
||||
VERIFY_ARE_EQUAL(std::nullopt, optionalOutput); // changed to nullopt
|
||||
|
||||
//// 2.c. Valid ////
|
||||
VERIFY_IS_TRUE(GetValue(object, optionalOutput));
|
||||
VERIFY_ARE_EQUAL(expected, optionalOutput);
|
||||
}
|
||||
|
||||
void JsonUtilsTests::DocumentedBehaviors_GetValueForKey_Returning()
|
||||
{
|
||||
// These are mostly duplicates of the GetValue tests save for the additional case (d)
|
||||
std::string expected{ "correct" };
|
||||
std::string key{ "key" };
|
||||
std::string nullKey{ "nullKey" };
|
||||
std::string invalidKey{ "invalidKey" };
|
||||
|
||||
Json::Value object{ Json::objectValue };
|
||||
object[key] = Json::Value{ expected };
|
||||
object[nullKey] = Json::Value::nullSingleton();
|
||||
|
||||
//// 1. Bare Value ////
|
||||
//// 1.a. Type Invalid - Exception ////
|
||||
VERIFY_THROWS_SPECIFIC(GetValueForKey<int>(object, key), KeyedException, _ReturnTrueForException);
|
||||
|
||||
//// 1.b. JSON NULL - Zero Value ////
|
||||
std::string zeroValueString{};
|
||||
VERIFY_ARE_EQUAL(zeroValueString, GetValueForKey<std::string>(object, nullKey));
|
||||
|
||||
//// 1.c. Valid - Valid ////
|
||||
VERIFY_ARE_EQUAL(expected, GetValueForKey<std::string>(object, key));
|
||||
|
||||
//// 1.d. Not Found - Zero Value ////
|
||||
VERIFY_ARE_EQUAL(zeroValueString, GetValueForKey<std::string>(object, invalidKey));
|
||||
|
||||
//// 2. Optional ////
|
||||
//// 2.a. Type Invalid - Exception ////
|
||||
VERIFY_THROWS_SPECIFIC(GetValueForKey<std::optional<int>>(object, key), KeyedException, _ReturnTrueForException);
|
||||
|
||||
//// 2.b. JSON NULL - nullopt ////
|
||||
VERIFY_ARE_EQUAL(std::nullopt, GetValueForKey<std::optional<std::string>>(object, nullKey));
|
||||
|
||||
//// 2.c. Valid - Valid ////
|
||||
VERIFY_ARE_EQUAL(expected, GetValueForKey<std::optional<std::string>>(object, key));
|
||||
|
||||
//// 2.d. Not Found - nullopt ////
|
||||
VERIFY_ARE_EQUAL(std::nullopt, GetValueForKey<std::optional<std::string>>(object, invalidKey));
|
||||
}
|
||||
|
||||
void JsonUtilsTests::DocumentedBehaviors_GetValueForKey_Filling()
|
||||
{
|
||||
// These are mostly duplicates of the GetValue tests save for the additional case (d)
|
||||
std::string expected{ "correct" };
|
||||
std::string key{ "key" };
|
||||
std::string nullKey{ "nullKey" };
|
||||
std::string invalidKey{ "invalidKey" };
|
||||
|
||||
Json::Value object{ Json::objectValue };
|
||||
object[key] = Json::Value{ expected };
|
||||
object[nullKey] = Json::Value::nullSingleton();
|
||||
|
||||
//// 1. Bare Value ////
|
||||
int outputRedHerring{ 5 }; // explicitly not the zero value
|
||||
std::string output{ "sentinel" }; // explicitly not the zero value
|
||||
|
||||
//// 1.a. Type Invalid - Exception ////
|
||||
VERIFY_THROWS_SPECIFIC(GetValueForKey(object, key, outputRedHerring), KeyedException, _ReturnTrueForException);
|
||||
VERIFY_ARE_EQUAL(5, outputRedHerring); // unchanged
|
||||
|
||||
//// 1.b. JSON NULL - Unchanged ////
|
||||
VERIFY_IS_FALSE(GetValueForKey(object, nullKey, output)); // FALSE = storage not modified!
|
||||
VERIFY_ARE_EQUAL("sentinel", output); // unchanged
|
||||
|
||||
//// 1.c. Valid ////
|
||||
VERIFY_IS_TRUE(GetValueForKey(object, key, output));
|
||||
VERIFY_ARE_EQUAL(expected, output);
|
||||
|
||||
//// 1.d. Not Found - Unchanged ////
|
||||
// (restore the sentinel)
|
||||
output = "sentinel";
|
||||
VERIFY_IS_FALSE(GetValueForKey(object, invalidKey, output));
|
||||
VERIFY_ARE_EQUAL("sentinel", output);
|
||||
|
||||
//// 2. Optional ////
|
||||
std::optional<int> optionalOutputRedHerring{ 6 }; // explicitly not nullopt
|
||||
std::optional<std::string> optionalOutput{ "sentinel2" }; // explicitly not nullopt
|
||||
|
||||
//// 2.a. Type Invalid - Exception ////
|
||||
VERIFY_THROWS_SPECIFIC(GetValueForKey(object, key, optionalOutputRedHerring), KeyedException, _ReturnTrueForException);
|
||||
VERIFY_ARE_EQUAL(6, optionalOutputRedHerring); // unchanged
|
||||
|
||||
//// 2.b. JSON NULL - nullopt ////
|
||||
VERIFY_IS_TRUE(GetValueForKey(object, nullKey, optionalOutput)); // TRUE = storage modified!
|
||||
VERIFY_ARE_EQUAL(std::nullopt, optionalOutput); // changed to nullopt
|
||||
|
||||
//// 2.c. Valid ////
|
||||
VERIFY_IS_TRUE(GetValueForKey(object, key, optionalOutput));
|
||||
VERIFY_ARE_EQUAL(expected, optionalOutput);
|
||||
|
||||
//// 2.d. Not Found - Unchanged ////
|
||||
optionalOutput = "sentinel";
|
||||
VERIFY_IS_FALSE(GetValueForKey(object, invalidKey, optionalOutput));
|
||||
VERIFY_ARE_EQUAL("sentinel", optionalOutput);
|
||||
}
|
||||
|
||||
// Since we've established the filling and returning functions work,
|
||||
// we're going to focus on some type-specific tests and use the returning
|
||||
// versions.
|
||||
|
||||
template<typename TExpected, typename TJson>
|
||||
static void TryBasicType(TExpected&& expected, TJson&& json)
|
||||
{
|
||||
Json::Value jsonObject{ json };
|
||||
const auto value{ GetValue<TExpected>(jsonObject) };
|
||||
VERIFY_ARE_EQUAL(expected, value, NoThrowString{}.Format(L"(type: %hs)", typeid(TExpected).name()));
|
||||
}
|
||||
|
||||
void JsonUtilsTests::BasicTypeConversion()
|
||||
{
|
||||
// Battery of all basic types ;P
|
||||
TryBasicType(std::string{ "hello" }, "hello");
|
||||
TryBasicType(int{ -1024 }, -1024);
|
||||
TryBasicType(unsigned int{ 1024 }, 1024);
|
||||
TryBasicType(false, false);
|
||||
TryBasicType(1.0f, 1.0f);
|
||||
|
||||
// string -> wstring
|
||||
TryBasicType(std::wstring{ L"hello" }, "hello");
|
||||
|
||||
// float -> double
|
||||
TryBasicType(1.0, 1.0f);
|
||||
|
||||
// double -> float
|
||||
TryBasicType(1.0f, 1.0);
|
||||
|
||||
TryBasicType(til::color{ 0xab, 0xcd, 0xef }, "#abcdef");
|
||||
|
||||
static const std::string testGuidString{ "{AA8147AA-E289-4508-BE83-FB68361EF2F3}" }; // can't use a string_view; jsoncpp hates it
|
||||
static const GUID testGuid{ 0xaa8147aa, 0xe289, 0x4508, { 0xbe, 0x83, 0xfb, 0x68, 0x36, 0x1e, 0xf2, 0xf3 } };
|
||||
|
||||
TryBasicType(testGuid, testGuidString);
|
||||
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<GUID>({ "NOT_A_GUID" }), TypeMismatchException, _ReturnTrueForException);
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<GUID>({ "{too short for a guid but just a bit}" }), TypeMismatchException, _ReturnTrueForException);
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<GUID>({ "{proper length string not a guid tho?}" }), std::exception, _ReturnTrueForException);
|
||||
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<til::color>({ "#" }), TypeMismatchException, _ReturnTrueForException);
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<til::color>({ "#1234567890" }), TypeMismatchException, _ReturnTrueForException);
|
||||
}
|
||||
|
||||
void JsonUtilsTests::BasicTypeWithCustomConverter()
|
||||
{
|
||||
// { "key": "100" }
|
||||
Json::Value object{ Json::objectValue };
|
||||
object["key"] = Json::Value{ "100" };
|
||||
|
||||
//// Temporary (rvalue) Converter ////
|
||||
VERIFY_ARE_EQUAL(100, GetValue<int>(object["key"], CustomConverter{}));
|
||||
VERIFY_ARE_EQUAL(100, GetValueForKey<int>(object, "key", CustomConverter{}));
|
||||
|
||||
//// lvalue converter ////
|
||||
CustomConverter converterWithFactor{ 2 };
|
||||
VERIFY_ARE_EQUAL(200, GetValue<int>(object["key"], converterWithFactor));
|
||||
VERIFY_ARE_EQUAL(200, GetValueForKey<int>(object, "key", converterWithFactor));
|
||||
}
|
||||
|
||||
void JsonUtilsTests::CustomTypeWithConverterSpecialization()
|
||||
{
|
||||
// { "key": 1024 }
|
||||
Json::Value object{ Json::objectValue };
|
||||
object["key"] = Json::Value{ 1024 };
|
||||
|
||||
VERIFY_ARE_EQUAL(StructWithConverterSpecialization{ 1024 }, GetValue<StructWithConverterSpecialization>(object["key"]));
|
||||
VERIFY_ARE_EQUAL(StructWithConverterSpecialization{ 1024 }, GetValueForKey<StructWithConverterSpecialization>(object, "key"));
|
||||
}
|
||||
|
||||
void JsonUtilsTests::EnumMapper()
|
||||
{
|
||||
// Basic string
|
||||
Json::Value stringFirst{ "first" };
|
||||
VERIFY_ARE_EQUAL(JsonTestEnum::First, GetValue<JsonTestEnum>(stringFirst));
|
||||
|
||||
Json::Value stringSecond{ "second" };
|
||||
VERIFY_ARE_EQUAL(JsonTestEnum::Second, GetValue<JsonTestEnum>(stringSecond));
|
||||
|
||||
// Unknown value should produce something?
|
||||
Json::Value stringUnknown{ "unknown" };
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<JsonTestEnum>(stringUnknown), UnexpectedValueException, _ReturnTrueForException);
|
||||
}
|
||||
|
||||
void JsonUtilsTests::FlagMapper()
|
||||
{
|
||||
// One flag
|
||||
Json::Value stringFirst{ "first" };
|
||||
VERIFY_ARE_EQUAL(JsonTestFlags::First, GetValue<JsonTestFlags>(stringFirst));
|
||||
|
||||
Json::Value stringSecond{ "second" };
|
||||
VERIFY_ARE_EQUAL(JsonTestFlags::Second, GetValue<JsonTestFlags>(stringSecond));
|
||||
|
||||
Json::Value stringAll{ "all" };
|
||||
VERIFY_ARE_EQUAL(JsonTestFlags::All, GetValue<JsonTestFlags>(stringAll));
|
||||
|
||||
// Multiple flags
|
||||
Json::Value arrayFirstSecond{ Json::arrayValue };
|
||||
arrayFirstSecond.append({ "first" });
|
||||
arrayFirstSecond.append({ "second" });
|
||||
VERIFY_ARE_EQUAL(JsonTestFlags::First | JsonTestFlags::Second, GetValue<JsonTestFlags>(arrayFirstSecond));
|
||||
|
||||
// No flags
|
||||
Json::Value emptyArray{ Json::arrayValue };
|
||||
VERIFY_ARE_EQUAL(JsonTestFlags::None, GetValue<JsonTestFlags>(emptyArray));
|
||||
|
||||
// Stacking Always + Any
|
||||
Json::Value arrayAllFirst{ Json::arrayValue };
|
||||
arrayAllFirst.append({ "all" });
|
||||
arrayAllFirst.append({ "first" });
|
||||
VERIFY_ARE_EQUAL(JsonTestFlags::All, GetValue<JsonTestFlags>(arrayAllFirst));
|
||||
|
||||
// Stacking None + Any (Exception)
|
||||
Json::Value arrayNoneFirst{ Json::arrayValue };
|
||||
arrayNoneFirst.append({ "none" });
|
||||
arrayNoneFirst.append({ "first" });
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<JsonTestFlags>(arrayNoneFirst), UnexpectedValueException, _ReturnTrueForException);
|
||||
|
||||
// Stacking Any + None (Exception; same as above, different order)
|
||||
Json::Value arrayFirstNone{ Json::arrayValue };
|
||||
arrayFirstNone.append({ "first" });
|
||||
arrayFirstNone.append({ "none" });
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<JsonTestFlags>(arrayFirstNone), UnexpectedValueException, _ReturnTrueForException);
|
||||
|
||||
// Unknown flag value?
|
||||
Json::Value stringUnknown{ "unknown" };
|
||||
VERIFY_THROWS_SPECIFIC(GetValue<JsonTestFlags>(stringUnknown), UnexpectedValueException, _ReturnTrueForException);
|
||||
}
|
||||
|
||||
void JsonUtilsTests::NestedExceptionDuringKeyParse()
|
||||
{
|
||||
std::string key{ "key" };
|
||||
Json::Value object{ Json::objectValue };
|
||||
object[key] = Json::Value{ "string" };
|
||||
|
||||
auto CheckKeyedException = [](const KeyedException& k) {
|
||||
try
|
||||
{
|
||||
k.RethrowInner();
|
||||
}
|
||||
catch (TypeMismatchException&)
|
||||
{
|
||||
// This Keyed exception contained the correct inner.
|
||||
return true;
|
||||
}
|
||||
// This Keyed exception did not contain the correct inner.
|
||||
return false;
|
||||
};
|
||||
VERIFY_THROWS_SPECIFIC(GetValueForKey<int>(object, key), KeyedException, CheckKeyedException);
|
||||
}
|
||||
|
||||
}
|
|
@ -36,6 +36,7 @@
|
|||
<ItemGroup>
|
||||
<ClCompile Include="ColorHelperTests.cpp" />
|
||||
<ClCompile Include="JsonTests.cpp" />
|
||||
<ClCompile Include="JsonUtilsTests.cpp" />
|
||||
<ClCompile Include="DynamicProfileTests.cpp" />
|
||||
<ClCompile Include="precomp.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
|
|
|
@ -11,6 +11,8 @@ Author(s):
|
|||
- Mike Griese (migrie) 12-Jun-2018
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Microsoft::Console::Utils
|
||||
{
|
||||
bool IsValidHandle(const HANDLE handle) noexcept;
|
||||
|
|
Loading…
Reference in a new issue