#include "nlohmann/json-schema.hpp" #include "nlohmann/json.hpp" #include #include #include #include //============================================================================== // Test macros //============================================================================== #define LOG_ERROR(LOG_ERROR__ARGS) \ std::cerr << __FILE__ << ":" << __LINE__ << ": " << LOG_ERROR__ARGS << std::endl #define EXPECT_THROW_WITH_MESSAGE(EXPRESSION, MESSAGE) \ do { \ try { \ EXPRESSION; \ LOG_ERROR("Expected exception not thrown with matching regex: \"" << MESSAGE << "\""); \ ++g_error_count; \ } catch (const std::exception &error) { \ const std::regex error_re{MESSAGE}; \ if (!std::regex_search(error.what(), error_re)) { \ LOG_ERROR("Expected exception with matching regex: \"" << MESSAGE << "\", but got this instead: " << error.what()); \ ++g_error_count; \ } \ } \ } while (false) #define ASSERT_OR_EXPECT_EQ(FIRST_THING, SECOND_THING, RETURN_IN_CASE_OF_ERROR) \ do { \ if ((FIRST_THING) != (SECOND_THING)) { \ LOG_ERROR("The two values of " << (FIRST_THING) << " (" #FIRST_THING << ") and " << (SECOND_THING) << " (" #SECOND_THING << ") should be equal"); \ if (RETURN_IN_CASE_OF_ERROR) { \ return; \ } \ } \ } while (false) #define ASSERT_EQ(FIRST_THING, SECOND_THING) ASSERT_OR_EXPECT_EQ(FIRST_THING, SECOND_THING, true) #define EXPECT_EQ(FIRST_THING, SECOND_THING) ASSERT_OR_EXPECT_EQ(FIRST_THING, SECOND_THING, true) #define EXPECT_MATCH(STRING, REGEX) \ do { \ if (!std::regex_search((STRING), std::regex{(REGEX)})) { \ LOG_ERROR("String \"" << (STRING) << "\" doesn't match with regex: \"" << (REGEX) << "\""); \ ++g_error_count; \ } \ } while (false) namespace { //============================================================================== // Test environment //============================================================================== int g_error_count = 0; //============================================================================== // The schema used for testing //============================================================================== const std::string g_schema_template = R"( { "properties": { "first": { "%COMBINATION_FIRST_LEVEL%": [ { "properties": { "second": { "%COMBINATION_SECOND_LEVEL%": [ { "minimum": 5, "type": "integer" }, { "multipleOf": 2, "type": "integer" } ] } }, "type": "object" }, { "minimum": 20, "type": "integer" }, { "minLength": 10, "type": "string" } ] } }, "type": "object" } )"; auto generateSchema(const std::string &first_combination, const std::string &second_combination) -> nlohmann::json { static const std::regex first_replace_re{"%COMBINATION_FIRST_LEVEL%"}; static const std::regex second_replace_re{"%COMBINATION_SECOND_LEVEL%"}; std::string intermediate = std::regex_replace(g_schema_template, first_replace_re, first_combination); return nlohmann::json::parse(std::regex_replace(intermediate, second_replace_re, second_combination)); } //============================================================================== // Error handler to catch all the errors generated by the validator - also inside the combinations //============================================================================== class MyErrorHandler : public nlohmann::json_schema::error_handler { public: struct ErrorEntry { nlohmann::json::json_pointer ptr; nlohmann::json intance; std::string message; }; using ErrorEntryList = std::vector; auto getErrors() const -> const ErrorEntryList & { return m_error_list; } private: auto error(const nlohmann::json::json_pointer &ptr, const nlohmann::json &instance, const std::string &message) -> void override { m_error_list.push_back(ErrorEntry{ptr, instance, message}); } ErrorEntryList m_error_list; }; //============================================================================== // Error string helpers //============================================================================== auto operator<<(std::string first, const std::string &second) -> std::string { first += ".*"; first += second; return first; } auto rootError(const std::string &combination_type, std::size_t number_of_subschemas) -> std::string { return "no subschema has succeeded, but one of them is required to validate. Type: " + combination_type + ", number of failed subschemas: " + std::to_string(number_of_subschemas); } auto combinationError(const std::string &combination_type, std::size_t test_case_number) -> std::string { return "[combination: " + combination_type + " / case#" + std::to_string(test_case_number) + "]"; } //============================================================================== // Validator function - for simplicity //============================================================================== auto validate(const nlohmann::json &schema, const nlohmann::json &instance, nlohmann::json_schema::error_handler *error_handler = nullptr) -> void { nlohmann::json_schema::json_validator validator; validator.set_root_schema(schema); if (error_handler) { validator.validate(instance, *error_handler); } else { validator.validate(instance); } } //============================================================================== // The test cases //============================================================================== auto simpleTest(const std::string &first_combination, const std::string &second_combination) -> void { const nlohmann::json schema = generateSchema(first_combination, second_combination); EXPECT_THROW_WITH_MESSAGE(validate(schema, nlohmann::json{{"first", {{"second", 1}}}}), rootError(first_combination, 3)); if (second_combination == "oneOf") { EXPECT_THROW_WITH_MESSAGE(validate(schema, nlohmann::json{{"first", {{"second", 8}}}}), rootError(first_combination, 3)); } EXPECT_THROW_WITH_MESSAGE(validate(schema, nlohmann::json{{"first", 10}}), rootError(first_combination, 3)); EXPECT_THROW_WITH_MESSAGE(validate(schema, nlohmann::json{{"first", "short"}}), rootError(first_combination, 3)); } auto verboseTest(const std::string &first_combination, const std::string &second_combination) -> void { const nlohmann::json schema = generateSchema(first_combination, second_combination); { MyErrorHandler error_handler; validate(schema, nlohmann::json{{"first", {{"second", 1}}}}, &error_handler); const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors(); EXPECT_EQ(error_list.size(), 6); EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3)); EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first/second"}); EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << rootError(second_combination, 2)); EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first/second"}); EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 0) << combinationError(second_combination, 0) << "instance is below minimum of 5"); EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first/second"}); EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 0) << combinationError(second_combination, 1) << "instance is not a multiple of 2.0"); EXPECT_EQ(error_list[4].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[4].message, combinationError(first_combination, 1) << "unexpected instance type"); EXPECT_EQ(error_list[5].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[5].message, combinationError(first_combination, 2) << "unexpected instance type"); } { MyErrorHandler error_handler; validate(schema, nlohmann::json{{"first", {{"second", "not-an-integer"}}}}, &error_handler); const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors(); EXPECT_EQ(error_list.size(), 6); EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3)); EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first/second"}); EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << rootError(second_combination, 2)); EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first/second"}); EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 0) << combinationError(second_combination, 0) << "unexpected instance type"); EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first/second"}); EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 0) << combinationError(second_combination, 1) << "unexpected instance type"); EXPECT_EQ(error_list[4].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[4].message, combinationError(first_combination, 1) << "unexpected instance type"); EXPECT_EQ(error_list[5].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[5].message, combinationError(first_combination, 2) << "unexpected instance type"); } if (second_combination == "oneOf") { MyErrorHandler error_handler; validate(schema, nlohmann::json{{"first", {{"second", 8}}}}, &error_handler); const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors(); EXPECT_EQ(error_list.size(), 4); EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3)); EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first/second"}); EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << "more than one subschema has succeeded, but exactly one of them is required to validate"); EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 1) << "unexpected instance type"); EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 2) << "unexpected instance type"); } { MyErrorHandler error_handler; validate(schema, nlohmann::json{{"first", 10}}, &error_handler); const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors(); EXPECT_EQ(error_list.size(), 4); EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3)); EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << "unexpected instance type"); EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 1) << "instance is below minimum of 20"); EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 2) << "unexpected instance type"); } { MyErrorHandler error_handler; validate(schema, nlohmann::json{{"first", "short"}}, &error_handler); const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors(); EXPECT_EQ(error_list.size(), 4); EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3)); EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << "unexpected instance type"); EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 1) << "unexpected instance type"); EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first"}); EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 2) << "instance is too short as per minLength:10"); } } } // namespace //============================================================================== // MAIN - calling the test cases //============================================================================== auto main() -> int { simpleTest("anyOf", "anyOf"); simpleTest("anyOf", "oneOf"); simpleTest("oneOf", "anyOf"); simpleTest("oneOf", "oneOf"); verboseTest("anyOf", "anyOf"); verboseTest("anyOf", "oneOf"); verboseTest("oneOf", "anyOf"); verboseTest("oneOf", "oneOf"); return g_error_count; }