#pragma once #include #include #include #include #include #include #include namespace jvalidate { class ValidationResult { public: // Only allow ValidationVisitor to construct the elements of a validation result template friend class ValidationVisitor; using DocPointer = detail::Pointer; using SchemaPointer = detail::Pointer; using Annotation = std::variant>; /** * @brief The result info at any given (DocPointer, SchemaPointer) path. * The key for errors/annotations represents the leaf element of SchemaPointer * instead of including it in the map. * * This allows better locality of error info. For example: * { * "valid": false, * "evaluationPath": "/definitions/EvenPercent", * "instanceLocation": "/foo/bar/percentages/2", * "errors": { * "max": "105 > 100", * "multipleOf": "105 is not a multiple of 2" * } * } */ struct LocalResult { bool valid; std::map errors; std::map annotations; }; struct indent { indent(int i) : i(i) {} friend std::ostream & operator<<(std::ostream & os, indent id) { while (id.i-- > 0) os << " "; return os; } int i; }; private: bool valid_; std::map> results_; public: /** * @brief Writes this object to an osteam in the list format as described in * https://json-schema.org/blog/posts/interpreting-output * This means that the json-schema for a ValidationResult looks like this: * { * "$defs": { * "Pointer": { * "format": "json-pointer", * "type": "string" * }, * "Annotation": { * "items": { "type": "string" }, * "type": [ "string", "array" ] * } * }, * "properties": { * "valid": { "type": "boolean" }, * "details": { * "items": { * "properties": { * "valid": { "type": "boolean" }, * "evaluationPath": { "$ref": "#/$defs/Pointer" }, * "instanceLocation": { "$ref": "#/$defs/Pointer" }, * "annotations": { "$ref": "#/$defs/Annotation" }, * "errors": { "$ref": "#/$defs/Annotation" } * } * "type": "object" * }, * "type": "array" * } * } * "type": "object" * } */ friend std::ostream & operator<<(std::ostream & os, ValidationResult const & result) { char const * div = "\n"; os << "{\n" << indent(1) << R"("valid": )" << (result.valid_ ? "true" : "false") << ',' << '\n'; os << indent(1) << R"("details": [)"; for (auto const & [doc_path, by_schema] : result.results_) { for (auto const & [schema_path, local] : by_schema) { os << std::exchange(div, ",\n") << indent(2) << '{' << '\n'; os << indent(3) << R"("valid": )" << (local.valid ? "true" : "false") << ',' << '\n'; os << indent(3) << R"("evaluationPath": ")" << schema_path << '"' << ',' << '\n'; os << indent(3) << R"("instanceLocation": ")" << doc_path << '"'; print(os, local.annotations, "annotations", 3); print(os, local.errors, "errors", 3); os << '\n' << indent(2) << '}'; } } return os << '\n' << indent(1) << ']' << '\n' << '}'; } static void print(std::ostream & os, std::map const & named, std::string_view name, int const i) { if (named.empty()) { return; } os << ',' << '\n'; os << indent(i) << '"' << name << '"' << ':' << ' ' << '{' << '\n'; for (auto const & [key, anno] : named) { os << indent(i + 1) << '"' << key << '"' << ':' << ' '; if (auto const * str = std::get_if<0>(&anno)) { os << '"' << *str << '"'; } else if (auto const * vec = std::get_if<1>(&anno)) { os << '['; char const * div = "\n"; for (size_t n = 0; n < vec->size(); ++n) { os << std::exchange(div, ",\n") << indent(i + 2) << '"' << vec->at(n) << '"'; } os << '\n' << indent(i + 1) << ']'; } os << '\n'; } os << indent(i) << '}'; } bool valid() const { return valid_; } /** * @brief Are there any validation details associated with the given document * location and schema section. * * @param where A path into the document being validated * @param schema_path The schema path (not counting the leaf element that * actually evaluates the document) * * @return true if the schema path has produced an annotation or error for the * document path */ bool has(detail::Pointer const & where, detail::Pointer const & schema_path) const { return has(where) && results_.at(where).contains(schema_path); } /** * @brief Are there any validation details associated with the given document * location * * @param where A path into the document being validated * * @return true if any rule has produced an annotation or error for the * document path */ bool has(detail::Pointer const & where) const { return results_.contains(where); } /** * @brief Extracts the annotation for requested document and schema location, if it exists * * @param where A path into the document being validated * @param schema_path The schema path, without its leaf element * @param name The leaf schema path (i.e. the rule being evaluated). * @pre name.empty() == schema_path.empty() * * @returns An Annotation for the given path info provided, or nullptr if no annotation exists */ Annotation const * annotation(detail::Pointer const & where, detail::Pointer const & schema_path, std::string const & name) const { if (not results_.contains(where)) { return nullptr; } auto const & by_schema = results_.at(where); if (not by_schema.contains(schema_path)) { return nullptr; } auto const & local = by_schema.at(schema_path); if (not local.annotations.contains(name)) { return nullptr; } return &local.annotations.at(name); } /** * @brief Extracts the error for requested document and schema location, if it exists * * @param where A path into the document being validated * @param schema_path The schema path, without its leaf element * @param name The leaf schema path (i.e. the rule being evaluated). * @pre name.empty() == schema_path.empty() * * @returns An Annotation for the given path info provided, or nullptr if no annotation exists */ Annotation const * error(detail::Pointer const & where, detail::Pointer const & schema_path, std::string const & name) const { if (not results_.contains(where)) { return nullptr; } auto const & by_schema = results_.at(where); if (not by_schema.contains(schema_path)) { return nullptr; } auto const & local = by_schema.at(schema_path); if (not local.errors.contains(name)) { return nullptr; } return &local.errors.at(name); } private: /** * @brief Transfer the contents of another ValidationResult into this one using * {@see std::map::merge} to transfer the data minimizing the need for copy/move. * * @param result The ValidationResult being consumed */ void merge(ValidationResult && result) & { for (auto && [where, by_schema] : result.results_) { for (auto && [schema_path, local] : by_schema) { results_[where][schema_path].annotations.merge(local.annotations); results_[where][schema_path].errors.merge(local.errors); } } } /** * @brief Declare that the document is accepted/rejected by the given schema * * @param where A path into the document being validated * @param schema_path The schema path * @param valid Is this location valid according to the schema */ void valid(detail::Pointer const & where, detail::Pointer const & schema_path, bool valid) { if (has(where, schema_path)) { results_[where][schema_path].valid = valid; } if (where.empty() && schema_path.empty()) { valid_ = valid; } } /** * @brief Attach an error message for part of the document. * Because of the existance of things like "not" schemas, error() can also be * called to add an Annotation for a gate that is passed, but was within a * "not" schema. * * @param where A path into the document being validated * @param schema_path The schema path, without its leaf element * @param name The leaf schema path (i.e. the rule being evaluated). * @pre name.empty() == schema_path.empty() * @param message The annotation(s) being placed as an error */ void error(detail::Pointer const & where, detail::Pointer const & schema_path, std::string const & name, Annotation message) { if (std::visit([](auto const & v) { return v.empty(); }, message)) { return; } results_[where][schema_path].errors.emplace(name, std::move(message)); } /** * @brief Attach some contextual annotations for part of the document * * @param where A path into the document being validated * @param schema_path The schema path, without its leaf element * @param name The leaf schema path (i.e. the rule being evaluated). * @pre name.empty() == schema_path.empty() * @param message The annotation(s) being placed for context */ void annotate(detail::Pointer const & where, detail::Pointer const & schema_path, std::string const & name, Annotation message) { if (std::visit([](auto const & v) { return v.empty(); }, message)) { return; } results_[where][schema_path].annotations.emplace(name, std::move(message)); } }; }