From d2a53a0dc9a863434f7f6289f89b7c204c2076c6 Mon Sep 17 00:00:00 2001 From: Adrien4193 Date: Wed, 1 May 2024 15:35:13 +0200 Subject: [PATCH 1/8] Checkpoint core. --- src/brayns/core/jsonv2/JsonReflector.h | 90 ++++++ src/brayns/core/jsonv2/JsonSchema.h | 88 ++++++ src/brayns/core/jsonv2/JsonValidator.cpp | 383 +++++++++++++++++++++++ src/brayns/core/jsonv2/JsonValidator.h | 125 ++++++++ src/brayns/core/jsonv2/JsonValue.cpp | 91 ++++++ src/brayns/core/jsonv2/JsonValue.h | 51 +++ src/brayns/core/utils/EnumReflector.h | 137 ++++++++ src/brayns/core/utils/String.cpp | 61 ++++ src/brayns/core/utils/String.h | 32 ++ 9 files changed, 1058 insertions(+) create mode 100644 src/brayns/core/jsonv2/JsonReflector.h create mode 100644 src/brayns/core/jsonv2/JsonSchema.h create mode 100644 src/brayns/core/jsonv2/JsonValidator.cpp create mode 100644 src/brayns/core/jsonv2/JsonValidator.h create mode 100644 src/brayns/core/jsonv2/JsonValue.cpp create mode 100644 src/brayns/core/jsonv2/JsonValue.h create mode 100644 src/brayns/core/utils/EnumReflector.h create mode 100644 src/brayns/core/utils/String.cpp create mode 100644 src/brayns/core/utils/String.h diff --git a/src/brayns/core/jsonv2/JsonReflector.h b/src/brayns/core/jsonv2/JsonReflector.h new file mode 100644 index 000000000..928987e8d --- /dev/null +++ b/src/brayns/core/jsonv2/JsonReflector.h @@ -0,0 +1,90 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "JsonSchema.h" +#include "JsonValue.h" + +namespace brayns::experimental +{ +template +struct JsonReflector +{ + template + static constexpr auto alwaysFalse = false; + + static_assert(alwaysFalse, "Please specialize JsonReflector"); + + static JsonSchema getSchema() + { + return {}; + } + + static JsonValue serialize(const T &value) + { + return {}; + } + + static T deserialize(const JsonValue &json) + { + return {}; + } +}; + +template +struct JsonSchemaStorage +{ + static inline const JsonSchema schema = JsonReflector::getSchema(); +}; + +template +const JsonSchema &getJsonSchema() +{ + return JsonSchemaStorage::schema; +} + +template +JsonValue serializeToJson(const T &value) +{ + return JsonReflector::serialize(value); +} + +template +T deserialize(const JsonValue &json) +{ + return JsonReflector::deserialize(json); +} + +template +std::string stringify(const T &value) +{ + auto json = serializeToJson(value); + return stringify(json); +} + +template +T parseJson(const std::string &data) +{ + auto json = parseJson(data); + return deserialize(json); +} +} diff --git a/src/brayns/core/jsonv2/JsonSchema.h b/src/brayns/core/jsonv2/JsonSchema.h new file mode 100644 index 000000000..230d4f7bf --- /dev/null +++ b/src/brayns/core/jsonv2/JsonSchema.h @@ -0,0 +1,88 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "JsonValue.h" + +namespace brayns::experimental +{ +enum class JsonType +{ + Undefined, + Null, + Boolean, + Integer, + Number, + String, + Array, + Object, +}; + +constexpr bool isNumeric(JsonType type) +{ + return type == JsonType::Integer || type == JsonType::Number; +} + +template<> +struct EnumReflector +{ + static EnumMap reflect() + { + return { + {"undefined", JsonType::Undefined}, + {"null", JsonType::Null}, + {"boolean", JsonType::Boolean}, + {"integer", JsonType::Integer}, + {"number", JsonType::Number}, + {"string", JsonType::String}, + {"array", JsonType::Array}, + {"object", JsonType::Object}, + }; + } +}; + +struct JsonSchema +{ + std::string title; + std::string description; + bool required = false; + JsonValue defaultValue; + JsonType type = JsonType::Undefined; + std::optional minimum; + std::optional maximum; + std::vector items; + std::optional minItems; + std::optional maxItems; + std::map properties; + std::vector enums; + std::vector oneOf; + + auto operator<=>(const JsonSchema &) const = default; +}; +} diff --git a/src/brayns/core/jsonv2/JsonValidator.cpp b/src/brayns/core/jsonv2/JsonValidator.cpp new file mode 100644 index 000000000..08e3243b1 --- /dev/null +++ b/src/brayns/core/jsonv2/JsonValidator.cpp @@ -0,0 +1,383 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "JsonValidator.h" + +#include +#include +#include + +#include + +namespace +{ +using namespace brayns::experimental; + +class ErrorContext +{ +public: + void push(JsonPathItem item) + { + _path.push_back(std::move(item)); + } + + void pop() + { + _path.pop_back(); + } + + void add(JsonError error) + { + _errors.push_back({_path, std::move(error)}); + } + + std::vector build() + { + return std::exchange(_errors, {}); + } + +private: + JsonPath _path; + std::vector _errors; +}; + +void check(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors); + +struct RequiredType +{ + JsonType value; + + bool accepts(JsonType type) + { + if (value == JsonType::Undefined) + { + return true; + } + if (type == value) + { + return true; + } + if (value == JsonType::Number && type == JsonType::Integer) + { + return true; + } + return false; + } +}; + +JsonType getJsonType(const JsonValue &json) +{ + if (json.isEmpty()) + { + return JsonType::Null; + } + if (json.isBoolean()) + { + return JsonType::Boolean; + } + if (json.isInteger()) + { + return JsonType::Integer; + } + if (json.isNumeric()) + { + return JsonType::Number; + } + if (json.isString()) + { + return JsonType::String; + } + if (isArray(json)) + { + return JsonType::Array; + } + if (isObject(json)) + { + return JsonType::Object; + } + throw std::invalid_argument("Value is not JSON"); +} + +void checkOneOf(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors) +{ + for (const auto &oneof : schema.oneOf) + { + auto suberrors = validate(json, oneof); + if (!suberrors.empty()) + { + return; + } + } + errors.add(InvalidOneOf{}); +} + +bool checkType(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors) +{ + auto required = RequiredType{schema.type}; + auto type = getJsonType(json); + if (required.accepts(type)) + { + return true; + } + errors.add(InvalidType{type, required.value}); + return false; +} + +void checkEnum(const std::string &value, const JsonSchema &schema, ErrorContext &errors) +{ + auto i = std::ranges::find(schema.enums, value); + if (i != schema.enums.end()) + { + return; + } + errors.add(InvalidEnum{value}); +} + +void checkRange(double value, const JsonSchema &schema, ErrorContext &errors) +{ + if (schema.minimum && value < *schema.minimum) + { + errors.add(BelowMinimum{value, *schema.minimum}); + } + if (schema.maximum && value > *schema.maximum) + { + errors.add(AboveMaximum{value, *schema.maximum}); + } +} + +void checkItemCount(std::size_t count, const JsonSchema &schema, ErrorContext &errors) +{ + if (schema.minItems && count < *schema.minItems) + { + errors.add(NotEnoughItems{count, *schema.minItems}); + } + if (schema.maxItems && count > *schema.maxItems) + { + errors.add(TooManyItems{count, *schema.maxItems}); + } +} + +void checkArrayItems(const JsonArray &array, const JsonSchema &schema, ErrorContext &errors) +{ + const auto &itemSchema = schema.items.at(0); + + auto index = std::size_t(0); + for (const auto &value : array) + { + errors.push(index); + check(value, itemSchema, errors); + errors.pop(); + + ++index; + } +} + +void checkMapItems(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +{ + const auto &itemSchema = schema.items.at(0); + + for (const auto &[key, value] : object) + { + errors.push(key); + check(value, itemSchema, errors); + errors.pop(); + } +} + +void checkRequiredProperties(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +{ + for (const auto &[key, property] : schema.properties) + { + if (!property.required) + { + continue; + } + if (object.has(key)) + { + continue; + } + errors.add(MissingRequiredProperty{key}); + } +} + +void checkUnknownProperties(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +{ + for (const auto &[key, value] : object) + { + if (schema.properties.contains(key)) + { + continue; + } + errors.add(UnknownProperty{key}); + } +} + +void checkProperties(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +{ + for (const auto &[key, value] : object) + { + errors.push(key); + check(value, schema.properties.at(key), errors); + errors.pop(); + } +} + +void checkObject(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +{ + if (!schema.items.empty()) + { + checkMapItems(object, schema, errors); + return; + } + checkUnknownProperties(object, schema, errors); + checkRequiredProperties(object, schema, errors); + checkProperties(object, schema, errors); +} + +void check(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors) +{ + if (!schema.oneOf.empty()) + { + checkOneOf(json, schema, errors); + return; + } + if (!checkType(json, schema, errors)) + { + return; + } + if (!schema.enums.empty()) + { + const auto &value = json.extract(); + checkEnum(value, schema, errors); + return; + } + if (isNumeric(schema.type)) + { + auto value = json.convert(); + checkRange(value, schema, errors); + return; + } + if (schema.type == JsonType::Array) + { + const auto &value = getArray(json); + checkItemCount(value.size(), schema, errors); + checkArrayItems(value, schema, errors); + return; + } + if (schema.type == JsonType::Object) + { + const auto &object = getObject(json); + checkObject(object, schema, errors); + return; + } +} +} + +namespace brayns::experimental +{ +std::string toString(const JsonPath &path) +{ + auto result = std::string(); + + for (const auto &item : path) + { + const auto *index = std::get_if(&item); + + if (index != nullptr) + { + result.append(fmt::format("[{}]", *index)); + continue; + } + + const auto &key = std::get(item); + + if (result.empty()) + { + result.append(key); + continue; + } + + result.push_back('.'); + result.append(key); + } + + return result; +} + +std::string toString(const InvalidType &error) +{ + const auto &type = getEnumName(error.type); + const auto &expected = getEnumName(error.expected); + return fmt::format("Invalid type: expected {}, got {}", expected, type); +} + +std::string toString(const BelowMinimum &error) +{ + return fmt::format("Value below minimum: {} < {}", error.value, error.minimum); +} + +std::string toString(const AboveMaximum &error) +{ + return fmt::format("Value above maximum: {} > {}", error.value, error.maximum); +} + +std::string toString(const NotEnoughItems &error) +{ + return fmt::format("Too many items: {} < {}", error.count, error.minItems); +} + +std::string toString(const TooManyItems &error) +{ + return fmt::format("Too many items: {} > {}", error.count, error.maxItems); +} + +std::string toString(const MissingRequiredProperty &error) +{ + return fmt::format("Missing required property: '{}'", error.name); +} + +std::string toString(const UnknownProperty &error) +{ + return fmt::format("Unknown property: '{}'", error.name); +} + +std::string toString(const InvalidEnum &error) +{ + return fmt::format("Invalid enum: '{}'", error.name); +} + +std::string toString(const InvalidOneOf &) +{ + return "Invalid oneOf"; +} + +std::string toString(const JsonError &error) +{ + return std::visit([](const auto &value) { return toString(value); }, error); +} + +std::vector validate(const JsonValue &json, const JsonSchema &schema) +{ + auto errors = ErrorContext(); + check(json, schema, errors); + return errors.build(); +} +} diff --git a/src/brayns/core/jsonv2/JsonValidator.h b/src/brayns/core/jsonv2/JsonValidator.h new file mode 100644 index 000000000..fb4be1b6b --- /dev/null +++ b/src/brayns/core/jsonv2/JsonValidator.h @@ -0,0 +1,125 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +#include "JsonSchema.h" +#include "JsonValue.h" + +namespace brayns::experimental +{ +using JsonPathItem = std::variant; +using JsonPath = std::vector; + +std::string toString(const JsonPath &path); + +struct InvalidType +{ + JsonType type; + JsonType expected; +}; + +std::string toString(const InvalidType &error); + +struct BelowMinimum +{ + double value; + double minimum; +}; + +std::string toString(const BelowMinimum &error); + +struct AboveMaximum +{ + double value; + double maximum; +}; + +std::string toString(const AboveMaximum &error); + +struct NotEnoughItems +{ + std::size_t count; + std::size_t minItems; +}; + +std::string toString(const NotEnoughItems &error); + +struct TooManyItems +{ + std::size_t count; + std::size_t maxItems; +}; + +std::string toString(const TooManyItems &error); + +struct MissingRequiredProperty +{ + std::string name; +}; + +std::string toString(const MissingRequiredProperty &error); + +struct UnknownProperty +{ + std::string name; +}; + +std::string toString(const UnknownProperty &error); + +struct InvalidEnum +{ + std::string name; +}; + +std::string toString(const InvalidEnum &error); + +struct InvalidOneOf +{ +}; + +std::string toString(const InvalidOneOf &error); + +using JsonError = std::variant< + InvalidType, + AboveMaximum, + BelowMinimum, + TooManyItems, + NotEnoughItems, + MissingRequiredProperty, + UnknownProperty, + InvalidEnum, + InvalidOneOf>; + +std::string toString(const JsonError &error); + +struct JsonSchemaError +{ + JsonPath path; + JsonError error; +}; + +std::vector validate(const JsonValue &json, const JsonSchema &schema); +} diff --git a/src/brayns/core/jsonv2/JsonValue.cpp b/src/brayns/core/jsonv2/JsonValue.cpp new file mode 100644 index 000000000..0618e763b --- /dev/null +++ b/src/brayns/core/jsonv2/JsonValue.cpp @@ -0,0 +1,91 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "JsonValue.h" + +#include + +#include +#include + +namespace brayns::experimental +{ +JsonArray &createArray(JsonValue &json) +{ + auto ptr = Poco::makeShared(); + json = ptr; + return *ptr; +} + +JsonObject &createObject(JsonValue &json) +{ + auto ptr = Poco::makeShared(); + json = ptr; + return *ptr; +} + +bool isArray(const JsonValue &json) +{ + return json.type() == typeid(JsonArray::Ptr); +} + +bool isObject(const JsonValue &json) +{ + return json.type() == typeid(JsonObject::Ptr); +} + +const JsonArray &getArray(const JsonValue &json) +{ + try + { + return *json.extract(); + } + catch (const Poco::Exception &e) + { + throw JsonException(e.displayText()); + } +} + +const JsonObject &getObject(const JsonValue &json) +{ + try + { + return *json.extract(); + } + catch (const Poco::Exception &e) + { + throw JsonException(e.displayText()); + } +} + +std::string stringify(const JsonValue &json) +{ + auto stream = std::ostringstream(); + Poco::JSON::Stringifier::condense(json, stream); + return stream.str(); +} + +JsonValue parseJson(const std::string &data) +{ + auto parser = Poco::JSON::Parser(); + return parser.parse(data); +} +} diff --git a/src/brayns/core/jsonv2/JsonValue.h b/src/brayns/core/jsonv2/JsonValue.h new file mode 100644 index 000000000..0e29f88bb --- /dev/null +++ b/src/brayns/core/jsonv2/JsonValue.h @@ -0,0 +1,51 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace brayns::experimental +{ +using JsonValue = Poco::Dynamic::Var; +using JsonArray = Poco::JSON::Array; +using JsonObject = Poco::JSON::Object; +using JsonException = Poco::JSON::JSONException; + +struct NullJson +{ +}; + +JsonArray &createArray(JsonValue &json); +JsonObject &createObject(JsonValue &json); +bool isArray(const JsonValue &json); +bool isObject(const JsonValue &json); +const JsonArray &getArray(const JsonValue &json); +const JsonObject &getObject(const JsonValue &json); +std::string stringify(const JsonValue &json); +JsonValue parseJson(const std::string &data); +} diff --git a/src/brayns/core/utils/EnumReflector.h b/src/brayns/core/utils/EnumReflector.h new file mode 100644 index 000000000..495501331 --- /dev/null +++ b/src/brayns/core/utils/EnumReflector.h @@ -0,0 +1,137 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace brayns::experimental +{ +template +using EnumMap = std::vector>; + +template +struct EnumReflector +{ + template + static constexpr auto alwaysFalse = false; + + static_assert(alwaysFalse, "Please specialize EnumReflector"); + + static EnumMap reflect() + { + return {}; + } +}; + +template +struct EnumStorage +{ + static inline const EnumMap mapping = EnumReflector::reflect(); +}; + +template +const EnumMap &getEnumMapping() +{ + return EnumStorage::mapping; +} + +template +std::vector getEnumNames() +{ + const auto &mapping = getEnumMapping(); + std::vector names; + names.reserve(mapping.size()); + for (const auto &[name, value] : mapping) + { + names.push_back(name); + } + return names; +} + +template +static std::vector getEnumValues() +{ + const auto &mapping = getEnumMapping(); + std::vector values; + values.reserve(values.size()); + for (const auto &[name, value] : mapping) + { + values.push_back(value); + } + return values; +} + +template +static const std::string *findEnumName(const T &value) +{ + const auto &mapping = getEnumMapping(); + for (const auto &[key, item] : mapping) + { + if (item == value) + { + return &key; + } + } + return nullptr; +} + +template +static const T *findEnumValue(const std::string &name) +{ + const auto &mapping = getEnumMapping(); + for (const auto &[key, item] : mapping) + { + if (key == name) + { + return &item; + } + } + return nullptr; +} + +template +const std::string &getEnumName(const T &value) +{ + const auto *name = findEnumName(value); + if (name) + { + return *name; + } + throw std::invalid_argument(fmt::format("Invalid enum value: {}", int(value))); +} + +template +const T &getEnumValue(const std::string &name) +{ + const auto *value = findValue(name); + if (value) + { + return *value; + } + throw std::invalid_argument(fmt::format("Invalid enum name '{}'", name)); +} +} diff --git a/src/brayns/core/utils/String.cpp b/src/brayns/core/utils/String.cpp new file mode 100644 index 000000000..da39d3361 --- /dev/null +++ b/src/brayns/core/utils/String.cpp @@ -0,0 +1,61 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "String.h" + +namespace brayns::experimental +{ +std::string join(std::span values, char separator) +{ + return join(values, {&separator, 1}); +} + +std::string join(std::span values, std::string_view separator) +{ + auto count = values.size(); + + if (count == 0) + { + return {}; + } + + auto step = separator.size(); + auto reserved = (count - 1) * step; + + for (const auto &value : values) + { + reserved += value.size(); + } + + auto result = std::string(); + result.reserve(reserved); + + result.append(values[0]); + + for (auto i = std::size_t(1); i < values.size(); ++i) + { + result.append(separator); + result.append(values[i]); + } + + return result; +} +} diff --git a/src/brayns/core/utils/String.h b/src/brayns/core/utils/String.h new file mode 100644 index 000000000..c0a4db8e6 --- /dev/null +++ b/src/brayns/core/utils/String.h @@ -0,0 +1,32 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +namespace brayns::experimental +{ +std::string join(std::span values, char separator); +std::string join(std::span values, std::string_view separator); +} From fe997c483171dd11a48a6dce15c70160f74241c3 Mon Sep 17 00:00:00 2001 From: Adrien4193 Date: Thu, 2 May 2024 14:25:37 +0200 Subject: [PATCH 2/8] Checkpoint basic types. --- src/brayns/core/jsonv2/Json.h | 33 ++++++ src/brayns/core/jsonv2/JsonReflector.h | 13 +-- src/brayns/core/jsonv2/JsonSchema.cpp | 82 ++++++++++++++ src/brayns/core/jsonv2/JsonSchema.h | 125 ++++++++++++++++++---- src/brayns/core/jsonv2/JsonValidator.cpp | 59 +--------- src/brayns/core/jsonv2/JsonValue.cpp | 23 ++-- src/brayns/core/jsonv2/JsonValue.h | 12 ++- src/brayns/core/jsonv2/types/Arrays.h | 83 ++++++++++++++ src/brayns/core/jsonv2/types/Enums.h | 64 +++++++++++ src/brayns/core/jsonv2/types/Maps.h | 77 +++++++++++++ src/brayns/core/jsonv2/types/Math.h | 104 ++++++++++++++++++ src/brayns/core/jsonv2/types/Primitives.h | 78 ++++++++++++++ src/brayns/core/jsonv2/types/Variants.h | 108 +++++++++++++++++++ src/brayns/core/utils/EnumReflector.h | 35 +++--- tests/core/jsonv2/TestJsonReflection.cpp | 104 ++++++++++++++++++ 15 files changed, 878 insertions(+), 122 deletions(-) create mode 100644 src/brayns/core/jsonv2/Json.h create mode 100644 src/brayns/core/jsonv2/JsonSchema.cpp create mode 100644 src/brayns/core/jsonv2/types/Arrays.h create mode 100644 src/brayns/core/jsonv2/types/Enums.h create mode 100644 src/brayns/core/jsonv2/types/Maps.h create mode 100644 src/brayns/core/jsonv2/types/Math.h create mode 100644 src/brayns/core/jsonv2/types/Primitives.h create mode 100644 src/brayns/core/jsonv2/types/Variants.h create mode 100644 tests/core/jsonv2/TestJsonReflection.cpp diff --git a/src/brayns/core/jsonv2/Json.h b/src/brayns/core/jsonv2/Json.h new file mode 100644 index 000000000..9cc77878e --- /dev/null +++ b/src/brayns/core/jsonv2/Json.h @@ -0,0 +1,33 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "JsonReflector.h" +#include "JsonSchema.h" +#include "JsonValidator.h" +#include "JsonValue.h" + +#include "types/Arrays.h" +#include "types/Enums.h" +#include "types/Maps.h" +#include "types/Primitives.h" +#include "types/Variants.h" diff --git a/src/brayns/core/jsonv2/JsonReflector.h b/src/brayns/core/jsonv2/JsonReflector.h index 928987e8d..faf3d081e 100644 --- a/src/brayns/core/jsonv2/JsonReflector.h +++ b/src/brayns/core/jsonv2/JsonReflector.h @@ -50,16 +50,11 @@ struct JsonReflector } }; -template -struct JsonSchemaStorage -{ - static inline const JsonSchema schema = JsonReflector::getSchema(); -}; - template const JsonSchema &getJsonSchema() { - return JsonSchemaStorage::schema; + static const JsonSchema schema = JsonReflector::getSchema(); + return schema; } template @@ -69,13 +64,13 @@ JsonValue serializeToJson(const T &value) } template -T deserialize(const JsonValue &json) +T deserializeJson(const JsonValue &json) { return JsonReflector::deserialize(json); } template -std::string stringify(const T &value) +std::string stringifyToJson(const T &value) { auto json = serializeToJson(value); return stringify(json); diff --git a/src/brayns/core/jsonv2/JsonSchema.cpp b/src/brayns/core/jsonv2/JsonSchema.cpp new file mode 100644 index 000000000..ad5775409 --- /dev/null +++ b/src/brayns/core/jsonv2/JsonSchema.cpp @@ -0,0 +1,82 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "JsonSchema.h" + +#include + +namespace brayns::experimental +{ +EnumInfo EnumReflector::reflect() +{ + return { + {"undefined", JsonType::Undefined}, + {"null", JsonType::Null}, + {"boolean", JsonType::Boolean}, + {"integer", JsonType::Integer}, + {"number", JsonType::Number}, + {"string", JsonType::String}, + {"array", JsonType::Array}, + {"object", JsonType::Object}, + }; +} + +JsonType getJsonType(const JsonValue &json) +{ + if (json.isEmpty()) + { + return JsonType::Null; + } + if (json.isBoolean()) + { + return JsonType::Boolean; + } + if (json.isInteger()) + { + return JsonType::Integer; + } + if (json.isNumeric()) + { + return JsonType::Number; + } + if (json.isString()) + { + return JsonType::String; + } + if (isArray(json)) + { + return JsonType::Array; + } + if (isObject(json)) + { + return JsonType::Object; + } + throw JsonException("Value is not JSON"); +} + +void RequiredJsonType::throwIfNotCompatible(JsonType type) +{ + if (!isCompatible(type)) + { + throw JsonException("Incompatible JSON types"); + } +} +} diff --git a/src/brayns/core/jsonv2/JsonSchema.h b/src/brayns/core/jsonv2/JsonSchema.h index 230d4f7bf..fd2122a17 100644 --- a/src/brayns/core/jsonv2/JsonSchema.h +++ b/src/brayns/core/jsonv2/JsonSchema.h @@ -21,9 +21,11 @@ #pragma once +#include #include #include #include +#include #include #include @@ -34,6 +36,7 @@ namespace brayns::experimental { enum class JsonType { + Unknown, Undefined, Null, Boolean, @@ -44,44 +47,120 @@ enum class JsonType Object, }; +template<> +struct EnumReflector +{ + static EnumInfo reflect(); +}; + constexpr bool isNumeric(JsonType type) { return type == JsonType::Integer || type == JsonType::Number; } +constexpr bool isPrimitive(JsonType type) +{ + return type >= JsonType::Undefined && type <= JsonType::String; +} + +template +struct JsonTypeReflector +{ + static inline constexpr auto type = JsonType::Unknown; +}; + template<> -struct EnumReflector +struct JsonTypeReflector +{ + static inline constexpr auto type = JsonType::Undefined; +}; + +template<> +struct JsonTypeReflector +{ + static inline constexpr auto type = JsonType::Null; +}; + +template<> +struct JsonTypeReflector { - static EnumMap reflect() + static inline constexpr auto type = JsonType::Boolean; +}; + +template +struct JsonTypeReflector +{ + static inline constexpr auto type = JsonType::Integer; +}; + +template +struct JsonTypeReflector +{ + static inline constexpr auto type = JsonType::Number; +}; + +template<> +struct JsonTypeReflector +{ + static inline constexpr auto type = JsonType::String; +}; + +template +constexpr JsonType jsonTypeOf = JsonTypeReflector::type; + +JsonType getJsonType(const JsonValue &json); + +struct RequiredJsonType +{ + JsonType value; + + void throwIfNotCompatible(JsonType type); + + constexpr bool isCompatible(JsonType type) { - return { - {"undefined", JsonType::Undefined}, - {"null", JsonType::Null}, - {"boolean", JsonType::Boolean}, - {"integer", JsonType::Integer}, - {"number", JsonType::Number}, - {"string", JsonType::String}, - {"array", JsonType::Array}, - {"object", JsonType::Object}, - }; + if (value == JsonType::Unknown || type == JsonType::Unknown) + { + return false; + } + if (type == value) + { + return true; + } + if (value == JsonType::Undefined) + { + return true; + } + if (value == JsonType::Number && type == JsonType::Integer) + { + return true; + } + return false; } }; +template +void throwIfNotCompatible(const JsonValue &json) +{ + auto type = getJsonType(json); + auto required = RequiredJsonType{jsonTypeOf}; + required.throwIfNotCompatible(type); +} + struct JsonSchema { - std::string title; - std::string description; + std::string title = {}; + std::string description = {}; bool required = false; - JsonValue defaultValue; + JsonValue defaultValue = {}; JsonType type = JsonType::Undefined; - std::optional minimum; - std::optional maximum; - std::vector items; - std::optional minItems; - std::optional maxItems; - std::map properties; - std::vector enums; - std::vector oneOf; + std::optional minimum = {}; + std::optional maximum = {}; + std::vector items = {}; + std::optional minItems = {}; + std::optional maxItems = {}; + std::map properties = {}; + std::vector enums = {}; + std::vector oneOf = {}; auto operator<=>(const JsonSchema &) const = default; }; diff --git a/src/brayns/core/jsonv2/JsonValidator.cpp b/src/brayns/core/jsonv2/JsonValidator.cpp index 08e3243b1..55fea0c47 100644 --- a/src/brayns/core/jsonv2/JsonValidator.cpp +++ b/src/brayns/core/jsonv2/JsonValidator.cpp @@ -61,61 +61,6 @@ class ErrorContext void check(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors); -struct RequiredType -{ - JsonType value; - - bool accepts(JsonType type) - { - if (value == JsonType::Undefined) - { - return true; - } - if (type == value) - { - return true; - } - if (value == JsonType::Number && type == JsonType::Integer) - { - return true; - } - return false; - } -}; - -JsonType getJsonType(const JsonValue &json) -{ - if (json.isEmpty()) - { - return JsonType::Null; - } - if (json.isBoolean()) - { - return JsonType::Boolean; - } - if (json.isInteger()) - { - return JsonType::Integer; - } - if (json.isNumeric()) - { - return JsonType::Number; - } - if (json.isString()) - { - return JsonType::String; - } - if (isArray(json)) - { - return JsonType::Array; - } - if (isObject(json)) - { - return JsonType::Object; - } - throw std::invalid_argument("Value is not JSON"); -} - void checkOneOf(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors) { for (const auto &oneof : schema.oneOf) @@ -131,9 +76,9 @@ void checkOneOf(const JsonValue &json, const JsonSchema &schema, ErrorContext &e bool checkType(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors) { - auto required = RequiredType{schema.type}; + auto required = RequiredJsonType{schema.type}; auto type = getJsonType(json); - if (required.accepts(type)) + if (required.isCompatible(type)) { return true; } diff --git a/src/brayns/core/jsonv2/JsonValue.cpp b/src/brayns/core/jsonv2/JsonValue.cpp index 0618e763b..1476df0ac 100644 --- a/src/brayns/core/jsonv2/JsonValue.cpp +++ b/src/brayns/core/jsonv2/JsonValue.cpp @@ -28,18 +28,14 @@ namespace brayns::experimental { -JsonArray &createArray(JsonValue &json) +JsonArray::Ptr createJsonArray() { - auto ptr = Poco::makeShared(); - json = ptr; - return *ptr; + return Poco::makeShared(); } -JsonObject &createObject(JsonValue &json) +JsonObject::Ptr createJsonObject() { - auto ptr = Poco::makeShared(); - json = ptr; - return *ptr; + return Poco::makeShared(); } bool isArray(const JsonValue &json) @@ -85,7 +81,14 @@ std::string stringify(const JsonValue &json) JsonValue parseJson(const std::string &data) { - auto parser = Poco::JSON::Parser(); - return parser.parse(data); + try + { + auto parser = Poco::JSON::Parser(); + return parser.parse(data); + } + catch (const Poco::Exception &e) + { + throw JsonException(e.displayText()); + } } } diff --git a/src/brayns/core/jsonv2/JsonValue.h b/src/brayns/core/jsonv2/JsonValue.h index 0e29f88bb..e33494546 100644 --- a/src/brayns/core/jsonv2/JsonValue.h +++ b/src/brayns/core/jsonv2/JsonValue.h @@ -21,6 +21,7 @@ #pragma once +#include #include #include @@ -34,14 +35,19 @@ namespace brayns::experimental using JsonValue = Poco::Dynamic::Var; using JsonArray = Poco::JSON::Array; using JsonObject = Poco::JSON::Object; -using JsonException = Poco::JSON::JSONException; struct NullJson { }; -JsonArray &createArray(JsonValue &json); -JsonObject &createObject(JsonValue &json); +class JsonException : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +JsonArray::Ptr createJsonArray(); +JsonObject::Ptr createJsonObject(); bool isArray(const JsonValue &json); bool isObject(const JsonValue &json); const JsonArray &getArray(const JsonValue &json); diff --git a/src/brayns/core/jsonv2/types/Arrays.h b/src/brayns/core/jsonv2/types/Arrays.h new file mode 100644 index 000000000..1111f1a13 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Arrays.h @@ -0,0 +1,83 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +#include "Primitives.h" + +namespace brayns::experimental +{ +template +struct JsonArrayReflector +{ + using ValueType = typename T::value_type; + + static JsonSchema getSchema() + { + return { + .type = JsonType::Array, + .items = getJsonSchema(), + }; + } + + static JsonValue serialize(const T &value) + { + auto array = createJsonArray(); + for (const auto &item : value) + { + auto jsonItem = serializeToJson(item); + array->add(jsonItem); + } + return array; + } + + static T deserialize(const JsonValue &json) + { + const auto &array = getArray(json); + auto value = T(); + for (const auto &jsonItem : array) + { + auto item = deserializeJson(jsonItem); + value.push_back(std::move(item)); + } + return value; + } +}; + +template +struct JsonReflector> : JsonArrayReflector> +{ +}; + +template +struct JsonReflector> : JsonArrayReflector> +{ +}; + +template +struct JsonReflector> : JsonArrayReflector> +{ +}; +} diff --git a/src/brayns/core/jsonv2/types/Enums.h b/src/brayns/core/jsonv2/types/Enums.h new file mode 100644 index 000000000..e76638ff3 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Enums.h @@ -0,0 +1,64 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +#include + +#include "Primitives.h" + +namespace brayns::experimental +{ +template +concept Enum = std::is_enum_v; + +template +struct JsonReflector +{ + static JsonSchema getSchema() + { + return { + .type = JsonType::String, + .enums = getEnumNames(), + }; + } + + static JsonValue serialize(const T &value) + { + return getEnumName(value); + } + + static T deserialize(const JsonValue &json) + { + auto name = deserializeJson(json); + try + { + return getEnumValue(name); + } + catch (const std::exception &e) + { + throw JsonException(e.what()); + } + } +}; +} diff --git a/src/brayns/core/jsonv2/types/Maps.h b/src/brayns/core/jsonv2/types/Maps.h new file mode 100644 index 000000000..50f1a7ca4 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Maps.h @@ -0,0 +1,77 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +#include "Primitives.h" + +namespace brayns::experimental +{ +template +struct JsonMapReflector +{ + using ValueType = typename T::mapped_type; + + static JsonSchema getSchema() + { + return { + .type = JsonType::Object, + .items = getJsonSchema(), + }; + } + + static JsonValue serialize(const T &value) + { + auto object = createJsonObject(); + for (const auto &[key, item] : value) + { + auto jsonItem = serializeToJson(item); + object->set(key, jsonItem); + } + return object; + } + + static T deserialize(const JsonValue &json) + { + const auto &object = getObject(json); + auto value = T(); + for (const auto &[key, jsonItem] : object) + { + value[key] = deserializeJson(jsonItem); + } + return value; + } +}; + +template +struct JsonReflector> : JsonMapReflector> +{ +}; + +template +struct JsonReflector> : JsonMapReflector> +{ +}; +} diff --git a/src/brayns/core/jsonv2/types/Math.h b/src/brayns/core/jsonv2/types/Math.h new file mode 100644 index 000000000..7008ab874 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Math.h @@ -0,0 +1,104 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +#include "Primitives.h" + +namespace brayns::experimental +{ +template +struct StaticJsonArray +{ + static auto &getItem(auto &value, std::size_t index) + { + return value[index]; + } +}; + +template<> +struct StaticJsonArray +{ + static auto &getItem(auto &value, std::size_t index) + { + return (&value.i)[index]; + } +}; + +template +struct JsonMathReflector +{ + using ValueType = typename T::Scalar; + + static inline constexpr auto itemCount = sizeof(T) / sizeof(ValueType); + + static JsonSchema getSchema() + { + return { + .type = JsonType::Array, + .items = getJsonSchema(), + .minItems = itemCount, + .maxItems = itemCount, + }; + } + + static JsonValue serialize(const T &value) + { + auto array = createJsonArray(); + for (auto i = std::size_t(0); i < itemCount; ++i) + { + const auto &item = StaticJsonArray::getItem(value, i); + auto jsonItem = serializeToJson(item); + array->add(jsonItem); + } + return array; + } + + static T deserialize(const JsonValue &json) + { + const auto &array = getArray(json); + auto value = T(); + if (array.size() != itemCount) + { + throw JsonException("Invalid static array size"); + } + auto i = std::size_t(0); + for (const auto &jsonItem : array) + { + auto &item = StaticJsonArray::getItem(value, i); + item = deserializeJson(jsonItem); + ++i; + } + return value; + } +}; + +template +struct JsonReflector> : JsonMathReflector> +{ +}; + +struct JsonReflector : JsonMathReflector +{ +}; +} diff --git a/src/brayns/core/jsonv2/types/Primitives.h b/src/brayns/core/jsonv2/types/Primitives.h new file mode 100644 index 000000000..85ea19427 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Primitives.h @@ -0,0 +1,78 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include + +#include + +namespace brayns::experimental +{ +template +concept JsonPrimitive = isPrimitive(jsonTypeOf); + +template +struct JsonReflector +{ + static JsonSchema getSchema() + { + constexpr auto type = jsonTypeOf; + auto schema = JsonSchema{.type = type}; + if constexpr (isNumeric(type)) + { + schema.minimum = std::numeric_limits::lowest(); + schema.maximum = std::numeric_limits::max(); + } + return schema; + } + + static JsonValue serialize(const T &value) + { + if constexpr (std::is_same_v) + { + return {}; + } + else + { + return value; + } + } + + static T deserialize(const JsonValue &json) + { + throwIfNotCompatible(json); + if constexpr (std::is_same_v) + { + return json; + } + else if constexpr (std::is_same_v) + { + return {}; + } + else + { + return json.convert(); + } + } +}; +} diff --git a/src/brayns/core/jsonv2/types/Variants.h b/src/brayns/core/jsonv2/types/Variants.h new file mode 100644 index 000000000..6fda8560f --- /dev/null +++ b/src/brayns/core/jsonv2/types/Variants.h @@ -0,0 +1,108 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include + +#include + +namespace brayns::experimental +{ +template +struct JsonReflector> +{ + static JsonSchema getSchema() + { + return { + .oneOf = {getJsonSchema(), getJsonSchema()}, + .required = false, + }; + } + + static JsonValue serialize(const T &value) + { + if (!value) + { + return {}; + } + return serializeToJson(*value); + } + + static T deserialize(const JsonValue &json) + { + if (json.isEmpty()) + { + return std::nullopt; + } + return deserialize(json); + } +}; + +template +struct JsonReflector> +{ + static JsonSchema getSchema() + { + return { + .oneOf = {getJsonSchema()...}, + }; + } + + static JsonValue serialize(const std::variant &value) + { + return std::visit([](const auto &item) { return serializeToJson(item); }, value); + } + + static std::variant deserialize(const JsonValue &json) + { + return tryDeserialize(json); + } + +private: + template + static std::variant tryDeserialize(const JsonValue &json) + { + try + { + return deserializeJson(json); + } + catch (...) + { + return tryDeserialize(); + } + } + + template + static std::variant tryDeserialize(const JsonValue &json) + { + try + { + return deserializeJson(json); + } + catch (...) + { + throw JsonException("Invalid oneOf"); + } + } +}; +} diff --git a/src/brayns/core/utils/EnumReflector.h b/src/brayns/core/utils/EnumReflector.h index 495501331..cb39dd38c 100644 --- a/src/brayns/core/utils/EnumReflector.h +++ b/src/brayns/core/utils/EnumReflector.h @@ -31,7 +31,7 @@ namespace brayns::experimental { template -using EnumMap = std::vector>; +using EnumInfo = std::vector>; template struct EnumReflector @@ -41,31 +41,26 @@ struct EnumReflector static_assert(alwaysFalse, "Please specialize EnumReflector"); - static EnumMap reflect() + static EnumInfo reflect() { return {}; } }; template -struct EnumStorage +const EnumInfo &reflectEnum() { - static inline const EnumMap mapping = EnumReflector::reflect(); -}; - -template -const EnumMap &getEnumMapping() -{ - return EnumStorage::mapping; + static const EnumInfo info = EnumReflector::reflect(); + return info; } template std::vector getEnumNames() { - const auto &mapping = getEnumMapping(); + const auto &info = reflectEnum(); std::vector names; - names.reserve(mapping.size()); - for (const auto &[name, value] : mapping) + names.reserve(info.size()); + for (const auto &[name, value] : info) { names.push_back(name); } @@ -75,10 +70,10 @@ std::vector getEnumNames() template static std::vector getEnumValues() { - const auto &mapping = getEnumMapping(); + const auto &info = reflectEnum(); std::vector values; values.reserve(values.size()); - for (const auto &[name, value] : mapping) + for (const auto &[name, value] : info) { values.push_back(value); } @@ -88,8 +83,8 @@ static std::vector getEnumValues() template static const std::string *findEnumName(const T &value) { - const auto &mapping = getEnumMapping(); - for (const auto &[key, item] : mapping) + const auto &info = reflectEnum(); + for (const auto &[key, item] : info) { if (item == value) { @@ -102,8 +97,8 @@ static const std::string *findEnumName(const T &value) template static const T *findEnumValue(const std::string &name) { - const auto &mapping = getEnumMapping(); - for (const auto &[key, item] : mapping) + const auto &info = reflectEnum(); + for (const auto &[key, item] : info) { if (key == name) { @@ -127,7 +122,7 @@ const std::string &getEnumName(const T &value) template const T &getEnumValue(const std::string &name) { - const auto *value = findValue(name); + const auto *value = findEnumValue(name); if (value) { return *value; diff --git a/tests/core/jsonv2/TestJsonReflection.cpp b/tests/core/jsonv2/TestJsonReflection.cpp new file mode 100644 index 000000000..1978bafc2 --- /dev/null +++ b/tests/core/jsonv2/TestJsonReflection.cpp @@ -0,0 +1,104 @@ +/* Copyright (c) 2015-2024, EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * Responsible author: Nadir Roman Guerrero + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include + +#include + +using namespace brayns::experimental; + +enum class SomeEnum +{ + Value1, + Value2, +}; + +namespace brayns::experimental +{ +template<> +struct EnumReflector +{ + static EnumInfo reflect() + { + return { + {"value1", SomeEnum::Value1}, + {"value2", SomeEnum::Value2}, + }; + } +}; +} + +TEST_CASE("JsonReflection") +{ + SUBCASE("Undefined") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Undefined}); + CHECK_EQ(deserializeJson(1), JsonValue(1)); + CHECK_EQ(serializeToJson(JsonValue("2")), JsonValue("2")); + } + SUBCASE("Null") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Null}); + deserializeJson({}); + CHECK_EQ(serializeToJson(NullJson()), JsonValue()); + CHECK_THROWS_AS(deserializeJson("xyz"), JsonException); + } + SUBCASE("Boolean") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Boolean}); + CHECK_EQ(deserializeJson(true), true); + CHECK_EQ(serializeToJson(true), JsonValue(true)); + CHECK_THROWS_AS(deserializeJson("xyz"), JsonException); + } + SUBCASE("Integer") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Integer, .minimum = 0, .maximum = 255}); + CHECK_EQ(getJsonSchema().type, JsonType::Integer); + CHECK_EQ(getJsonSchema().type, JsonType::Integer); + CHECK_EQ(deserializeJson(1), 1); + CHECK_EQ(serializeToJson(1), JsonValue(1)); + CHECK_THROWS_AS(deserializeJson(1.5), JsonException); + } + SUBCASE("Number") + { + constexpr auto fmin = std::numeric_limits::lowest(); + constexpr auto fmax = std::numeric_limits::max(); + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Number, .minimum = fmin, .maximum = fmax}); + CHECK_EQ(getJsonSchema().type, JsonType::Number); + CHECK_EQ(deserializeJson(1), 1.0f); + CHECK_EQ(serializeToJson(1.5f), JsonValue(1.5f)); + CHECK_THROWS_AS(deserializeJson("1.5"), JsonException); + } + SUBCASE("String") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::String}); + CHECK_EQ(deserializeJson("test"), JsonValue("test")); + CHECK_EQ(serializeToJson(std::string("test")), JsonValue("test")); + CHECK_THROWS_AS(deserializeJson(1), JsonException); + } + SUBCASE("Enums") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::String, .enums = {"value1", "value2"}}); + CHECK_EQ(deserializeJson("value1"), SomeEnum::Value1); + CHECK_EQ(serializeToJson(SomeEnum::Value2), JsonValue("value2")); + CHECK_THROWS_AS(deserializeJson(1), JsonException); + CHECK_THROWS_AS(deserializeJson("value3"), JsonException); + } +} From d7e5324ed09135e5722ab4d83389f3a3db7fae11 Mon Sep 17 00:00:00 2001 From: Adrien4193 Date: Thu, 2 May 2024 15:25:34 +0200 Subject: [PATCH 3/8] Checkpoint testing. --- src/brayns/core/jsonv2/Json.h | 1 + src/brayns/core/jsonv2/JsonReflector.h | 4 +- src/brayns/core/jsonv2/JsonValue.cpp | 2 +- src/brayns/core/jsonv2/JsonValue.h | 2 +- src/brayns/core/jsonv2/types/Arrays.h | 2 +- src/brayns/core/jsonv2/types/Maps.h | 2 +- src/brayns/core/jsonv2/types/Math.h | 11 +-- src/brayns/core/jsonv2/types/Variants.h | 30 +++----- tests/core/jsonv2/TestJsonReflection.cpp | 98 +++++++++++++++++++++++- 9 files changed, 120 insertions(+), 32 deletions(-) diff --git a/src/brayns/core/jsonv2/Json.h b/src/brayns/core/jsonv2/Json.h index 9cc77878e..1481c4df9 100644 --- a/src/brayns/core/jsonv2/Json.h +++ b/src/brayns/core/jsonv2/Json.h @@ -29,5 +29,6 @@ #include "types/Arrays.h" #include "types/Enums.h" #include "types/Maps.h" +#include "types/Math.h" #include "types/Primitives.h" #include "types/Variants.h" diff --git a/src/brayns/core/jsonv2/JsonReflector.h b/src/brayns/core/jsonv2/JsonReflector.h index faf3d081e..bf630255d 100644 --- a/src/brayns/core/jsonv2/JsonReflector.h +++ b/src/brayns/core/jsonv2/JsonReflector.h @@ -73,13 +73,13 @@ template std::string stringifyToJson(const T &value) { auto json = serializeToJson(value); - return stringify(json); + return stringifyToJson(json); } template T parseJson(const std::string &data) { auto json = parseJson(data); - return deserialize(json); + return deserializeJson(json); } } diff --git a/src/brayns/core/jsonv2/JsonValue.cpp b/src/brayns/core/jsonv2/JsonValue.cpp index 1476df0ac..ee3a43215 100644 --- a/src/brayns/core/jsonv2/JsonValue.cpp +++ b/src/brayns/core/jsonv2/JsonValue.cpp @@ -72,7 +72,7 @@ const JsonObject &getObject(const JsonValue &json) } } -std::string stringify(const JsonValue &json) +std::string stringifyToJson(const JsonValue &json) { auto stream = std::ostringstream(); Poco::JSON::Stringifier::condense(json, stream); diff --git a/src/brayns/core/jsonv2/JsonValue.h b/src/brayns/core/jsonv2/JsonValue.h index e33494546..813d862d0 100644 --- a/src/brayns/core/jsonv2/JsonValue.h +++ b/src/brayns/core/jsonv2/JsonValue.h @@ -52,6 +52,6 @@ bool isArray(const JsonValue &json); bool isObject(const JsonValue &json); const JsonArray &getArray(const JsonValue &json); const JsonObject &getObject(const JsonValue &json); -std::string stringify(const JsonValue &json); +std::string stringifyToJson(const JsonValue &json); JsonValue parseJson(const std::string &data); } diff --git a/src/brayns/core/jsonv2/types/Arrays.h b/src/brayns/core/jsonv2/types/Arrays.h index 1111f1a13..2d61cf81e 100644 --- a/src/brayns/core/jsonv2/types/Arrays.h +++ b/src/brayns/core/jsonv2/types/Arrays.h @@ -38,7 +38,7 @@ struct JsonArrayReflector { return { .type = JsonType::Array, - .items = getJsonSchema(), + .items = {getJsonSchema()}, }; } diff --git a/src/brayns/core/jsonv2/types/Maps.h b/src/brayns/core/jsonv2/types/Maps.h index 50f1a7ca4..f1e0c3d4f 100644 --- a/src/brayns/core/jsonv2/types/Maps.h +++ b/src/brayns/core/jsonv2/types/Maps.h @@ -38,7 +38,7 @@ struct JsonMapReflector { return { .type = JsonType::Object, - .items = getJsonSchema(), + .items = {getJsonSchema()}, }; } diff --git a/src/brayns/core/jsonv2/types/Math.h b/src/brayns/core/jsonv2/types/Math.h index 7008ab874..c39968706 100644 --- a/src/brayns/core/jsonv2/types/Math.h +++ b/src/brayns/core/jsonv2/types/Math.h @@ -21,7 +21,7 @@ #pragma once -#include +#include #include "Primitives.h" @@ -56,7 +56,7 @@ struct JsonMathReflector { return { .type = JsonType::Array, - .items = getJsonSchema(), + .items = {getJsonSchema()}, .minItems = itemCount, .maxItems = itemCount, }; @@ -67,7 +67,7 @@ struct JsonMathReflector auto array = createJsonArray(); for (auto i = std::size_t(0); i < itemCount; ++i) { - const auto &item = StaticJsonArray::getItem(value, i); + const auto &item = StaticJsonArray::getItem(value, i); auto jsonItem = serializeToJson(item); array->add(jsonItem); } @@ -85,7 +85,7 @@ struct JsonMathReflector auto i = std::size_t(0); for (const auto &jsonItem : array) { - auto &item = StaticJsonArray::getItem(value, i); + auto &item = StaticJsonArray::getItem(value, i); item = deserializeJson(jsonItem); ++i; } @@ -94,10 +94,11 @@ struct JsonMathReflector }; template -struct JsonReflector> : JsonMathReflector> +struct JsonReflector> : JsonMathReflector> { }; +template<> struct JsonReflector : JsonMathReflector { }; diff --git a/src/brayns/core/jsonv2/types/Variants.h b/src/brayns/core/jsonv2/types/Variants.h index 6fda8560f..fd46c51a5 100644 --- a/src/brayns/core/jsonv2/types/Variants.h +++ b/src/brayns/core/jsonv2/types/Variants.h @@ -34,12 +34,12 @@ struct JsonReflector> static JsonSchema getSchema() { return { - .oneOf = {getJsonSchema(), getJsonSchema()}, .required = false, + .oneOf = {getJsonSchema(), getJsonSchema()}, }; } - static JsonValue serialize(const T &value) + static JsonValue serialize(const std::optional &value) { if (!value) { @@ -48,13 +48,13 @@ struct JsonReflector> return serializeToJson(*value); } - static T deserialize(const JsonValue &json) + static std::optional deserialize(const JsonValue &json) { if (json.isEmpty()) { return std::nullopt; } - return deserialize(json); + return deserializeJson(json); } }; @@ -88,20 +88,14 @@ struct JsonReflector> } catch (...) { - return tryDeserialize(); - } - } - - template - static std::variant tryDeserialize(const JsonValue &json) - { - try - { - return deserializeJson(json); - } - catch (...) - { - throw JsonException("Invalid oneOf"); + if constexpr (sizeof...(Us) == 0) + { + throw JsonException("Invalid oneOf"); + } + else + { + return tryDeserialize(json); + } } } }; diff --git a/tests/core/jsonv2/TestJsonReflection.cpp b/tests/core/jsonv2/TestJsonReflection.cpp index 1978bafc2..f1805689a 100644 --- a/tests/core/jsonv2/TestJsonReflection.cpp +++ b/tests/core/jsonv2/TestJsonReflection.cpp @@ -22,6 +22,7 @@ #include +using namespace brayns; using namespace brayns::experimental; enum class SomeEnum @@ -47,6 +48,11 @@ struct EnumReflector TEST_CASE("JsonReflection") { + constexpr auto imin = std::numeric_limits::lowest(); + constexpr auto imax = std::numeric_limits::max(); + constexpr auto fmin = std::numeric_limits::lowest(); + constexpr auto fmax = std::numeric_limits::max(); + SUBCASE("Undefined") { CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Undefined}); @@ -78,8 +84,6 @@ TEST_CASE("JsonReflection") } SUBCASE("Number") { - constexpr auto fmin = std::numeric_limits::lowest(); - constexpr auto fmax = std::numeric_limits::max(); CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Number, .minimum = fmin, .maximum = fmax}); CHECK_EQ(getJsonSchema().type, JsonType::Number); CHECK_EQ(deserializeJson(1), 1.0f); @@ -93,7 +97,7 @@ TEST_CASE("JsonReflection") CHECK_EQ(serializeToJson(std::string("test")), JsonValue("test")); CHECK_THROWS_AS(deserializeJson(1), JsonException); } - SUBCASE("Enums") + SUBCASE("Enum") { CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::String, .enums = {"value1", "value2"}}); CHECK_EQ(deserializeJson("value1"), SomeEnum::Value1); @@ -101,4 +105,92 @@ TEST_CASE("JsonReflection") CHECK_THROWS_AS(deserializeJson(1), JsonException); CHECK_THROWS_AS(deserializeJson("value3"), JsonException); } + SUBCASE("Array") + { + CHECK_EQ( + getJsonSchema>(), + JsonSchema{.type = JsonType::Array, .items = {JsonSchema{.type = JsonType::String}}}); + CHECK_EQ(parseJson>("[1,2,3]"), std::vector{1, 2, 3}); + CHECK_EQ(stringifyToJson(std::vector{1, 2, 3}), "[1,2,3]"); + } + SUBCASE("Math") + { + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .type = JsonType::Array, + .items = {JsonSchema{.type = JsonType::Number, .minimum = fmin, .maximum = fmax}}, + .minItems = 3, + .maxItems = 3, + }); + + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .type = JsonType::Array, + .items = {JsonSchema{.type = JsonType::Number, .minimum = fmin, .maximum = fmax}}, + .minItems = 4, + .maxItems = 4, + }); + + CHECK_EQ(parseJson("[1,2,3]"), Vector3(1, 2, 3)); + CHECK_EQ(parseJson("[1,2,3,4]"), Quaternion(4, 1, 2, 3)); + + CHECK_EQ(stringifyToJson(Vector3(1, 2, 3)), "[1,2,3]"); + CHECK_EQ(stringifyToJson(Quaternion(4, 1, 2, 3)), "[1,2,3,4]"); + + CHECK_THROWS_AS(parseJson("[1,2,3,4]"), JsonException); + CHECK_THROWS_AS(parseJson("[1,2,3,4,5]"), JsonException); + + CHECK_THROWS_AS(parseJson("[1,2]"), JsonException); + CHECK_THROWS_AS(parseJson("[1,2]"), JsonException); + } + SUBCASE("Map") + { + using Map = std::map; + + CHECK_EQ( + getJsonSchema>(), + JsonSchema{ + .type = JsonType::Object, + .items = {JsonSchema{.type = JsonType::String}}, + }); + + auto map = Map{{"test1", 1}, {"test2", 2}}; + auto json = R"({"test1":1,"test2":2})"; + + CHECK_EQ(parseJson(json), map); + CHECK_EQ(stringifyToJson(map), json); + + CHECK_THROWS_AS(parseJson(R"({"invalid":2.5})"), JsonException); + } + SUBCASE("Variant") + { + using Variant = std::variant; + + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .oneOf = { + JsonSchema{.type = JsonType::String}, + JsonSchema{.type = JsonType::Integer, .minimum = imin, .maximum = imax}, + }}); + CHECK_EQ(serializeToJson(Variant("test")), JsonValue("test")); + CHECK_EQ(serializeToJson(Variant(1)), JsonValue(1)); + CHECK_EQ(deserializeJson(1), Variant(1)); + CHECK_EQ(deserializeJson("test"), Variant("test")); + CHECK_THROWS_AS(deserializeJson(1.5), JsonException); + + CHECK_EQ( + getJsonSchema>(), + JsonSchema{ + .required = false, + .oneOf = {JsonSchema{.type = JsonType::String}, JsonSchema{.type = JsonType::Null}}, + }); + CHECK_EQ(serializeToJson(std::optional("test")), JsonValue("test")); + CHECK_EQ(serializeToJson(std::optional()), JsonValue()); + CHECK_EQ(deserializeJson>({}), std::nullopt); + CHECK_EQ(deserializeJson>("test"), std::string("test")); + CHECK_THROWS_AS(deserializeJson>(1.5), JsonException); + } } From 1f10d5be31cc987c6ba0d800714ebcab70f744d2 Mon Sep 17 00:00:00 2001 From: Adrien4193 Date: Thu, 2 May 2024 16:34:57 +0200 Subject: [PATCH 4/8] Testing ok. --- src/brayns/core/jsonv2/JsonValidator.cpp | 18 +- src/brayns/core/jsonv2/JsonValidator.h | 2 +- tests/core/jsonv2/TestJsonSchema.cpp | 259 +++++++++++++++++++++++ 3 files changed, 271 insertions(+), 8 deletions(-) create mode 100644 tests/core/jsonv2/TestJsonSchema.cpp diff --git a/src/brayns/core/jsonv2/JsonValidator.cpp b/src/brayns/core/jsonv2/JsonValidator.cpp index 55fea0c47..71573bfd4 100644 --- a/src/brayns/core/jsonv2/JsonValidator.cpp +++ b/src/brayns/core/jsonv2/JsonValidator.cpp @@ -65,8 +65,8 @@ void checkOneOf(const JsonValue &json, const JsonSchema &schema, ErrorContext &e { for (const auto &oneof : schema.oneOf) { - auto suberrors = validate(json, oneof); - if (!suberrors.empty()) + auto suberrors = validateJsonSchema(json, oneof); + if (suberrors.empty()) { return; } @@ -177,10 +177,14 @@ void checkUnknownProperties(const JsonObject &object, const JsonSchema &schema, void checkProperties(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) { - for (const auto &[key, value] : object) + for (const auto &[key, itemSchema] : schema.properties) { + if (!object.has(key)) + { + continue; + } errors.push(key); - check(value, schema.properties.at(key), errors); + check(object.get(key), itemSchema, errors); errors.pop(); } } @@ -271,7 +275,7 @@ std::string toString(const InvalidType &error) { const auto &type = getEnumName(error.type); const auto &expected = getEnumName(error.expected); - return fmt::format("Invalid type: expected {}, got {}", expected, type); + return fmt::format("Invalid type: expected {} got {}", expected, type); } std::string toString(const BelowMinimum &error) @@ -286,7 +290,7 @@ std::string toString(const AboveMaximum &error) std::string toString(const NotEnoughItems &error) { - return fmt::format("Too many items: {} < {}", error.count, error.minItems); + return fmt::format("Not enough items: {} < {}", error.count, error.minItems); } std::string toString(const TooManyItems &error) @@ -319,7 +323,7 @@ std::string toString(const JsonError &error) return std::visit([](const auto &value) { return toString(value); }, error); } -std::vector validate(const JsonValue &json, const JsonSchema &schema) +std::vector validateJsonSchema(const JsonValue &json, const JsonSchema &schema) { auto errors = ErrorContext(); check(json, schema, errors); diff --git a/src/brayns/core/jsonv2/JsonValidator.h b/src/brayns/core/jsonv2/JsonValidator.h index fb4be1b6b..9be384c5c 100644 --- a/src/brayns/core/jsonv2/JsonValidator.h +++ b/src/brayns/core/jsonv2/JsonValidator.h @@ -121,5 +121,5 @@ struct JsonSchemaError JsonError error; }; -std::vector validate(const JsonValue &json, const JsonSchema &schema); +std::vector validateJsonSchema(const JsonValue &json, const JsonSchema &schema); } diff --git a/tests/core/jsonv2/TestJsonSchema.cpp b/tests/core/jsonv2/TestJsonSchema.cpp new file mode 100644 index 000000000..d1e9092a5 --- /dev/null +++ b/tests/core/jsonv2/TestJsonSchema.cpp @@ -0,0 +1,259 @@ +/* Copyright (c) 2015-2024, EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * Responsible author: Nadir Roman Guerrero + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include + +#include + +using namespace brayns; +using namespace brayns::experimental; + +TEST_CASE("JsonSchema") +{ + SUBCASE("Wildcard") + { + auto schema = JsonSchema(); + auto json = parseJson(R"({"test": 10})"); + + auto errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + + json = 1; + errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + } + SUBCASE("One of") + { + auto schema = JsonSchema{.oneOf = {getJsonSchema(), getJsonSchema()}}; + + auto errors = validateJsonSchema(1.0f, schema); + CHECK(errors.empty()); + + errors = validateJsonSchema("Test", schema); + CHECK(errors.empty()); + + errors = validateJsonSchema(true, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid oneOf"); + } + SUBCASE("Invalid type") + { + auto schema = JsonSchema{.type = JsonType::String}; + + auto errors = validateJsonSchema(1, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected string got integer"); + + schema.type = JsonType::Number; + errors = validateJsonSchema(1, schema); + CHECK(errors.empty()); + } + SUBCASE("Limits") + { + auto schema = JsonSchema{ + .type = JsonType::Integer, + .minimum = -1, + .maximum = 3, + }; + + auto errors = validateJsonSchema(1, schema); + CHECK(errors.empty()); + + errors = validateJsonSchema(-1, schema); + CHECK(errors.empty()); + + errors = validateJsonSchema(3, schema); + CHECK(errors.empty()); + + errors = validateJsonSchema(-2, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Value below minimum: -2 < -1"); + + errors = validateJsonSchema(4, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Value above maximum: 4 > 3"); + + schema.minimum = std::nullopt; + schema.maximum = std::nullopt; + + errors = validateJsonSchema(-8, schema); + CHECK(errors.empty()); + + errors = validateJsonSchema(125, schema); + CHECK(errors.empty()); + } + SUBCASE("Enums") + { + auto schema = JsonSchema{ + .type = JsonType::String, + .enums = {"test1", "test2"}, + }; + + auto errors = validateJsonSchema("test1", schema); + CHECK(errors.empty()); + + errors = validateJsonSchema("test2", schema); + CHECK(errors.empty()); + + errors = validateJsonSchema("Test2", schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid enum: 'Test2'"); + } + SUBCASE("Property type") + { + auto schema = JsonSchema{ + .type = JsonType::Object, + .properties = {{ + "internal", + JsonSchema{ + .type = JsonType::Object, + .properties = {{"integer", getJsonSchema()}}, + }, + }}}; + + auto json = parseJson(R"({"internal": 1})"); + auto errors = validateJsonSchema(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected object got integer"); + + json = parseJson(R"({"internal": {"integer": true}})"); + errors = validateJsonSchema(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].path), "internal.integer"); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected integer got boolean"); + + json = parseJson(R"({"internal": {"integer": 1}})"); + errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + } + SUBCASE("Missing property") + { + auto schema = JsonSchema{ + .type = JsonType::Object, + .properties = + { + {"integer", JsonSchema{.required = true, .type = JsonType::Integer}}, + {"string", JsonSchema{.type = JsonType::String}}, + }, + }; + + auto json = parseJson(R"({"integer": 1, "string": "test"})"); + auto errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"({"string": "test"})"); + errors = validateJsonSchema(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Missing required property: 'integer'"); + + json = parseJson(R"({"integer": 1})"); + errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + } + SUBCASE("Unknown properties") + { + auto schema = JsonSchema{.type = JsonType::Object}; + + auto json = parseJson(R"({"something": 1})"); + auto errors = validateJsonSchema(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Unknown property: 'something'"); + + json = parseJson(R"({})"); + errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + } + SUBCASE("Item type") + { + auto schema = JsonSchema{ + .type = JsonType::Array, + .items = {JsonSchema{.type = JsonType::Integer}}, + }; + + auto json = parseJson(R"([1, 2, 3])"); + auto errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([])"); + errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([1, "test", 2])"); + errors = validateJsonSchema(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].path), "[1]"); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected integer got string"); + } + SUBCASE("Item count") + { + auto schema = JsonSchema{ + .type = JsonType::Array, + .items = {getJsonSchema()}, + .minItems = 1, + .maxItems = 3, + }; + + auto json = parseJson(R"([1])"); + auto errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([1, 2, 3])"); + errors = validateJsonSchema(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([])"); + errors = validateJsonSchema(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Not enough items: 0 < 1"); + + json = parseJson(R"([1, 2, 3, 4])"); + errors = validateJsonSchema(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Too many items: 4 > 3"); + } + SUBCASE("Nested") + { + auto internal = JsonSchema{ + .type = JsonType::Object, + .properties = {{"test3", getJsonSchema>()}}, + }; + + auto schema = JsonSchema{ + .type = JsonType::Object, + .properties = { + {"test1", JsonSchema{.required = true, .type = JsonType::Integer}}, + {"test2", + JsonSchema{ + .type = JsonType::Object, + .properties = {{"test3", getJsonSchema>()}}, + }}, + }}; + + auto json = parseJson(R"({"test2": {"test3": [1.3]}})"); + auto errors = validateJsonSchema(json, schema); + CHECK_EQ(errors.size(), 2); + + CHECK_EQ(toString(errors[0].path), ""); + CHECK_EQ(toString(errors[0].error), "Missing required property: 'test1'"); + + CHECK_EQ(toString(errors[1].path), "test2.test3[0]"); + CHECK_EQ(toString(errors[1].error), "Invalid type: expected integer got number"); + } +} From 0a41d1d3462b05c9bc881cf25a514f2582309aea Mon Sep 17 00:00:00 2001 From: Adrien4193 Date: Fri, 3 May 2024 09:04:06 +0200 Subject: [PATCH 5/8] Checkpoint schema serialization. --- src/brayns/core/jsonv2/Json.h | 1 + src/brayns/core/jsonv2/types/Schema.cpp | 196 ++++++++++++++++++++++++ src/brayns/core/jsonv2/types/Schema.h | 34 ++++ tests/core/jsonv2/TestJsonSchema.cpp | 22 +++ 4 files changed, 253 insertions(+) create mode 100644 src/brayns/core/jsonv2/types/Schema.cpp create mode 100644 src/brayns/core/jsonv2/types/Schema.h diff --git a/src/brayns/core/jsonv2/Json.h b/src/brayns/core/jsonv2/Json.h index 1481c4df9..404743202 100644 --- a/src/brayns/core/jsonv2/Json.h +++ b/src/brayns/core/jsonv2/Json.h @@ -31,4 +31,5 @@ #include "types/Maps.h" #include "types/Math.h" #include "types/Primitives.h" +#include "types/Schema.h" #include "types/Variants.h" diff --git a/src/brayns/core/jsonv2/types/Schema.cpp b/src/brayns/core/jsonv2/types/Schema.cpp new file mode 100644 index 000000000..a8333a2c1 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Schema.cpp @@ -0,0 +1,196 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "Schema.h" + +#include "Arrays.h" +#include "Enums.h" +#include "Maps.h" +#include "Primitives.h" + +namespace +{ +using namespace brayns::experimental; + +template +void set(JsonObject &object, const std::string &key, const T &value) +{ + auto json = serializeToJson(value); + object.set(key, json); +} + +void serializeNumber(JsonObject &object, const JsonSchema &schema) +{ + if (schema.minimum) + { + set(object, "minimum", *schema.minimum); + } + + if (schema.maximum) + { + set(object, "maximum", *schema.maximum); + } +} + +void serializeArray(JsonObject &object, const JsonSchema &schema) +{ + const auto &items = schema.items.at(0); + + if (items.type != JsonType::Undefined) + { + set(object, "items", items); + } + + if (schema.minItems) + { + set(object, "minItems", *schema.minItems); + } + + if (schema.maxItems) + { + set(object, "maxItems", *schema.maxItems); + } +} + +void serializeMap(JsonObject &object, const JsonSchema &schema) +{ + const auto &items = schema.items.at(0); + + if (items.type != JsonType::Undefined) + { + set(object, "additionalProperties", items); + } +} + +std::vector extractRequiredProperties(const JsonSchema &schema) +{ + const auto &properties = schema.properties; + + auto required = std::vector(); + + for (const auto &[key, value] : properties) + { + if (value.required) + { + required.push_back(key); + } + } + + return required; +} + +void serializeObject(JsonObject &object, const JsonSchema &schema) +{ + set(object, "additionalProperties", false); + + const auto &properties = schema.properties; + + if (properties.empty()) + { + return; + } + + set(object, "properties", properties); + + auto required = extractRequiredProperties(schema); + + if (!required.empty()) + { + set(object, "required", required); + } +} +} + +namespace brayns::experimental +{ +JsonSchema JsonReflector::getSchema() +{ + return JsonSchema{ + .title = "JsonSchema", + .type = JsonType::Object, + .items = {JsonSchema()}, + }; +} + +JsonValue JsonReflector::serialize(const JsonSchema &schema) +{ + auto object = createJsonObject(); + + if (!schema.title.empty()) + { + set(*object, "title", schema.title); + } + + if (!schema.description.empty()) + { + set(*object, "description", schema.description); + } + + if (schema.required) + { + set(*object, "default", schema.defaultValue); + } + + if (!schema.oneOf.empty()) + { + set(*object, "oneOf", schema.oneOf); + return object; + } + + if (schema.type != JsonType::Undefined) + { + set(*object, "type", schema.type); + } + + if (!schema.enums.empty()) + { + set(*object, "enum", schema.enums); + return object; + } + + if (isNumeric(schema.type)) + { + serializeNumber(*object, schema); + return object; + } + + if (schema.type == JsonType::Array) + { + serializeArray(*object, schema); + return object; + } + + if (schema.type != JsonType::Object) + { + return object; + } + + if (!schema.items.empty()) + { + serializeMap(*object, schema); + return object; + } + + serializeObject(*object, schema); + + return object; +} +} diff --git a/src/brayns/core/jsonv2/types/Schema.h b/src/brayns/core/jsonv2/types/Schema.h new file mode 100644 index 000000000..67005ded1 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Schema.h @@ -0,0 +1,34 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +namespace brayns::experimental +{ +template<> +struct JsonReflector +{ + static JsonSchema getSchema(); + static JsonValue serialize(const JsonSchema &schema); +}; +} diff --git a/tests/core/jsonv2/TestJsonSchema.cpp b/tests/core/jsonv2/TestJsonSchema.cpp index d1e9092a5..c438887ce 100644 --- a/tests/core/jsonv2/TestJsonSchema.cpp +++ b/tests/core/jsonv2/TestJsonSchema.cpp @@ -256,4 +256,26 @@ TEST_CASE("JsonSchema") CHECK_EQ(toString(errors[1].path), "test2.test3[0]"); CHECK_EQ(toString(errors[1].error), "Invalid type: expected integer got number"); } + SUBCASE("Serialize") + { + auto schema = getJsonSchema(); + auto json = stringifyToJson(schema); + auto ref = R"({"type":"string"})"; + CHECK_EQ(json, ref); + + schema = getJsonSchema>(); + json = stringifyToJson(schema); + ref = R"({"items":{"type":"string"},"type":"array"})"; + CHECK_EQ(json, ref); + + schema = getJsonSchema>(); + json = stringifyToJson(schema); + ref = R"({"additionalProperties":{"type":"boolean"},"type":"object"})"; + CHECK_EQ(json, ref); + + schema = getJsonSchema>(); + json = stringifyToJson(schema); + ref = R"({"oneOf":[{"type":"string"},{"type":"boolean"}]})"; + CHECK_EQ(json, ref); + } } From 78f0c1790165f6851f11d4b1551df3e0c5e09c29 Mon Sep 17 00:00:00 2001 From: Adrien4193 Date: Fri, 3 May 2024 10:16:08 +0200 Subject: [PATCH 6/8] Remove singletons. --- src/brayns/core/jsonv2/JsonReflector.h | 5 +- src/brayns/core/jsonv2/JsonSchema.cpp | 22 ++-- src/brayns/core/jsonv2/JsonValidator.cpp | 6 +- src/brayns/core/jsonv2/types/Enums.h | 10 +- src/brayns/core/utils/EnumReflector.h | 141 ++++++++++++----------- tests/core/jsonv2/TestJsonReflection.cpp | 18 ++- 6 files changed, 111 insertions(+), 91 deletions(-) diff --git a/src/brayns/core/jsonv2/JsonReflector.h b/src/brayns/core/jsonv2/JsonReflector.h index bf630255d..78d31834d 100644 --- a/src/brayns/core/jsonv2/JsonReflector.h +++ b/src/brayns/core/jsonv2/JsonReflector.h @@ -51,10 +51,9 @@ struct JsonReflector }; template -const JsonSchema &getJsonSchema() +JsonSchema getJsonSchema() { - static const JsonSchema schema = JsonReflector::getSchema(); - return schema; + return JsonReflector::getSchema(); } template diff --git a/src/brayns/core/jsonv2/JsonSchema.cpp b/src/brayns/core/jsonv2/JsonSchema.cpp index ad5775409..8c72ada8a 100644 --- a/src/brayns/core/jsonv2/JsonSchema.cpp +++ b/src/brayns/core/jsonv2/JsonSchema.cpp @@ -27,16 +27,18 @@ namespace brayns::experimental { EnumInfo EnumReflector::reflect() { - return { - {"undefined", JsonType::Undefined}, - {"null", JsonType::Null}, - {"boolean", JsonType::Boolean}, - {"integer", JsonType::Integer}, - {"number", JsonType::Number}, - {"string", JsonType::String}, - {"array", JsonType::Array}, - {"object", JsonType::Object}, - }; + return EnumInfo( + "JsonType", + { + {"undefined", JsonType::Undefined}, + {"null", JsonType::Null}, + {"boolean", JsonType::Boolean}, + {"integer", JsonType::Integer}, + {"number", JsonType::Number}, + {"string", JsonType::String}, + {"array", JsonType::Array}, + {"object", JsonType::Object}, + }); } JsonType getJsonType(const JsonValue &json) diff --git a/src/brayns/core/jsonv2/JsonValidator.cpp b/src/brayns/core/jsonv2/JsonValidator.cpp index 71573bfd4..98f858567 100644 --- a/src/brayns/core/jsonv2/JsonValidator.cpp +++ b/src/brayns/core/jsonv2/JsonValidator.cpp @@ -31,6 +31,8 @@ namespace { using namespace brayns::experimental; +const EnumInfo jsonTypeInfo = reflectEnum(); + class ErrorContext { public: @@ -273,8 +275,8 @@ std::string toString(const JsonPath &path) std::string toString(const InvalidType &error) { - const auto &type = getEnumName(error.type); - const auto &expected = getEnumName(error.expected); + const auto &type = jsonTypeInfo.getName(error.type); + const auto &expected = jsonTypeInfo.getName(error.expected); return fmt::format("Invalid type: expected {} got {}", expected, type); } diff --git a/src/brayns/core/jsonv2/types/Enums.h b/src/brayns/core/jsonv2/types/Enums.h index e76638ff3..0748bd8b3 100644 --- a/src/brayns/core/jsonv2/types/Enums.h +++ b/src/brayns/core/jsonv2/types/Enums.h @@ -38,14 +38,15 @@ struct JsonReflector static JsonSchema getSchema() { return { + .title = _info.getName(), .type = JsonType::String, - .enums = getEnumNames(), + .enums = _info.getNames(), }; } static JsonValue serialize(const T &value) { - return getEnumName(value); + return _info.getName(value); } static T deserialize(const JsonValue &json) @@ -53,12 +54,15 @@ struct JsonReflector auto name = deserializeJson(json); try { - return getEnumValue(name); + return _info.getValue(name); } catch (const std::exception &e) { throw JsonException(e.what()); } } + +private: + static inline const EnumInfo _info = reflectEnum(); }; } diff --git a/src/brayns/core/utils/EnumReflector.h b/src/brayns/core/utils/EnumReflector.h index cb39dd38c..cf25b59d6 100644 --- a/src/brayns/core/utils/EnumReflector.h +++ b/src/brayns/core/utils/EnumReflector.h @@ -23,7 +23,6 @@ #include #include -#include #include #include @@ -31,102 +30,108 @@ namespace brayns::experimental { template -using EnumInfo = std::vector>; - -template -struct EnumReflector +class EnumInfo { - template - static constexpr auto alwaysFalse = false; +public: + explicit EnumInfo(std::string name, std::vector> mapping): + _name(std::move(name)), + _mapping(std::move(mapping)) + { + } - static_assert(alwaysFalse, "Please specialize EnumReflector"); + const std::string &getName() const + { + return _name; + } - static EnumInfo reflect() + std::vector getNames() const { - return {}; + auto names = std::vector(); + names.reserve(_mapping.size()); + for (const auto &[name, value] : _mapping) + { + names.push_back(name); + } + return names; } -}; -template -const EnumInfo &reflectEnum() -{ - static const EnumInfo info = EnumReflector::reflect(); - return info; -} + std::vector getValues() const + { + auto values = std::vector(); + values.reserve(values.size()); + for (const auto &[name, value] : _mapping) + { + values.push_back(value); + } + return values; + } -template -std::vector getEnumNames() -{ - const auto &info = reflectEnum(); - std::vector names; - names.reserve(info.size()); - for (const auto &[name, value] : info) + const std::string *findName(const T &value) const { - names.push_back(name); + for (const auto &[key, item] : _mapping) + { + if (item == value) + { + return &key; + } + } + return nullptr; } - return names; -} -template -static std::vector getEnumValues() -{ - const auto &info = reflectEnum(); - std::vector values; - values.reserve(values.size()); - for (const auto &[name, value] : info) + const T *findValue(const std::string &name) const { - values.push_back(value); + for (const auto &[key, item] : _mapping) + { + if (key == name) + { + return &item; + } + } + return nullptr; } - return values; -} -template -static const std::string *findEnumName(const T &value) -{ - const auto &info = reflectEnum(); - for (const auto &[key, item] : info) + const std::string &getName(const T &value) const { - if (item == value) + const auto *name = findName(value); + if (name) { - return &key; + return *name; } + throw std::invalid_argument(fmt::format("Invalid enum value: {}", int(value))); } - return nullptr; -} -template -static const T *findEnumValue(const std::string &name) -{ - const auto &info = reflectEnum(); - for (const auto &[key, item] : info) + const T &getValue(const std::string &name) const { - if (key == name) + const auto *value = findValue(name); + if (value) { - return &item; + return *value; } + throw std::invalid_argument(fmt::format("Invalid enum name '{}'", name)); } - return nullptr; -} + +private: + std::string _name; + std::vector> _mapping; +}; template -const std::string &getEnumName(const T &value) +struct EnumReflector { - const auto *name = findEnumName(value); - if (name) + template + static constexpr auto alwaysFalse = false; + + static_assert(alwaysFalse, "Please specialize EnumReflector"); + + static EnumInfo reflect() { - return *name; + throw std::runtime_error("Not implemented"); } - throw std::invalid_argument(fmt::format("Invalid enum value: {}", int(value))); -} +}; template -const T &getEnumValue(const std::string &name) +EnumInfo reflectEnum() { - const auto *value = findEnumValue(name); - if (value) - { - return *value; - } - throw std::invalid_argument(fmt::format("Invalid enum name '{}'", name)); + return EnumReflector::reflect(); } } diff --git a/tests/core/jsonv2/TestJsonReflection.cpp b/tests/core/jsonv2/TestJsonReflection.cpp index f1805689a..c658f72f5 100644 --- a/tests/core/jsonv2/TestJsonReflection.cpp +++ b/tests/core/jsonv2/TestJsonReflection.cpp @@ -38,10 +38,12 @@ struct EnumReflector { static EnumInfo reflect() { - return { - {"value1", SomeEnum::Value1}, - {"value2", SomeEnum::Value2}, - }; + return EnumInfo( + "SomeEnum", + { + {"value1", SomeEnum::Value1}, + {"value2", SomeEnum::Value2}, + }); } }; } @@ -99,7 +101,13 @@ TEST_CASE("JsonReflection") } SUBCASE("Enum") { - CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::String, .enums = {"value1", "value2"}}); + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .title = "SomeEnum", + .type = JsonType::String, + .enums = {"value1", "value2"}, + }); CHECK_EQ(deserializeJson("value1"), SomeEnum::Value1); CHECK_EQ(serializeToJson(SomeEnum::Value2), JsonValue("value2")); CHECK_THROWS_AS(deserializeJson(1), JsonException); From bdc9aa45e89aef75fd5301c77386e43b33543538 Mon Sep 17 00:00:00 2001 From: Adrien4193 Date: Mon, 6 May 2024 11:07:18 +0200 Subject: [PATCH 7/8] Checkpoint new structure. --- src/brayns/core/jsonv2/Json.h | 1 + src/brayns/core/jsonv2/JsonReflector.h | 6 +- src/brayns/core/jsonv2/JsonSchema.cpp | 22 +-- src/brayns/core/jsonv2/JsonSchema.h | 7 +- src/brayns/core/jsonv2/JsonValidator.cpp | 32 ++-- src/brayns/core/jsonv2/JsonValidator.h | 19 +- src/brayns/core/jsonv2/JsonValue.cpp | 2 +- src/brayns/core/jsonv2/JsonValue.h | 3 +- src/brayns/core/jsonv2/types/Arrays.h | 2 +- src/brayns/core/jsonv2/types/Enums.h | 37 ++-- src/brayns/core/jsonv2/types/Maps.h | 2 +- src/brayns/core/jsonv2/types/Math.h | 40 ++-- src/brayns/core/jsonv2/types/Objects.h | 229 +++++++++++++++++++++++ src/brayns/core/jsonv2/types/Schema.cpp | 12 +- src/brayns/core/jsonv2/types/Variants.h | 4 +- src/brayns/core/utils/EnumReflector.h | 163 +++++++++------- tests/core/jsonv2/TestJsonReflection.cpp | 66 ++++--- tests/core/jsonv2/TestJsonSchema.cpp | 77 ++++---- 18 files changed, 494 insertions(+), 230 deletions(-) create mode 100644 src/brayns/core/jsonv2/types/Objects.h diff --git a/src/brayns/core/jsonv2/Json.h b/src/brayns/core/jsonv2/Json.h index 404743202..cea00a76a 100644 --- a/src/brayns/core/jsonv2/Json.h +++ b/src/brayns/core/jsonv2/Json.h @@ -30,6 +30,7 @@ #include "types/Enums.h" #include "types/Maps.h" #include "types/Math.h" +#include "types/Objects.h" #include "types/Primitives.h" #include "types/Schema.h" #include "types/Variants.h" diff --git a/src/brayns/core/jsonv2/JsonReflector.h b/src/brayns/core/jsonv2/JsonReflector.h index 78d31834d..d4bdcb724 100644 --- a/src/brayns/core/jsonv2/JsonReflector.h +++ b/src/brayns/core/jsonv2/JsonReflector.h @@ -63,7 +63,7 @@ JsonValue serializeToJson(const T &value) } template -T deserializeJson(const JsonValue &json) +T deserializeAs(const JsonValue &json) { return JsonReflector::deserialize(json); } @@ -72,13 +72,13 @@ template std::string stringifyToJson(const T &value) { auto json = serializeToJson(value); - return stringifyToJson(json); + return stringify(json); } template T parseJson(const std::string &data) { auto json = parseJson(data); - return deserializeJson(json); + return deserializeAs(json); } } diff --git a/src/brayns/core/jsonv2/JsonSchema.cpp b/src/brayns/core/jsonv2/JsonSchema.cpp index 8c72ada8a..65d80e96a 100644 --- a/src/brayns/core/jsonv2/JsonSchema.cpp +++ b/src/brayns/core/jsonv2/JsonSchema.cpp @@ -27,18 +27,16 @@ namespace brayns::experimental { EnumInfo EnumReflector::reflect() { - return EnumInfo( - "JsonType", - { - {"undefined", JsonType::Undefined}, - {"null", JsonType::Null}, - {"boolean", JsonType::Boolean}, - {"integer", JsonType::Integer}, - {"number", JsonType::Number}, - {"string", JsonType::String}, - {"array", JsonType::Array}, - {"object", JsonType::Object}, - }); + auto builder = EnumInfoBuilder(); + builder.field("undefined", JsonType::Undefined); + builder.field("null", JsonType::Null); + builder.field("boolean", JsonType::Boolean); + builder.field("integer", JsonType::Integer); + builder.field("number", JsonType::Number); + builder.field("string", JsonType::String); + builder.field("array", JsonType::Array); + builder.field("object", JsonType::Object); + return builder.build(); } JsonType getJsonType(const JsonValue &json) diff --git a/src/brayns/core/jsonv2/JsonSchema.h b/src/brayns/core/jsonv2/JsonSchema.h index fd2122a17..1f4c43de4 100644 --- a/src/brayns/core/jsonv2/JsonSchema.h +++ b/src/brayns/core/jsonv2/JsonSchema.h @@ -148,19 +148,18 @@ void throwIfNotCompatible(const JsonValue &json) struct JsonSchema { - std::string title = {}; std::string description = {}; - bool required = false; + bool required = true; JsonValue defaultValue = {}; + std::vector oneOf = {}; JsonType type = JsonType::Undefined; + std::string constant = {}; std::optional minimum = {}; std::optional maximum = {}; std::vector items = {}; std::optional minItems = {}; std::optional maxItems = {}; std::map properties = {}; - std::vector enums = {}; - std::vector oneOf = {}; auto operator<=>(const JsonSchema &) const = default; }; diff --git a/src/brayns/core/jsonv2/JsonValidator.cpp b/src/brayns/core/jsonv2/JsonValidator.cpp index 98f858567..d0ce18fa6 100644 --- a/src/brayns/core/jsonv2/JsonValidator.cpp +++ b/src/brayns/core/jsonv2/JsonValidator.cpp @@ -31,8 +31,6 @@ namespace { using namespace brayns::experimental; -const EnumInfo jsonTypeInfo = reflectEnum(); - class ErrorContext { public: @@ -67,7 +65,7 @@ void checkOneOf(const JsonValue &json, const JsonSchema &schema, ErrorContext &e { for (const auto &oneof : schema.oneOf) { - auto suberrors = validateJsonSchema(json, oneof); + auto suberrors = validate(json, oneof); if (suberrors.empty()) { return; @@ -88,14 +86,12 @@ bool checkType(const JsonValue &json, const JsonSchema &schema, ErrorContext &er return false; } -void checkEnum(const std::string &value, const JsonSchema &schema, ErrorContext &errors) +void checkConst(const std::string &value, const JsonSchema &schema, ErrorContext &errors) { - auto i = std::ranges::find(schema.enums, value); - if (i != schema.enums.end()) + if (value != schema.constant) { - return; + errors.add(InvalidConst{value, schema.constant}); } - errors.add(InvalidEnum{value}); } void checkRange(double value, const JsonSchema &schema, ErrorContext &errors) @@ -214,10 +210,10 @@ void check(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors { return; } - if (!schema.enums.empty()) + if (!schema.constant.empty()) { const auto &value = json.extract(); - checkEnum(value, schema, errors); + checkConst(value, schema, errors); return; } if (isNumeric(schema.type)) @@ -275,11 +271,16 @@ std::string toString(const JsonPath &path) std::string toString(const InvalidType &error) { - const auto &type = jsonTypeInfo.getName(error.type); - const auto &expected = jsonTypeInfo.getName(error.expected); + auto type = getEnumName(error.type); + auto expected = getEnumName(error.expected); return fmt::format("Invalid type: expected {} got {}", expected, type); } +std::string toString(const InvalidConst &error) +{ + return fmt::format("Invalid const: expected '{}' got '{}'", error.expected, error.value); +} + std::string toString(const BelowMinimum &error) { return fmt::format("Value below minimum: {} < {}", error.value, error.minimum); @@ -310,11 +311,6 @@ std::string toString(const UnknownProperty &error) return fmt::format("Unknown property: '{}'", error.name); } -std::string toString(const InvalidEnum &error) -{ - return fmt::format("Invalid enum: '{}'", error.name); -} - std::string toString(const InvalidOneOf &) { return "Invalid oneOf"; @@ -325,7 +321,7 @@ std::string toString(const JsonError &error) return std::visit([](const auto &value) { return toString(value); }, error); } -std::vector validateJsonSchema(const JsonValue &json, const JsonSchema &schema) +std::vector validate(const JsonValue &json, const JsonSchema &schema) { auto errors = ErrorContext(); check(json, schema, errors); diff --git a/src/brayns/core/jsonv2/JsonValidator.h b/src/brayns/core/jsonv2/JsonValidator.h index 9be384c5c..86626925d 100644 --- a/src/brayns/core/jsonv2/JsonValidator.h +++ b/src/brayns/core/jsonv2/JsonValidator.h @@ -43,6 +43,14 @@ struct InvalidType std::string toString(const InvalidType &error); +struct InvalidConst +{ + std::string value; + std::string expected; +}; + +std::string toString(const InvalidConst &error); + struct BelowMinimum { double value; @@ -89,13 +97,6 @@ struct UnknownProperty std::string toString(const UnknownProperty &error); -struct InvalidEnum -{ - std::string name; -}; - -std::string toString(const InvalidEnum &error); - struct InvalidOneOf { }; @@ -104,13 +105,13 @@ std::string toString(const InvalidOneOf &error); using JsonError = std::variant< InvalidType, + InvalidConst, AboveMaximum, BelowMinimum, TooManyItems, NotEnoughItems, MissingRequiredProperty, UnknownProperty, - InvalidEnum, InvalidOneOf>; std::string toString(const JsonError &error); @@ -121,5 +122,5 @@ struct JsonSchemaError JsonError error; }; -std::vector validateJsonSchema(const JsonValue &json, const JsonSchema &schema); +std::vector validate(const JsonValue &json, const JsonSchema &schema); } diff --git a/src/brayns/core/jsonv2/JsonValue.cpp b/src/brayns/core/jsonv2/JsonValue.cpp index ee3a43215..1476df0ac 100644 --- a/src/brayns/core/jsonv2/JsonValue.cpp +++ b/src/brayns/core/jsonv2/JsonValue.cpp @@ -72,7 +72,7 @@ const JsonObject &getObject(const JsonValue &json) } } -std::string stringifyToJson(const JsonValue &json) +std::string stringify(const JsonValue &json) { auto stream = std::ostringstream(); Poco::JSON::Stringifier::condense(json, stream); diff --git a/src/brayns/core/jsonv2/JsonValue.h b/src/brayns/core/jsonv2/JsonValue.h index 813d862d0..2b7a1111f 100644 --- a/src/brayns/core/jsonv2/JsonValue.h +++ b/src/brayns/core/jsonv2/JsonValue.h @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -52,6 +53,6 @@ bool isArray(const JsonValue &json); bool isObject(const JsonValue &json); const JsonArray &getArray(const JsonValue &json); const JsonObject &getObject(const JsonValue &json); -std::string stringifyToJson(const JsonValue &json); +std::string stringify(const JsonValue &json); JsonValue parseJson(const std::string &data); } diff --git a/src/brayns/core/jsonv2/types/Arrays.h b/src/brayns/core/jsonv2/types/Arrays.h index 2d61cf81e..3bbca59b0 100644 --- a/src/brayns/core/jsonv2/types/Arrays.h +++ b/src/brayns/core/jsonv2/types/Arrays.h @@ -59,7 +59,7 @@ struct JsonArrayReflector auto value = T(); for (const auto &jsonItem : array) { - auto item = deserializeJson(jsonItem); + auto item = deserializeAs(jsonItem); value.push_back(std::move(item)); } return value; diff --git a/src/brayns/core/jsonv2/types/Enums.h b/src/brayns/core/jsonv2/types/Enums.h index 0748bd8b3..dabd175c5 100644 --- a/src/brayns/core/jsonv2/types/Enums.h +++ b/src/brayns/core/jsonv2/types/Enums.h @@ -21,7 +21,7 @@ #pragma once -#include +#include #include @@ -29,40 +29,45 @@ namespace brayns::experimental { -template -concept Enum = std::is_enum_v; - -template +template struct JsonReflector { static JsonSchema getSchema() { - return { - .title = _info.getName(), - .type = JsonType::String, - .enums = _info.getNames(), - }; + const auto &fields = getEnumFields(); + + auto oneOf = std::vector(); + oneOf.reserve(fields.size()); + + for (const auto &field : fields) + { + oneOf.push_back({ + .description = field.description, + .type = JsonType::String, + .constant = field.name, + }); + } + + return {.oneOf = std::move(oneOf)}; } static JsonValue serialize(const T &value) { - return _info.getName(value); + return getEnumName(value); } static T deserialize(const JsonValue &json) { - auto name = deserializeJson(json); + auto name = deserializeAs(json); + try { - return _info.getValue(name); + return getEnumValue(name); } catch (const std::exception &e) { throw JsonException(e.what()); } } - -private: - static inline const EnumInfo _info = reflectEnum(); }; } diff --git a/src/brayns/core/jsonv2/types/Maps.h b/src/brayns/core/jsonv2/types/Maps.h index f1e0c3d4f..5399fe936 100644 --- a/src/brayns/core/jsonv2/types/Maps.h +++ b/src/brayns/core/jsonv2/types/Maps.h @@ -59,7 +59,7 @@ struct JsonMapReflector auto value = T(); for (const auto &[key, jsonItem] : object) { - value[key] = deserializeJson(jsonItem); + value[key] = deserializeAs(jsonItem); } return value; } diff --git a/src/brayns/core/jsonv2/types/Math.h b/src/brayns/core/jsonv2/types/Math.h index c39968706..e1489427f 100644 --- a/src/brayns/core/jsonv2/types/Math.h +++ b/src/brayns/core/jsonv2/types/Math.h @@ -27,24 +27,6 @@ namespace brayns::experimental { -template -struct StaticJsonArray -{ - static auto &getItem(auto &value, std::size_t index) - { - return value[index]; - } -}; - -template<> -struct StaticJsonArray -{ - static auto &getItem(auto &value, std::size_t index) - { - return (&value.i)[index]; - } -}; - template struct JsonMathReflector { @@ -67,7 +49,7 @@ struct JsonMathReflector auto array = createJsonArray(); for (auto i = std::size_t(0); i < itemCount; ++i) { - const auto &item = StaticJsonArray::getItem(value, i); + const auto &item = getItem(value, i); auto jsonItem = serializeToJson(item); array->add(jsonItem); } @@ -85,12 +67,28 @@ struct JsonMathReflector auto i = std::size_t(0); for (const auto &jsonItem : array) { - auto &item = StaticJsonArray::getItem(value, i); - item = deserializeJson(jsonItem); + auto &item = getItem(value, i); + item = deserializeAs(jsonItem); ++i; } return value; } + +private: + static auto &getItem(auto &value, std::size_t index) + { + return value[index]; + } + + static auto &getItem(const Quaternion &value, std::size_t index) + { + return (&value.i)[index]; + } + + static auto &getItem(Quaternion &value, std::size_t index) + { + return (&value.i)[index]; + } }; template diff --git a/src/brayns/core/jsonv2/types/Objects.h b/src/brayns/core/jsonv2/types/Objects.h new file mode 100644 index 000000000..30683af1e --- /dev/null +++ b/src/brayns/core/jsonv2/types/Objects.h @@ -0,0 +1,229 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace brayns::experimental +{ +template +struct JsonField +{ + std::string name; + JsonSchema schema; + std::function serialize; + std::function deserialize; +}; + +template +class JsonObjectInfo +{ +public: + explicit JsonObjectInfo(std::vector> fields): + _fields(std::move(fields)) + { + } + + JsonSchema getSchema() const + { + auto schema = JsonSchema{.type = JsonType::Object}; + for (const auto &field : _fields) + { + schema.properties[field.name] = field.schema; + } + return schema; + } + + JsonValue serialize(const T &value) const + { + auto object = createJsonObject(); + for (const auto &field : _fields) + { + auto jsonItem = field.serialize(value); + if (jsonItem.isEmpty() && !field.schema.required) + { + continue; + } + object->set(field.name, jsonItem); + } + return object; + } + + T deserialize(const JsonValue &json) const + { + auto value = T{}; + + const auto &object = getObject(json); + + for (const auto &field : _fields) + { + auto jsonItem = object.get(field.name); + + if (!jsonItem.isEmpty()) + { + field.deserialize(jsonItem, value); + continue; + } + + if (field.schema.required) + { + throw JsonException("Missing required field"); + } + + field.deserialize(field.schema.defaultValue, value); + } + } + +private: + std::vector> _fields; +}; + +template +struct JsonObjectReflector; + +template +concept ReflectedJsonObject = std::same_as::reflect()), JsonObjectInfo>; + +template +const JsonObjectInfo &reflectJsonObject() +{ + static const auto info = JsonObjectReflector::reflect(); + return info; +} + +template +struct JsonReflector +{ + static JsonSchema getSchema() + { + const auto &info = reflectJsonObject(); + return info.getSchema(); + } + + static JsonValue serialize(const T &value) + { + const auto &info = reflectJsonObject(); + return info.serialize(value); + } + + static T deserialize(const JsonValue &json) + { + const auto &info = reflectJsonObject(); + return info.deserialize(json); + } +}; + +template +class JsonFieldBuilder +{ +public: + explicit JsonFieldBuilder(JsonField &field): + _field(&field) + { + } + + JsonFieldBuilder description(std::string value) + { + _field->schema.description = std::move(value); + return *this; + } + + JsonFieldBuilder required(bool value) + { + _field->schema.required = value; + return *this; + } + + JsonFieldBuilder minimum(std::optional value) + { + _field->schema.minimum = value; + return *this; + } + + JsonFieldBuilder maximum(std::optional value) + { + _field->schema.maximum = value; + return *this; + } + + JsonFieldBuilder minItems(std::optional value) + { + _field->schema.minItems = value; + return *this; + } + + JsonFieldBuilder maxItems(std::optional value) + { + _field->schema.maxItems = value; + return *this; + } + + template + JsonFieldBuilder defaultValue(const T &value) + { + _field->schema.defaultValue = serializeToJson(value); + _field->schema.required = false; + return *this; + } + + JsonFieldBuilder defaultValue(const char *value) + { + return defaultValue(std::string(value)); + } + +private: + JsonField *_field; +}; + +template +class JsonObjectInfoBuilder +{ +public: + JsonFieldBuilder field(std::string name, auto &&getter) + { + using FieldType = std::decay_t()))>; + + auto &field = _fields.emplace_back(); + + field.name = std::move(name); + field.schema = getJsonSchema(); + field.serialize = [=](const auto &object) { return serializeToJson(getter(object)); }; + field.deserialize = [=](const auto &json, auto &object) { getter(object) = deserializeAs(json); }; + + return JsonFieldBuilder(field); + } + + JsonObjectInfo build() + { + return JsonObjectInfo(std::exchange(_fields, {})); + } + +private: + std::vector> _fields; +}; +} diff --git a/src/brayns/core/jsonv2/types/Schema.cpp b/src/brayns/core/jsonv2/types/Schema.cpp index a8333a2c1..49c47c870 100644 --- a/src/brayns/core/jsonv2/types/Schema.cpp +++ b/src/brayns/core/jsonv2/types/Schema.cpp @@ -124,7 +124,6 @@ namespace brayns::experimental JsonSchema JsonReflector::getSchema() { return JsonSchema{ - .title = "JsonSchema", .type = JsonType::Object, .items = {JsonSchema()}, }; @@ -134,17 +133,12 @@ JsonValue JsonReflector::serialize(const JsonSchema &schema) { auto object = createJsonObject(); - if (!schema.title.empty()) - { - set(*object, "title", schema.title); - } - if (!schema.description.empty()) { set(*object, "description", schema.description); } - if (schema.required) + if (!schema.required) { set(*object, "default", schema.defaultValue); } @@ -160,9 +154,9 @@ JsonValue JsonReflector::serialize(const JsonSchema &schema) set(*object, "type", schema.type); } - if (!schema.enums.empty()) + if (!schema.constant.empty()) { - set(*object, "enum", schema.enums); + set(*object, "const", schema.constant); return object; } diff --git a/src/brayns/core/jsonv2/types/Variants.h b/src/brayns/core/jsonv2/types/Variants.h index fd46c51a5..0823526ab 100644 --- a/src/brayns/core/jsonv2/types/Variants.h +++ b/src/brayns/core/jsonv2/types/Variants.h @@ -54,7 +54,7 @@ struct JsonReflector> { return std::nullopt; } - return deserializeJson(json); + return deserializeAs(json); } }; @@ -84,7 +84,7 @@ struct JsonReflector> { try { - return deserializeJson(json); + return deserializeAs(json); } catch (...) { diff --git a/src/brayns/core/utils/EnumReflector.h b/src/brayns/core/utils/EnumReflector.h index cf25b59d6..d24a0f7ab 100644 --- a/src/brayns/core/utils/EnumReflector.h +++ b/src/brayns/core/utils/EnumReflector.h @@ -21,117 +21,152 @@ #pragma once +#include +#include #include #include +#include +#include +#include #include #include namespace brayns::experimental { +template +struct EnumField +{ + std::string name; + T value; + std::string description; +}; + template class EnumInfo { public: - explicit EnumInfo(std::string name, std::vector> mapping): - _name(std::move(name)), - _mapping(std::move(mapping)) + explicit EnumInfo(std::vector> fields): + _fields(std::move(fields)) { } - const std::string &getName() const + const std::vector> &getFields() const { - return _name; + return _fields; } - std::vector getNames() const + const EnumField *findByName(std::string_view name) const { - auto names = std::vector(); - names.reserve(_mapping.size()); - for (const auto &[name, value] : _mapping) - { - names.push_back(name); - } - return names; + auto sameName = [&](const auto &field) { return field.name == name; }; + auto i = std::ranges::find_if(_fields, sameName); + return i == _fields.end() ? nullptr : &*i; } - std::vector getValues() const + const EnumField *findByValue(T value) const { - auto values = std::vector(); - values.reserve(values.size()); - for (const auto &[name, value] : _mapping) - { - values.push_back(value); - } - return values; + auto sameValue = [&](const auto &field) { return field.value == value; }; + auto i = std::ranges::find_if(_fields, sameValue); + return i == _fields.end() ? nullptr : &*i; } - const std::string *findName(const T &value) const + const EnumField &getByName(std::string_view name) const { - for (const auto &[key, item] : _mapping) + const auto *field = findByName(name); + if (field) { - if (item == value) - { - return &key; - } + return *field; } - return nullptr; + throw std::invalid_argument(fmt::format("Invalid enum name: '{}'", name)); } - const T *findValue(const std::string &name) const + const EnumField &getByValue(T value) const { - for (const auto &[key, item] : _mapping) + const auto *field = findByValue(value); + if (field) { - if (key == name) - { - return &item; - } + return *field; } - return nullptr; + throw std::invalid_argument(fmt::format("Invalid enum value: {}", std::underlying_type_t(value))); } - const std::string &getName(const T &value) const +private: + std::vector> _fields; +}; + +template +struct EnumReflector; + +template +concept ReflectedEnum = std::same_as::reflect()), EnumInfo>; + +template +const EnumInfo &reflectEnum() +{ + static const auto info = EnumReflector::reflect(); + return info; +} + +template +const std::vector> &getEnumFields() +{ + const auto &info = reflectEnum(); + return info.getFields(); +} + +template +const std::string &getEnumName(T value) +{ + const auto &info = reflectEnum(); + const auto &field = info.getByValue(value); + return field.name; +} + +template +T getEnumValue(std::string_view name) +{ + const auto &info = reflectEnum(); + const auto &field = info.getByName(name); + return field.value; +} + +template +class EnumFieldBuilder +{ +public: + explicit EnumFieldBuilder(EnumField &field): + _field(&field) { - const auto *name = findName(value); - if (name) - { - return *name; - } - throw std::invalid_argument(fmt::format("Invalid enum value: {}", int(value))); } - const T &getValue(const std::string &name) const + EnumFieldBuilder description(std::string description) { - const auto *value = findValue(name); - if (value) - { - return *value; - } - throw std::invalid_argument(fmt::format("Invalid enum name '{}'", name)); + _field->description = std::move(description); + return *this; } private: - std::string _name; - std::vector> _mapping; + EnumField *_field; }; template -struct EnumReflector +class EnumInfoBuilder { - template - static constexpr auto alwaysFalse = false; - - static_assert(alwaysFalse, "Please specialize EnumReflector"); +public: + EnumFieldBuilder field(std::string name, T value) + { + auto &emplaced = _fields.emplace_back(); + emplaced.name = std::move(name); + emplaced.value = value; + return EnumFieldBuilder(emplaced); + } - static EnumInfo reflect() + EnumInfo build() { - throw std::runtime_error("Not implemented"); + return EnumInfo(std::exchange(_fields, {})); } -}; -template -EnumInfo reflectEnum() -{ - return EnumReflector::reflect(); -} +private: + std::vector> _fields; +}; } diff --git a/tests/core/jsonv2/TestJsonReflection.cpp b/tests/core/jsonv2/TestJsonReflection.cpp index c658f72f5..a27929498 100644 --- a/tests/core/jsonv2/TestJsonReflection.cpp +++ b/tests/core/jsonv2/TestJsonReflection.cpp @@ -38,12 +38,10 @@ struct EnumReflector { static EnumInfo reflect() { - return EnumInfo( - "SomeEnum", - { - {"value1", SomeEnum::Value1}, - {"value2", SomeEnum::Value2}, - }); + auto builder = EnumInfoBuilder(); + builder.field("value1", SomeEnum::Value1).description("Value 1"); + builder.field("value2", SomeEnum::Value2).description("Value 2"); + return builder.build(); } }; } @@ -58,60 +56,68 @@ TEST_CASE("JsonReflection") SUBCASE("Undefined") { CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Undefined}); - CHECK_EQ(deserializeJson(1), JsonValue(1)); + CHECK_EQ(deserializeAs(1), JsonValue(1)); CHECK_EQ(serializeToJson(JsonValue("2")), JsonValue("2")); } SUBCASE("Null") { CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Null}); - deserializeJson({}); + deserializeAs({}); CHECK_EQ(serializeToJson(NullJson()), JsonValue()); - CHECK_THROWS_AS(deserializeJson("xyz"), JsonException); + CHECK_THROWS_AS(deserializeAs("xyz"), JsonException); } SUBCASE("Boolean") { CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Boolean}); - CHECK_EQ(deserializeJson(true), true); + CHECK_EQ(deserializeAs(true), true); CHECK_EQ(serializeToJson(true), JsonValue(true)); - CHECK_THROWS_AS(deserializeJson("xyz"), JsonException); + CHECK_THROWS_AS(deserializeAs("xyz"), JsonException); } SUBCASE("Integer") { CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Integer, .minimum = 0, .maximum = 255}); CHECK_EQ(getJsonSchema().type, JsonType::Integer); CHECK_EQ(getJsonSchema().type, JsonType::Integer); - CHECK_EQ(deserializeJson(1), 1); + CHECK_EQ(deserializeAs(1), 1); CHECK_EQ(serializeToJson(1), JsonValue(1)); - CHECK_THROWS_AS(deserializeJson(1.5), JsonException); + CHECK_THROWS_AS(deserializeAs(1.5), JsonException); } SUBCASE("Number") { CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Number, .minimum = fmin, .maximum = fmax}); CHECK_EQ(getJsonSchema().type, JsonType::Number); - CHECK_EQ(deserializeJson(1), 1.0f); + CHECK_EQ(deserializeAs(1), 1.0f); CHECK_EQ(serializeToJson(1.5f), JsonValue(1.5f)); - CHECK_THROWS_AS(deserializeJson("1.5"), JsonException); + CHECK_THROWS_AS(deserializeAs("1.5"), JsonException); } SUBCASE("String") { CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::String}); - CHECK_EQ(deserializeJson("test"), JsonValue("test")); + CHECK_EQ(deserializeAs("test"), JsonValue("test")); CHECK_EQ(serializeToJson(std::string("test")), JsonValue("test")); - CHECK_THROWS_AS(deserializeJson(1), JsonException); + CHECK_THROWS_AS(deserializeAs(1), JsonException); } SUBCASE("Enum") { CHECK_EQ( getJsonSchema(), JsonSchema{ - .title = "SomeEnum", - .type = JsonType::String, - .enums = {"value1", "value2"}, - }); - CHECK_EQ(deserializeJson("value1"), SomeEnum::Value1); + .oneOf = { + JsonSchema{ + .description = "Value 1", + .type = JsonType::String, + .constant = "value1", + }, + JsonSchema{ + .description = "Value 2", + .type = JsonType::String, + .constant = "value2", + }, + }}); + CHECK_EQ(deserializeAs("value1"), SomeEnum::Value1); CHECK_EQ(serializeToJson(SomeEnum::Value2), JsonValue("value2")); - CHECK_THROWS_AS(deserializeJson(1), JsonException); - CHECK_THROWS_AS(deserializeJson("value3"), JsonException); + CHECK_THROWS_AS(deserializeAs(1), JsonException); + CHECK_THROWS_AS(deserializeAs("value3"), JsonException); } SUBCASE("Array") { @@ -185,9 +191,9 @@ TEST_CASE("JsonReflection") }}); CHECK_EQ(serializeToJson(Variant("test")), JsonValue("test")); CHECK_EQ(serializeToJson(Variant(1)), JsonValue(1)); - CHECK_EQ(deserializeJson(1), Variant(1)); - CHECK_EQ(deserializeJson("test"), Variant("test")); - CHECK_THROWS_AS(deserializeJson(1.5), JsonException); + CHECK_EQ(deserializeAs(1), Variant(1)); + CHECK_EQ(deserializeAs("test"), Variant("test")); + CHECK_THROWS_AS(deserializeAs(1.5), JsonException); CHECK_EQ( getJsonSchema>(), @@ -197,8 +203,8 @@ TEST_CASE("JsonReflection") }); CHECK_EQ(serializeToJson(std::optional("test")), JsonValue("test")); CHECK_EQ(serializeToJson(std::optional()), JsonValue()); - CHECK_EQ(deserializeJson>({}), std::nullopt); - CHECK_EQ(deserializeJson>("test"), std::string("test")); - CHECK_THROWS_AS(deserializeJson>(1.5), JsonException); + CHECK_EQ(deserializeAs>({}), std::nullopt); + CHECK_EQ(deserializeAs>("test"), std::string("test")); + CHECK_THROWS_AS(deserializeAs>(1.5), JsonException); } } diff --git a/tests/core/jsonv2/TestJsonSchema.cpp b/tests/core/jsonv2/TestJsonSchema.cpp index c438887ce..752ec5e23 100644 --- a/tests/core/jsonv2/TestJsonSchema.cpp +++ b/tests/core/jsonv2/TestJsonSchema.cpp @@ -32,24 +32,24 @@ TEST_CASE("JsonSchema") auto schema = JsonSchema(); auto json = parseJson(R"({"test": 10})"); - auto errors = validateJsonSchema(json, schema); + auto errors = validate(json, schema); CHECK(errors.empty()); json = 1; - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK(errors.empty()); } SUBCASE("One of") { auto schema = JsonSchema{.oneOf = {getJsonSchema(), getJsonSchema()}}; - auto errors = validateJsonSchema(1.0f, schema); + auto errors = validate(1.0f, schema); CHECK(errors.empty()); - errors = validateJsonSchema("Test", schema); + errors = validate("Test", schema); CHECK(errors.empty()); - errors = validateJsonSchema(true, schema); + errors = validate(true, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Invalid oneOf"); } @@ -57,12 +57,12 @@ TEST_CASE("JsonSchema") { auto schema = JsonSchema{.type = JsonType::String}; - auto errors = validateJsonSchema(1, schema); + auto errors = validate(1, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Invalid type: expected string got integer"); schema.type = JsonType::Number; - errors = validateJsonSchema(1, schema); + errors = validate(1, schema); CHECK(errors.empty()); } SUBCASE("Limits") @@ -73,48 +73,49 @@ TEST_CASE("JsonSchema") .maximum = 3, }; - auto errors = validateJsonSchema(1, schema); + auto errors = validate(1, schema); CHECK(errors.empty()); - errors = validateJsonSchema(-1, schema); + errors = validate(-1, schema); CHECK(errors.empty()); - errors = validateJsonSchema(3, schema); + errors = validate(3, schema); CHECK(errors.empty()); - errors = validateJsonSchema(-2, schema); + errors = validate(-2, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Value below minimum: -2 < -1"); - errors = validateJsonSchema(4, schema); + errors = validate(4, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Value above maximum: 4 > 3"); schema.minimum = std::nullopt; schema.maximum = std::nullopt; - errors = validateJsonSchema(-8, schema); + errors = validate(-8, schema); CHECK(errors.empty()); - errors = validateJsonSchema(125, schema); + errors = validate(125, schema); CHECK(errors.empty()); } - SUBCASE("Enums") + SUBCASE("Constant") { auto schema = JsonSchema{ .type = JsonType::String, - .enums = {"test1", "test2"}, + .constant = "test", }; - auto errors = validateJsonSchema("test1", schema); + auto errors = validate("test", schema); CHECK(errors.empty()); - errors = validateJsonSchema("test2", schema); - CHECK(errors.empty()); + errors = validate("test1", schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid const: expected 'test' got 'test1'"); - errors = validateJsonSchema("Test2", schema); + errors = validate(1, schema); CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Invalid enum: 'Test2'"); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected string got integer"); } SUBCASE("Property type") { @@ -129,18 +130,18 @@ TEST_CASE("JsonSchema") }}}; auto json = parseJson(R"({"internal": 1})"); - auto errors = validateJsonSchema(json, schema); + auto errors = validate(json, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Invalid type: expected object got integer"); json = parseJson(R"({"internal": {"integer": true}})"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].path), "internal.integer"); CHECK_EQ(toString(errors[0].error), "Invalid type: expected integer got boolean"); json = parseJson(R"({"internal": {"integer": 1}})"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK(errors.empty()); } SUBCASE("Missing property") @@ -155,16 +156,16 @@ TEST_CASE("JsonSchema") }; auto json = parseJson(R"({"integer": 1, "string": "test"})"); - auto errors = validateJsonSchema(json, schema); + auto errors = validate(json, schema); CHECK(errors.empty()); json = parseJson(R"({"string": "test"})"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Missing required property: 'integer'"); json = parseJson(R"({"integer": 1})"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK(errors.empty()); } SUBCASE("Unknown properties") @@ -172,12 +173,12 @@ TEST_CASE("JsonSchema") auto schema = JsonSchema{.type = JsonType::Object}; auto json = parseJson(R"({"something": 1})"); - auto errors = validateJsonSchema(json, schema); + auto errors = validate(json, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Unknown property: 'something'"); json = parseJson(R"({})"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK(errors.empty()); } SUBCASE("Item type") @@ -188,15 +189,15 @@ TEST_CASE("JsonSchema") }; auto json = parseJson(R"([1, 2, 3])"); - auto errors = validateJsonSchema(json, schema); + auto errors = validate(json, schema); CHECK(errors.empty()); json = parseJson(R"([])"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK(errors.empty()); json = parseJson(R"([1, "test", 2])"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].path), "[1]"); CHECK_EQ(toString(errors[0].error), "Invalid type: expected integer got string"); @@ -211,20 +212,20 @@ TEST_CASE("JsonSchema") }; auto json = parseJson(R"([1])"); - auto errors = validateJsonSchema(json, schema); + auto errors = validate(json, schema); CHECK(errors.empty()); json = parseJson(R"([1, 2, 3])"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK(errors.empty()); json = parseJson(R"([])"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Not enough items: 0 < 1"); json = parseJson(R"([1, 2, 3, 4])"); - errors = validateJsonSchema(json, schema); + errors = validate(json, schema); CHECK_EQ(errors.size(), 1); CHECK_EQ(toString(errors[0].error), "Too many items: 4 > 3"); } @@ -247,7 +248,7 @@ TEST_CASE("JsonSchema") }}; auto json = parseJson(R"({"test2": {"test3": [1.3]}})"); - auto errors = validateJsonSchema(json, schema); + auto errors = validate(json, schema); CHECK_EQ(errors.size(), 2); CHECK_EQ(toString(errors[0].path), ""); @@ -256,7 +257,7 @@ TEST_CASE("JsonSchema") CHECK_EQ(toString(errors[1].path), "test2.test3[0]"); CHECK_EQ(toString(errors[1].error), "Invalid type: expected integer got number"); } - SUBCASE("Serialize") + SUBCASE("Schema as JSON") { auto schema = getJsonSchema(); auto json = stringifyToJson(schema); From b2f28010eae8072a7d71ac32977decbf37642a20 Mon Sep 17 00:00:00 2001 From: Adrien4193 Date: Mon, 6 May 2024 12:12:02 +0200 Subject: [PATCH 8/8] Working + tests. --- src/brayns/core/jsonv2/JsonValue.cpp | 2 +- src/brayns/core/jsonv2/types/Objects.h | 20 ++-- tests/core/jsonv2/TestJsonReflection.cpp | 111 ++++++++++++++++++++++- tests/core/jsonv2/TestJsonSchema.cpp | 4 +- 4 files changed, 122 insertions(+), 15 deletions(-) diff --git a/src/brayns/core/jsonv2/JsonValue.cpp b/src/brayns/core/jsonv2/JsonValue.cpp index 1476df0ac..e4e534ab3 100644 --- a/src/brayns/core/jsonv2/JsonValue.cpp +++ b/src/brayns/core/jsonv2/JsonValue.cpp @@ -75,7 +75,7 @@ const JsonObject &getObject(const JsonValue &json) std::string stringify(const JsonValue &json) { auto stream = std::ostringstream(); - Poco::JSON::Stringifier::condense(json, stream); + Poco::JSON::Stringifier::condense(json, stream, 0); return stream.str(); } diff --git a/src/brayns/core/jsonv2/types/Objects.h b/src/brayns/core/jsonv2/types/Objects.h index 30683af1e..33882e2a0 100644 --- a/src/brayns/core/jsonv2/types/Objects.h +++ b/src/brayns/core/jsonv2/types/Objects.h @@ -97,6 +97,8 @@ class JsonObjectInfo field.deserialize(field.schema.defaultValue, value); } + + return value; } private: @@ -153,12 +155,6 @@ class JsonFieldBuilder return *this; } - JsonFieldBuilder required(bool value) - { - _field->schema.required = value; - return *this; - } - JsonFieldBuilder minimum(std::optional value) { _field->schema.minimum = value; @@ -204,16 +200,20 @@ template class JsonObjectInfoBuilder { public: - JsonFieldBuilder field(std::string name, auto &&getter) + JsonFieldBuilder field(std::string name, auto &&getPtr) { - using FieldType = std::decay_t()))>; + using FieldPtr = decltype(getPtr(std::declval())); + + static_assert(std::is_pointer_v, "getPtr must return a pointer to the object field"); + + using FieldType = std::decay_t>; auto &field = _fields.emplace_back(); field.name = std::move(name); field.schema = getJsonSchema(); - field.serialize = [=](const auto &object) { return serializeToJson(getter(object)); }; - field.deserialize = [=](const auto &json, auto &object) { getter(object) = deserializeAs(json); }; + field.serialize = [=](const auto &object) { return serializeToJson(*getPtr(object)); }; + field.deserialize = [=](const auto &json, auto &object) { *getPtr(object) = deserializeAs(json); }; return JsonFieldBuilder(field); } diff --git a/tests/core/jsonv2/TestJsonReflection.cpp b/tests/core/jsonv2/TestJsonReflection.cpp index a27929498..8c71f4604 100644 --- a/tests/core/jsonv2/TestJsonReflection.cpp +++ b/tests/core/jsonv2/TestJsonReflection.cpp @@ -25,14 +25,14 @@ using namespace brayns; using namespace brayns::experimental; +namespace brayns::experimental +{ enum class SomeEnum { Value1, Value2, }; -namespace brayns::experimental -{ template<> struct EnumReflector { @@ -44,6 +44,52 @@ struct EnumReflector return builder.build(); } }; + +struct Internal +{ + int value; +}; + +template<> +struct JsonObjectReflector +{ + static JsonObjectInfo reflect() + { + auto builder = JsonObjectInfoBuilder(); + builder.field("value", [](auto &object) { return &object.value; }); + return builder.build(); + } +}; + +struct SomeObject +{ + bool required; + int bounded; + bool description; + std::string withDefault; + std::optional optional; + SomeEnum someEnum = SomeEnum::Value1; + std::vector array; + Internal internal; +}; + +template<> +struct JsonObjectReflector +{ + static JsonObjectInfo reflect() + { + auto builder = JsonObjectInfoBuilder(); + builder.field("required", [](auto &object) { return &object.required; }); + builder.field("bounded", [](auto &object) { return &object.bounded; }).minimum(1).maximum(3); + builder.field("description", [](auto &object) { return &object.description; }).description("Test"); + builder.field("default", [](auto &object) { return &object.withDefault; }).defaultValue("test"); + builder.field("optional", [](auto &object) { return &object.optional; }); + builder.field("enum", [](auto &object) { return &object.someEnum; }); + builder.field("array", [](auto &object) { return &object.array; }).minItems(1).maxItems(3); + builder.field("internal", [](auto &object) { return &object.internal; }); + return builder.build(); + } +}; } TEST_CASE("JsonReflection") @@ -207,4 +253,65 @@ TEST_CASE("JsonReflection") CHECK_EQ(deserializeAs>("test"), std::string("test")); CHECK_THROWS_AS(deserializeAs>(1.5), JsonException); } + SUBCASE("Object") + { + auto schema = getJsonSchema(); + + CHECK_EQ(schema.type, JsonType::Object); + + const auto &properties = schema.properties; + + CHECK_EQ(properties.at("required"), getJsonSchema()); + CHECK_EQ(properties.at("bounded"), JsonSchema{.type = JsonType::Integer, .minimum = 1, .maximum = 3}); + CHECK_EQ(properties.at("description"), JsonSchema{.description = "Test", .type = JsonType::Boolean}); + CHECK_EQ( + properties.at("default"), + JsonSchema{.required = false, .defaultValue = "test", .type = JsonType::String}); + CHECK_EQ(properties.at("optional"), getJsonSchema>()); + CHECK_EQ(properties.at("enum"), getJsonSchema()); + CHECK_EQ( + properties.at("array"), + JsonSchema{ + .type = JsonType::Array, + .items = {getJsonSchema()}, + .minItems = 1, + .maxItems = 3, + }); + CHECK_EQ( + properties.at("internal"), + JsonSchema{ + .type = JsonType::Object, + .properties = {{"value", getJsonSchema()}}, + }); + + auto internal = createJsonObject(); + internal->set("value", 2); + + auto object = createJsonObject(); + object->set("required", true); + object->set("bounded", 2); + object->set("description", true); + object->set("enum", "value2"); + object->set("array", serializeToJson(std::vector{1, 2, 3})); + object->set("internal", internal); + + auto json = JsonValue(object); + + auto test = deserializeAs(json); + + CHECK(test.required); + CHECK_EQ(test.bounded, 2); + CHECK(test.description); + CHECK_EQ(test.withDefault, "test"); + CHECK_FALSE(test.optional); + CHECK_EQ(test.someEnum, SomeEnum::Value2); + CHECK_EQ(test.array, std::vector{1, 2, 3}); + CHECK_EQ(test.internal.value, 2); + + object->set("default", "test"); + + auto backToJson = serializeToJson(test); + + CHECK_EQ(backToJson, json); + } } diff --git a/tests/core/jsonv2/TestJsonSchema.cpp b/tests/core/jsonv2/TestJsonSchema.cpp index 752ec5e23..1fad74076 100644 --- a/tests/core/jsonv2/TestJsonSchema.cpp +++ b/tests/core/jsonv2/TestJsonSchema.cpp @@ -150,8 +150,8 @@ TEST_CASE("JsonSchema") .type = JsonType::Object, .properties = { - {"integer", JsonSchema{.required = true, .type = JsonType::Integer}}, - {"string", JsonSchema{.type = JsonType::String}}, + {"integer", JsonSchema{.type = JsonType::Integer}}, + {"string", JsonSchema{.required = false, .type = JsonType::String}}, }, };