|
|
@@ -2,7 +2,6 @@
|
|
|
|
|
|
#include <map>
|
|
|
#include <ostream>
|
|
|
-#include <unordered_set>
|
|
|
#include <utility>
|
|
|
#include <variant>
|
|
|
#include <vector>
|
|
|
@@ -13,10 +12,29 @@
|
|
|
namespace jvalidate {
|
|
|
class ValidationResult {
|
|
|
public:
|
|
|
+ // Only allow ValidationVisitor to construct the elements of a validation result
|
|
|
template <Adapter A, RegexEngine RE> 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;
|
|
|
@@ -40,6 +58,40 @@ private:
|
|
|
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';
|
|
|
@@ -82,12 +134,42 @@ public:
|
|
|
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)) {
|
|
|
@@ -104,6 +186,16 @@ public:
|
|
|
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)) {
|
|
|
@@ -121,7 +213,13 @@ public:
|
|
|
}
|
|
|
|
|
|
private:
|
|
|
- void merge(ValidationResult && result) {
|
|
|
+ /**
|
|
|
+ * @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);
|
|
|
@@ -130,6 +228,13 @@ private:
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @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;
|
|
|
@@ -139,6 +244,18 @@ private:
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @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)) {
|
|
|
@@ -147,6 +264,15 @@ private:
|
|
|
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)) {
|