Skip to content

Commit

Permalink
feat: Add JSON serialization capabilities (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
FrogTheFrog authored Jun 25, 2024
1 parent dbceacf commit 4e8fd6d
Show file tree
Hide file tree
Showing 24 changed files with 587 additions and 12 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# Project configuration
#
cmake_minimum_required(VERSION 3.13) # todo: what is the minimum version required?
cmake_minimum_required(VERSION 3.24)
project(libdisplaydevice
DESCRIPTION "Library to modify display devices."
HOMEPAGE_URL "https://app.lizardbyte.dev"
Expand Down
2 changes: 1 addition & 1 deletion cmake/Boost_DD.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
include_guard(GLOBAL)

find_package(Boost 1.85)
find_package(Boost 1.85 QUIET GLOBAL)
if(NOT Boost_FOUND)
message(STATUS "Boost v1.85.x package not found in the system. Falling back to FetchContent.")
include(FetchContent)
Expand Down
11 changes: 11 additions & 0 deletions cmake/Json_DD.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#
# Loads the nlohmann_json library giving the priority to the system package first, with a fallback
# to the submodule.
#
include_guard(GLOBAL)

find_package(nlohmann_json 3.11 QUIET GLOBAL)
if(NOT nlohmann_json_FOUND)
message(STATUS "nlohmann_json v3.11.x package not found in the system. Falling back to submodule.")
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../third-party/json third-party/json)
endif()
9 changes: 8 additions & 1 deletion src/common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ set(MODULE libcommon)

# Globing headers (so that they appear in some IDEs) and sources
file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/displaydevice/*.h")
file(GLOB HEADER_DETAIL_LIST CONFIGURE_DEPENDS "include/displaydevice/detail/*.h")
file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp")

# Automatic library - will be static or dynamic based on user setting
add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST})
add_library(${MODULE} ${HEADER_LIST} ${HEADER_DETAIL_LIST} ${SOURCE_LIST})

# Provide the includes together with this library
target_include_directories(${MODULE} PUBLIC include)

# Additional external libraries
include(Json_DD)

# Link the additional libraries
target_link_libraries(${MODULE} PRIVATE nlohmann_json::nlohmann_json)
59 changes: 59 additions & 0 deletions src/common/include/displaydevice/detail/jsonconverter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#ifdef DD_JSON_DETAIL
// system includes
#include <nlohmann/json.hpp>

namespace display_device {
// A shared "toJson" implementation. Extracted here for UTs + coverage.
template <typename Type>
std::string
toJsonHelper(const Type &obj, const std::optional<unsigned int> &indent, bool *success) {
try {
if (success) {
*success = true;
}

nlohmann::json json_obj = obj;
return json_obj.dump(static_cast<int>(indent.value_or(-1)));
}
catch (const std::exception &err) { // GCOVR_EXCL_BR_LINE for fallthrough branch
if (success) {
*success = false;
}

return err.what();
}
}

// A shared "fromJson" implementation. Extracted here for UTs + coverage.
template <typename Type>
bool
fromJsonHelper(const std::string &string, Type &obj, std::string *error_message = nullptr) {
try {
if (error_message) {
error_message->clear();
}

Type parsed_obj = nlohmann::json::parse(string);
obj = std::move(parsed_obj);
return true;
}
catch (const std::exception &err) {
if (error_message) {
*error_message = err.what();
}

return false;
}
}

#define DD_JSON_DEFINE_CONVERTER(Type) \
std::string toJson(const Type &obj, const std::optional<unsigned int> &indent, bool *success) { \
return toJsonHelper(obj, indent, success); \
} \
bool fromJson(const std::string &string, Type &obj, std::string *error_message) { \
return fromJsonHelper<Type>(string, obj, error_message); \
}
} // namespace display_device
#endif
19 changes: 19 additions & 0 deletions src/common/include/displaydevice/detail/jsonserializer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

// local includes
#include "jsonserializerdetails.h"

#ifdef DD_JSON_DETAIL
namespace display_device {
// Enums
DD_JSON_DECLARE_SERIALIZE_TYPE(HdrState)
DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfiguration::DevicePreparation)

// Structs
DD_JSON_DECLARE_SERIALIZE_TYPE(Resolution)
DD_JSON_DECLARE_SERIALIZE_TYPE(Point)
DD_JSON_DECLARE_SERIALIZE_TYPE(EnumeratedDevice::Info)
DD_JSON_DECLARE_SERIALIZE_TYPE(EnumeratedDevice)
DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfiguration)
} // namespace display_device
#endif
84 changes: 84 additions & 0 deletions src/common/include/displaydevice/detail/jsonserializerdetails.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#pragma once

#ifdef DD_JSON_DETAIL
// system includes
#include <nlohmann/json.hpp>

// Special versions of the NLOHMANN definitions to remove the "m_" prefix in string form ('cause I like it that way ;P)
#define DD_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.m_##v1;
#define DD_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.m_##v1);

// Coverage has trouble with inlined functions when they are included in different units,
// therefore the usual macro was split into declaration and definition
#define DD_JSON_DECLARE_SERIALIZE_TYPE(Type) \
void to_json(nlohmann::json &nlohmann_json_j, const Type &nlohmann_json_t); \
void from_json(const nlohmann::json &nlohmann_json_j, Type &nlohmann_json_t);

#define DD_JSON_DEFINE_SERIALIZE_STRUCT(Type, ...) \
void to_json(nlohmann::json &nlohmann_json_j, const Type &nlohmann_json_t) { \
NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(DD_JSON_TO, __VA_ARGS__)) \
} \
\
void from_json(const nlohmann::json &nlohmann_json_j, Type &nlohmann_json_t) { \
NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(DD_JSON_FROM, __VA_ARGS__)) \
}

// Coverage has trouble with getEnumMap() function since it has a lot of "fallthrough"
// branches when creating a map, therefore the macro has baked in pattern to disable branch coverage
// in GCOVR
#define DD_JSON_DEFINE_SERIALIZE_ENUM_GCOVR_EXCL_BR_LINE(Type, ...) \
const std::map<Type, nlohmann::json> & \
getEnumMap(const Type &) { \
static_assert(std::is_enum<Type>::value, #Type " must be an enum!"); \
static const std::map<Type, nlohmann::json> map = __VA_ARGS__; \
return map; \
} \
\
void to_json(nlohmann::json &nlohmann_json_j, const Type &nlohmann_json_t) { \
nlohmann_json_j = findInEnumMap<Type>(#Type " is missing enum mapping!", [nlohmann_json_t](const auto &pair) { return pair.first == nlohmann_json_t; })->second; \
} \
\
void from_json(const nlohmann::json &nlohmann_json_j, Type &nlohmann_json_t) { \
nlohmann_json_t = findInEnumMap<Type>(#Type " is missing enum mapping!", [&nlohmann_json_j](const auto &pair) { return pair.second == nlohmann_json_j; })->first; \
}

namespace display_device {
// A shared function for enums to find values in the map. Extracted here for UTs + coverage
template <class T, class Predicate>
std::map<T, nlohmann::json>::const_iterator
findInEnumMap(const char *error_msg, Predicate predicate) {
const auto &map { getEnumMap(T {}) };
auto it { std::find_if(std::begin(map), std::end(map), predicate) };
if (it == std::end(map)) { // GCOVR_EXCL_BR_LINE for fallthrough branch
throw std::runtime_error(error_msg); // GCOVR_EXCL_BR_LINE for fallthrough branch
}
return it;
}
} // namespace display_device

namespace nlohmann {
// Specialization for optional types until they actually implement it.
template <typename T>
struct adl_serializer<std::optional<T>> {
static void
to_json(json &nlohmann_json_j, const std::optional<T> &nlohmann_json_t) {
if (nlohmann_json_t == std::nullopt) {
nlohmann_json_j = nullptr;
}
else {
nlohmann_json_j = *nlohmann_json_t;
}
}

static void
from_json(const json &nlohmann_json_j, std::optional<T> &nlohmann_json_t) {
if (nlohmann_json_j.is_null()) {
nlohmann_json_t = std::nullopt;
}
else {
nlohmann_json_t = nlohmann_json_j.template get<T>();
}
}
};
} // namespace nlohmann
#endif
23 changes: 23 additions & 0 deletions src/common/include/displaydevice/json.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

// local includes
#include "types.h"

/**
* @brief Helper MACRO to declare the toJson and fromJson converters for a type.
*
* EXAMPLES:
* ```cpp
* EnumeratedDeviceList devices;
* DD_LOG(info) << "Got devices:\n" << toJson(devices);
* ```
*/
#define DD_JSON_DECLARE_CONVERTER(Type) \
[[nodiscard]] std::string toJson(const Type &obj, const std::optional<unsigned int> &indent = 2u, bool *success = nullptr); \
[[nodiscard]] bool fromJson(const std::string &string, Type &obj, std::string *error_message = nullptr); // NOLINT(*-macro-parentheses)

// Shared converters (add as needed)
namespace display_device {
DD_JSON_DECLARE_CONVERTER(EnumeratedDeviceList)
DD_JSON_DECLARE_CONVERTER(SingleDisplayConfiguration)
} // namespace display_device
14 changes: 14 additions & 0 deletions src/common/json.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// header include
#include "displaydevice/json.h"

// special ordered include of details
#define DD_JSON_DETAIL
// clang-format off
#include "displaydevice/detail/jsonserializer.h"
#include "displaydevice/detail/jsonconverter.h"
// clang-format on

namespace display_device {
DD_JSON_DEFINE_CONVERTER(EnumeratedDeviceList)
DD_JSON_DEFINE_CONVERTER(SingleDisplayConfiguration)
} // namespace display_device
23 changes: 23 additions & 0 deletions src/common/jsonserializer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// special ordered include of details
#define DD_JSON_DETAIL
// clang-format off
#include "displaydevice/types.h"
#include "displaydevice/detail/jsonserializer.h"
// clang-format on

namespace display_device {
// Enums
DD_JSON_DEFINE_SERIALIZE_ENUM_GCOVR_EXCL_BR_LINE(HdrState, { { HdrState::Disabled, "Disabled" },
{ HdrState::Enabled, "Enabled" } })
DD_JSON_DEFINE_SERIALIZE_ENUM_GCOVR_EXCL_BR_LINE(SingleDisplayConfiguration::DevicePreparation, { { SingleDisplayConfiguration::DevicePreparation::VerifyOnly, "VerifyOnly" },
{ SingleDisplayConfiguration::DevicePreparation::EnsureActive, "EnsureActive" },
{ SingleDisplayConfiguration::DevicePreparation::EnsurePrimary, "EnsurePrimary" },
{ SingleDisplayConfiguration::DevicePreparation::EnsureOnlyDisplay, "EnsureOnlyDisplay" } })

// Structs
DD_JSON_DEFINE_SERIALIZE_STRUCT(Resolution, width, height)
DD_JSON_DEFINE_SERIALIZE_STRUCT(Point, x, y)
DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice::Info, resolution, resolution_scale, refresh_rate, primary, origin_point, hdr_state)
DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice, device_id, display_name, friendly_name, info)
DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfiguration, device_id, device_prep, resolution, refresh_rate, hdr_state)
} // namespace display_device
4 changes: 2 additions & 2 deletions src/common/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace display_device {
Logger &
Logger::get() {
static Logger instance;
static Logger instance; // GCOVR_EXCL_BR_LINE for some reason...
return instance;
}

Expand Down Expand Up @@ -87,7 +87,7 @@ namespace display_device {
}

// Log level
switch (log_level) {
switch (log_level) { // GCOVR_EXCL_BR_LINE for when there is no case match...
case LogLevel::verbose:
stream << "VERBOSE: ";
break;
Expand Down
5 changes: 4 additions & 1 deletion src/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ set(MODULE libwindows)

# Globing headers (so that they appear in some IDEs) and sources
file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/displaydevice/windows/*.h")
file(GLOB HEADER_DETAIL_LIST CONFIGURE_DEPENDS "include/displaydevice/windows/detail/*.h")
file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp")

# Automatic library - will be static or dynamic based on user setting
add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST})
add_library(${MODULE} ${HEADER_LIST} ${HEADER_DETAIL_LIST} ${SOURCE_LIST})

# Provide the includes together with this library
target_include_directories(${MODULE} PUBLIC include)
Expand All @@ -19,11 +20,13 @@ target_compile_definitions(${MODULE} PRIVATE

# Additional external libraries
include(Boost_DD)
include(Json_DD)

# Link the additional libraries
target_link_libraries(${MODULE} PRIVATE
Boost::algorithm
Boost::scope
Boost::uuid
libcommon
nlohmann_json::nlohmann_json
setupapi)
12 changes: 12 additions & 0 deletions src/windows/include/displaydevice/windows/detail/jsonserializer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

// local includes
#include "displaydevice/detail/jsonserializer.h"

#ifdef DD_JSON_DETAIL
namespace display_device {
// Structs
DD_JSON_DECLARE_SERIALIZE_TYPE(Rational)
DD_JSON_DECLARE_SERIALIZE_TYPE(DisplayMode)
} // namespace display_device
#endif
12 changes: 12 additions & 0 deletions src/windows/include/displaydevice/windows/json.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

// local includes
#include "displaydevice/json.h"
#include "types.h"

// Windows' converters (add as needed)
namespace display_device {
DD_JSON_DECLARE_CONVERTER(ActiveTopology)
DD_JSON_DECLARE_CONVERTER(DeviceDisplayModeMap)
DD_JSON_DECLARE_CONVERTER(HdrStateMap)
} // namespace display_device
15 changes: 15 additions & 0 deletions src/windows/json.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// header include
#include "displaydevice/windows/json.h"

// special ordered include of details
#define DD_JSON_DETAIL
// clang-format off
#include "displaydevice/windows/detail/jsonserializer.h"
#include "displaydevice/detail/jsonconverter.h"
// clang-format on

namespace display_device {
DD_JSON_DEFINE_CONVERTER(ActiveTopology)
DD_JSON_DEFINE_CONVERTER(DeviceDisplayModeMap)
DD_JSON_DEFINE_CONVERTER(HdrStateMap)
} // namespace display_device
12 changes: 12 additions & 0 deletions src/windows/jsonserializer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// special ordered include of details
#define DD_JSON_DETAIL
// clang-format off
#include "displaydevice/windows/types.h"
#include "displaydevice/windows/detail/jsonserializer.h"
// clang-format on

namespace display_device {
// Structs
DD_JSON_DEFINE_SERIALIZE_STRUCT(Rational, numerator, denominator)
DD_JSON_DEFINE_SERIALIZE_STRUCT(DisplayMode, resolution, refresh_rate)
} // namespace display_device
5 changes: 5 additions & 0 deletions tests/fixtures/comparison.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ namespace display_device {
operator==(const EnumeratedDevice &lhs, const EnumeratedDevice &rhs) {
return lhs.m_device_id == rhs.m_device_id && lhs.m_display_name == rhs.m_display_name && lhs.m_friendly_name == rhs.m_friendly_name && lhs.m_info == rhs.m_info;
}

bool
operator==(const SingleDisplayConfiguration &lhs, const SingleDisplayConfiguration &rhs) {
return lhs.m_device_id == rhs.m_device_id && lhs.m_device_prep == rhs.m_device_prep && lhs.m_resolution == rhs.m_resolution && lhs.m_refresh_rate == rhs.m_refresh_rate && lhs.m_hdr_state == rhs.m_hdr_state;
}
} // namespace display_device
3 changes: 3 additions & 0 deletions tests/fixtures/include/fixtures/comparison.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ namespace display_device {

bool
operator==(const EnumeratedDevice &lhs, const EnumeratedDevice &rhs);

bool
operator==(const SingleDisplayConfiguration &lhs, const SingleDisplayConfiguration &rhs);
} // namespace display_device
Loading

0 comments on commit 4e8fd6d

Please sign in to comment.