From 28b76e8e03a79392d13ec1573ca840a945d3a21d Mon Sep 17 00:00:00 2001 From: Adrien4193 <39053578+Adrien4193@users.noreply.github.com> Date: Mon, 6 May 2024 13:10:27 +0200 Subject: [PATCH] BRAYNS-631 Refactor JSON. (#1256) --- src/brayns/core/jsonv2/Json.h | 36 +++ src/brayns/core/jsonv2/JsonReflector.h | 84 ++++++ src/brayns/core/jsonv2/JsonSchema.cpp | 82 ++++++ src/brayns/core/jsonv2/JsonSchema.h | 166 +++++++++++ src/brayns/core/jsonv2/JsonValidator.cpp | 330 ++++++++++++++++++++++ src/brayns/core/jsonv2/JsonValidator.h | 126 +++++++++ src/brayns/core/jsonv2/JsonValue.cpp | 94 ++++++ src/brayns/core/jsonv2/JsonValue.h | 58 ++++ src/brayns/core/jsonv2/types/Arrays.h | 83 ++++++ src/brayns/core/jsonv2/types/Enums.h | 73 +++++ src/brayns/core/jsonv2/types/Maps.h | 77 +++++ src/brayns/core/jsonv2/types/Math.h | 103 +++++++ src/brayns/core/jsonv2/types/Objects.h | 229 +++++++++++++++ src/brayns/core/jsonv2/types/Primitives.h | 78 +++++ src/brayns/core/jsonv2/types/Schema.cpp | 190 +++++++++++++ src/brayns/core/jsonv2/types/Schema.h | 34 +++ src/brayns/core/jsonv2/types/Variants.h | 102 +++++++ src/brayns/core/utils/EnumReflector.h | 172 +++++++++++ src/brayns/core/utils/String.cpp | 61 ++++ src/brayns/core/utils/String.h | 32 +++ tests/core/jsonv2/TestJsonReflection.cpp | 317 +++++++++++++++++++++ tests/core/jsonv2/TestJsonSchema.cpp | 282 ++++++++++++++++++ 22 files changed, 2809 insertions(+) create mode 100644 src/brayns/core/jsonv2/Json.h create mode 100644 src/brayns/core/jsonv2/JsonReflector.h create mode 100644 src/brayns/core/jsonv2/JsonSchema.cpp 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/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/Objects.h create mode 100644 src/brayns/core/jsonv2/types/Primitives.h create mode 100644 src/brayns/core/jsonv2/types/Schema.cpp create mode 100644 src/brayns/core/jsonv2/types/Schema.h create mode 100644 src/brayns/core/jsonv2/types/Variants.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 create mode 100644 tests/core/jsonv2/TestJsonReflection.cpp create mode 100644 tests/core/jsonv2/TestJsonSchema.cpp diff --git a/src/brayns/core/jsonv2/Json.h b/src/brayns/core/jsonv2/Json.h new file mode 100644 index 000000000..cea00a76a --- /dev/null +++ b/src/brayns/core/jsonv2/Json.h @@ -0,0 +1,36 @@ +/* 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/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 new file mode 100644 index 000000000..d4bdcb724 --- /dev/null +++ b/src/brayns/core/jsonv2/JsonReflector.h @@ -0,0 +1,84 @@ +/* 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 +JsonSchema getJsonSchema() +{ + return JsonReflector::getSchema(); +} + +template +JsonValue serializeToJson(const T &value) +{ + return JsonReflector::serialize(value); +} + +template +T deserializeAs(const JsonValue &json) +{ + return JsonReflector::deserialize(json); +} + +template +std::string stringifyToJson(const T &value) +{ + auto json = serializeToJson(value); + return stringify(json); +} + +template +T parseJson(const std::string &data) +{ + auto json = parseJson(data); + return deserializeAs(json); +} +} diff --git a/src/brayns/core/jsonv2/JsonSchema.cpp b/src/brayns/core/jsonv2/JsonSchema.cpp new file mode 100644 index 000000000..65d80e96a --- /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() +{ + 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) +{ + 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 new file mode 100644 index 000000000..1f4c43de4 --- /dev/null +++ b/src/brayns/core/jsonv2/JsonSchema.h @@ -0,0 +1,166 @@ +/* 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 + +#include + +#include "JsonValue.h" + +namespace brayns::experimental +{ +enum class JsonType +{ + Unknown, + Undefined, + Null, + Boolean, + Integer, + Number, + String, + Array, + 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 JsonTypeReflector +{ + static inline constexpr auto type = JsonType::Undefined; +}; + +template<> +struct JsonTypeReflector +{ + static inline constexpr auto type = JsonType::Null; +}; + +template<> +struct JsonTypeReflector +{ + 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) + { + 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 description = {}; + 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 = {}; + + 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..d0ce18fa6 --- /dev/null +++ b/src/brayns/core/jsonv2/JsonValidator.cpp @@ -0,0 +1,330 @@ +/* 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); + +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 = RequiredJsonType{schema.type}; + auto type = getJsonType(json); + if (required.isCompatible(type)) + { + return true; + } + errors.add(InvalidType{type, required.value}); + return false; +} + +void checkConst(const std::string &value, const JsonSchema &schema, ErrorContext &errors) +{ + if (value != schema.constant) + { + errors.add(InvalidConst{value, schema.constant}); + } +} + +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, itemSchema] : schema.properties) + { + if (!object.has(key)) + { + continue; + } + errors.push(key); + check(object.get(key), itemSchema, 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.constant.empty()) + { + const auto &value = json.extract(); + checkConst(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) +{ + 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); +} + +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("Not enough 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 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..86626925d --- /dev/null +++ b/src/brayns/core/jsonv2/JsonValidator.h @@ -0,0 +1,126 @@ +/* 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 InvalidConst +{ + std::string value; + std::string expected; +}; + +std::string toString(const InvalidConst &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 InvalidOneOf +{ +}; + +std::string toString(const InvalidOneOf &error); + +using JsonError = std::variant< + InvalidType, + InvalidConst, + AboveMaximum, + BelowMinimum, + TooManyItems, + NotEnoughItems, + MissingRequiredProperty, + UnknownProperty, + 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..e4e534ab3 --- /dev/null +++ b/src/brayns/core/jsonv2/JsonValue.cpp @@ -0,0 +1,94 @@ +/* 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::Ptr createJsonArray() +{ + return Poco::makeShared(); +} + +JsonObject::Ptr createJsonObject() +{ + return Poco::makeShared(); +} + +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, 0); + return stream.str(); +} + +JsonValue parseJson(const std::string &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 new file mode 100644 index 000000000..2b7a1111f --- /dev/null +++ b/src/brayns/core/jsonv2/JsonValue.h @@ -0,0 +1,58 @@ +/* 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 +#include +#include + +namespace brayns::experimental +{ +using JsonValue = Poco::Dynamic::Var; +using JsonArray = Poco::JSON::Array; +using JsonObject = Poco::JSON::Object; + +struct NullJson +{ +}; + +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); +const JsonObject &getObject(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 new file mode 100644 index 000000000..3bbca59b0 --- /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 = deserializeAs(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..dabd175c5 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Enums.h @@ -0,0 +1,73 @@ +/* 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 +struct JsonReflector +{ + static JsonSchema getSchema() + { + 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 getEnumName(value); + } + + static T deserialize(const JsonValue &json) + { + auto name = deserializeAs(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..5399fe936 --- /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] = deserializeAs(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..e1489427f --- /dev/null +++ b/src/brayns/core/jsonv2/types/Math.h @@ -0,0 +1,103 @@ +/* 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 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 = 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 = 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 +struct JsonReflector> : JsonMathReflector> +{ +}; + +template<> +struct JsonReflector : JsonMathReflector +{ +}; +} diff --git a/src/brayns/core/jsonv2/types/Objects.h b/src/brayns/core/jsonv2/types/Objects.h new file mode 100644 index 000000000..33882e2a0 --- /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); + } + + return 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 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 &&getPtr) + { + 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(*getPtr(object)); }; + field.deserialize = [=](const auto &json, auto &object) { *getPtr(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/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/Schema.cpp b/src/brayns/core/jsonv2/types/Schema.cpp new file mode 100644 index 000000000..49c47c870 --- /dev/null +++ b/src/brayns/core/jsonv2/types/Schema.cpp @@ -0,0 +1,190 @@ +/* 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{ + .type = JsonType::Object, + .items = {JsonSchema()}, + }; +} + +JsonValue JsonReflector::serialize(const JsonSchema &schema) +{ + auto object = createJsonObject(); + + 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.constant.empty()) + { + set(*object, "const", schema.constant); + 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/src/brayns/core/jsonv2/types/Variants.h b/src/brayns/core/jsonv2/types/Variants.h new file mode 100644 index 000000000..0823526ab --- /dev/null +++ b/src/brayns/core/jsonv2/types/Variants.h @@ -0,0 +1,102 @@ +/* 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 { + .required = false, + .oneOf = {getJsonSchema(), getJsonSchema()}, + }; + } + + static JsonValue serialize(const std::optional &value) + { + if (!value) + { + return {}; + } + return serializeToJson(*value); + } + + static std::optional deserialize(const JsonValue &json) + { + if (json.isEmpty()) + { + return std::nullopt; + } + return deserializeAs(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 deserializeAs(json); + } + catch (...) + { + if constexpr (sizeof...(Us) == 0) + { + throw JsonException("Invalid oneOf"); + } + else + { + return tryDeserialize(json); + } + } + } +}; +} diff --git a/src/brayns/core/utils/EnumReflector.h b/src/brayns/core/utils/EnumReflector.h new file mode 100644 index 000000000..d24a0f7ab --- /dev/null +++ b/src/brayns/core/utils/EnumReflector.h @@ -0,0 +1,172 @@ +/* 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 +#include +#include + +#include + +namespace brayns::experimental +{ +template +struct EnumField +{ + std::string name; + T value; + std::string description; +}; + +template +class EnumInfo +{ +public: + explicit EnumInfo(std::vector> fields): + _fields(std::move(fields)) + { + } + + const std::vector> &getFields() const + { + return _fields; + } + + const EnumField *findByName(std::string_view name) const + { + auto sameName = [&](const auto &field) { return field.name == name; }; + auto i = std::ranges::find_if(_fields, sameName); + return i == _fields.end() ? nullptr : &*i; + } + + const EnumField *findByValue(T value) const + { + auto sameValue = [&](const auto &field) { return field.value == value; }; + auto i = std::ranges::find_if(_fields, sameValue); + return i == _fields.end() ? nullptr : &*i; + } + + const EnumField &getByName(std::string_view name) const + { + const auto *field = findByName(name); + if (field) + { + return *field; + } + throw std::invalid_argument(fmt::format("Invalid enum name: '{}'", name)); + } + + const EnumField &getByValue(T value) const + { + const auto *field = findByValue(value); + if (field) + { + return *field; + } + throw std::invalid_argument(fmt::format("Invalid enum value: {}", std::underlying_type_t(value))); + } + +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) + { + } + + EnumFieldBuilder description(std::string description) + { + _field->description = std::move(description); + return *this; + } + +private: + EnumField *_field; +}; + +template +class EnumInfoBuilder +{ +public: + EnumFieldBuilder field(std::string name, T value) + { + auto &emplaced = _fields.emplace_back(); + emplaced.name = std::move(name); + emplaced.value = value; + return EnumFieldBuilder(emplaced); + } + + EnumInfo build() + { + return EnumInfo(std::exchange(_fields, {})); + } + +private: + std::vector> _fields; +}; +} 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); +} diff --git a/tests/core/jsonv2/TestJsonReflection.cpp b/tests/core/jsonv2/TestJsonReflection.cpp new file mode 100644 index 000000000..8c71f4604 --- /dev/null +++ b/tests/core/jsonv2/TestJsonReflection.cpp @@ -0,0 +1,317 @@ +/* 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; + +namespace brayns::experimental +{ +enum class SomeEnum +{ + Value1, + Value2, +}; + +template<> +struct EnumReflector +{ + static EnumInfo reflect() + { + auto builder = EnumInfoBuilder(); + builder.field("value1", SomeEnum::Value1).description("Value 1"); + builder.field("value2", SomeEnum::Value2).description("Value 2"); + 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") +{ + 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}); + CHECK_EQ(deserializeAs(1), JsonValue(1)); + CHECK_EQ(serializeToJson(JsonValue("2")), JsonValue("2")); + } + SUBCASE("Null") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Null}); + deserializeAs({}); + CHECK_EQ(serializeToJson(NullJson()), JsonValue()); + CHECK_THROWS_AS(deserializeAs("xyz"), JsonException); + } + SUBCASE("Boolean") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Boolean}); + CHECK_EQ(deserializeAs(true), true); + CHECK_EQ(serializeToJson(true), JsonValue(true)); + 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(deserializeAs(1), 1); + CHECK_EQ(serializeToJson(1), JsonValue(1)); + 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(deserializeAs(1), 1.0f); + CHECK_EQ(serializeToJson(1.5f), JsonValue(1.5f)); + CHECK_THROWS_AS(deserializeAs("1.5"), JsonException); + } + SUBCASE("String") + { + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::String}); + CHECK_EQ(deserializeAs("test"), JsonValue("test")); + CHECK_EQ(serializeToJson(std::string("test")), JsonValue("test")); + CHECK_THROWS_AS(deserializeAs(1), JsonException); + } + SUBCASE("Enum") + { + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .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(deserializeAs(1), JsonException); + CHECK_THROWS_AS(deserializeAs("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(deserializeAs(1), Variant(1)); + CHECK_EQ(deserializeAs("test"), Variant("test")); + CHECK_THROWS_AS(deserializeAs(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(deserializeAs>({}), std::nullopt); + 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 new file mode 100644 index 000000000..1fad74076 --- /dev/null +++ b/tests/core/jsonv2/TestJsonSchema.cpp @@ -0,0 +1,282 @@ +/* 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 = validate(json, schema); + CHECK(errors.empty()); + + json = 1; + errors = validate(json, schema); + CHECK(errors.empty()); + } + SUBCASE("One of") + { + auto schema = JsonSchema{.oneOf = {getJsonSchema(), getJsonSchema()}}; + + auto errors = validate(1.0f, schema); + CHECK(errors.empty()); + + errors = validate("Test", schema); + CHECK(errors.empty()); + + errors = validate(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 = 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 = validate(1, schema); + CHECK(errors.empty()); + } + SUBCASE("Limits") + { + auto schema = JsonSchema{ + .type = JsonType::Integer, + .minimum = -1, + .maximum = 3, + }; + + auto errors = validate(1, schema); + CHECK(errors.empty()); + + errors = validate(-1, schema); + CHECK(errors.empty()); + + errors = validate(3, schema); + CHECK(errors.empty()); + + errors = validate(-2, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Value below minimum: -2 < -1"); + + 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 = validate(-8, schema); + CHECK(errors.empty()); + + errors = validate(125, schema); + CHECK(errors.empty()); + } + SUBCASE("Constant") + { + auto schema = JsonSchema{ + .type = JsonType::String, + .constant = "test", + }; + + auto errors = validate("test", 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 = validate(1, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected string got integer"); + } + 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 = 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 = 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 = validate(json, schema); + CHECK(errors.empty()); + } + SUBCASE("Missing property") + { + auto schema = JsonSchema{ + .type = JsonType::Object, + .properties = + { + {"integer", JsonSchema{.type = JsonType::Integer}}, + {"string", JsonSchema{.required = false, .type = JsonType::String}}, + }, + }; + + auto json = parseJson(R"({"integer": 1, "string": "test"})"); + auto errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"({"string": "test"})"); + 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 = validate(json, schema); + CHECK(errors.empty()); + } + SUBCASE("Unknown properties") + { + auto schema = JsonSchema{.type = JsonType::Object}; + + auto json = parseJson(R"({"something": 1})"); + auto errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Unknown property: 'something'"); + + json = parseJson(R"({})"); + errors = validate(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 = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([])"); + errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([1, "test", 2])"); + 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"); + } + SUBCASE("Item count") + { + auto schema = JsonSchema{ + .type = JsonType::Array, + .items = {getJsonSchema()}, + .minItems = 1, + .maxItems = 3, + }; + + auto json = parseJson(R"([1])"); + auto errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([1, 2, 3])"); + errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([])"); + 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 = validate(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 = validate(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"); + } + SUBCASE("Schema as JSON") + { + 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); + } +}