Introduce JsonUtilsNew as documented in #5875 (#6355)

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:
Dustin L. Howett 2020-06-17 17:27:42 -07:00 committed by GitHub
parent 6485a2b440
commit 10bc1a6532
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 918 additions and 0 deletions

View file

@ -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

View 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

View 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);
}
}

View file

@ -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>

View file

@ -11,6 +11,8 @@ Author(s):
- Mike Griese (migrie) 12-Jun-2018
--*/
#pragma once
namespace Microsoft::Console::Utils
{
bool IsValidHandle(const HANDLE handle) noexcept;