| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- #pragma once
- #include <map>
- #include <ostream>
- #include <utility>
- #include <variant>
- #include <vector>
- #include <jvalidate/detail/pointer.h>
- #include <jvalidate/forward.h>
- namespace jvalidate {
- class ValidationResult {
- public:
- // Only allow ValidationVisitor to construct the elements of a validation result
- template <RegexEngine, typename> friend class ValidationVisitor;
- using DocPointer = detail::Pointer;
- using SchemaPointer = detail::Pointer;
- using Annotation = std::variant<std::string, std::vector<std::string>>;
- /**
- * @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<std::string, Annotation> errors;
- std::map<std::string, Annotation> 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<DocPointer, std::map<SchemaPointer, LocalResult>> 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<std::string, Annotation> 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) << '}';
- }
- /**
- * @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));
- }
- };
- }
|