validation_result.h 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. #pragma once
  2. #include <map>
  3. #include <ostream>
  4. #include <utility>
  5. #include <variant>
  6. #include <vector>
  7. #include <jvalidate/detail/pointer.h>
  8. #include <jvalidate/forward.h>
  9. namespace jvalidate {
  10. class ValidationResult {
  11. public:
  12. // Only allow ValidationVisitor to construct the elements of a validation result
  13. template <RegexEngine, typename> friend class ValidationVisitor;
  14. using DocPointer = detail::Pointer;
  15. using SchemaPointer = detail::Pointer;
  16. using Annotation = std::variant<std::string, std::vector<std::string>>;
  17. /**
  18. * @brief The result info at any given (DocPointer, SchemaPointer) path.
  19. * The key for errors/annotations represents the leaf element of SchemaPointer
  20. * instead of including it in the map.
  21. *
  22. * This allows better locality of error info. For example:
  23. * {
  24. * "valid": false,
  25. * "evaluationPath": "/definitions/EvenPercent",
  26. * "instanceLocation": "/foo/bar/percentages/2",
  27. * "errors": {
  28. * "max": "105 > 100",
  29. * "multipleOf": "105 is not a multiple of 2"
  30. * }
  31. * }
  32. */
  33. struct LocalResult {
  34. bool valid;
  35. std::map<std::string, Annotation> errors;
  36. std::map<std::string, Annotation> annotations;
  37. };
  38. struct indent {
  39. indent(int i) : i(i) {}
  40. friend std::ostream & operator<<(std::ostream & os, indent id) {
  41. while (id.i-- > 0)
  42. os << " ";
  43. return os;
  44. }
  45. int i;
  46. };
  47. private:
  48. bool valid_;
  49. std::map<DocPointer, std::map<SchemaPointer, LocalResult>> results_;
  50. public:
  51. /**
  52. * @brief Writes this object to an osteam in the list format as described in
  53. * https://json-schema.org/blog/posts/interpreting-output
  54. * This means that the json-schema for a ValidationResult looks like this:
  55. * {
  56. * "$defs": {
  57. * "Pointer": {
  58. * "format": "json-pointer",
  59. * "type": "string"
  60. * },
  61. * "Annotation": {
  62. * "items": { "type": "string" },
  63. * "type": [ "string", "array" ]
  64. * }
  65. * },
  66. * "properties": {
  67. * "valid": { "type": "boolean" },
  68. * "details": {
  69. * "items": {
  70. * "properties": {
  71. * "valid": { "type": "boolean" },
  72. * "evaluationPath": { "$ref": "#/$defs/Pointer" },
  73. * "instanceLocation": { "$ref": "#/$defs/Pointer" },
  74. * "annotations": { "$ref": "#/$defs/Annotation" },
  75. * "errors": { "$ref": "#/$defs/Annotation" }
  76. * }
  77. * "type": "object"
  78. * },
  79. * "type": "array"
  80. * }
  81. * }
  82. * "type": "object"
  83. * }
  84. */
  85. friend std::ostream & operator<<(std::ostream & os, ValidationResult const & result) {
  86. char const * div = "\n";
  87. os << "{\n" << indent(1) << R"("valid": )" << (result.valid_ ? "true" : "false") << ',' << '\n';
  88. os << indent(1) << R"("details": [)";
  89. for (auto const & [doc_path, by_schema] : result.results_) {
  90. for (auto const & [schema_path, local] : by_schema) {
  91. os << std::exchange(div, ",\n") << indent(2) << '{' << '\n';
  92. os << indent(3) << R"("valid": )" << (local.valid ? "true" : "false") << ',' << '\n';
  93. os << indent(3) << R"("evaluationPath": ")" << schema_path << '"' << ',' << '\n';
  94. os << indent(3) << R"("instanceLocation": ")" << doc_path << '"';
  95. print(os, local.annotations, "annotations", 3);
  96. print(os, local.errors, "errors", 3);
  97. os << '\n' << indent(2) << '}';
  98. }
  99. }
  100. return os << '\n' << indent(1) << ']' << '\n' << '}';
  101. }
  102. static void print(std::ostream & os, std::map<std::string, Annotation> const & named,
  103. std::string_view name, int const i) {
  104. if (named.empty()) {
  105. return;
  106. }
  107. os << ',' << '\n';
  108. os << indent(i) << '"' << name << '"' << ':' << ' ' << '{' << '\n';
  109. for (auto const & [key, anno] : named) {
  110. os << indent(i + 1) << '"' << key << '"' << ':' << ' ';
  111. if (auto const * str = std::get_if<0>(&anno)) {
  112. os << '"' << *str << '"';
  113. } else if (auto const * vec = std::get_if<1>(&anno)) {
  114. os << '[';
  115. char const * div = "\n";
  116. for (size_t n = 0; n < vec->size(); ++n) {
  117. os << std::exchange(div, ",\n") << indent(i + 2) << '"' << vec->at(n) << '"';
  118. }
  119. os << '\n' << indent(i + 1) << ']';
  120. }
  121. os << '\n';
  122. }
  123. os << indent(i) << '}';
  124. }
  125. bool valid() const { return valid_; }
  126. /**
  127. * @brief Are there any validation details associated with the given document
  128. * location and schema section.
  129. *
  130. * @param where A path into the document being validated
  131. * @param schema_path The schema path (not counting the leaf element that
  132. * actually evaluates the document)
  133. *
  134. * @return true if the schema path has produced an annotation or error for the
  135. * document path
  136. */
  137. bool has(detail::Pointer const & where, detail::Pointer const & schema_path) const {
  138. return has(where) && results_.at(where).contains(schema_path);
  139. }
  140. /**
  141. * @brief Are there any validation details associated with the given document
  142. * location
  143. *
  144. * @param where A path into the document being validated
  145. *
  146. * @return true if any rule has produced an annotation or error for the
  147. * document path
  148. */
  149. bool has(detail::Pointer const & where) const { return results_.contains(where); }
  150. /**
  151. * @brief Extracts the annotation for requested document and schema location, if it exists
  152. *
  153. * @param where A path into the document being validated
  154. * @param schema_path The schema path, without its leaf element
  155. * @param name The leaf schema path (i.e. the rule being evaluated).
  156. * @pre name.empty() == schema_path.empty()
  157. *
  158. * @returns An Annotation for the given path info provided, or nullptr if no annotation exists
  159. */
  160. Annotation const * annotation(detail::Pointer const & where, detail::Pointer const & schema_path,
  161. std::string const & name) const {
  162. if (not results_.contains(where)) {
  163. return nullptr;
  164. }
  165. auto const & by_schema = results_.at(where);
  166. if (not by_schema.contains(schema_path)) {
  167. return nullptr;
  168. }
  169. auto const & local = by_schema.at(schema_path);
  170. if (not local.annotations.contains(name)) {
  171. return nullptr;
  172. }
  173. return &local.annotations.at(name);
  174. }
  175. /**
  176. * @brief Extracts the error for requested document and schema location, if it exists
  177. *
  178. * @param where A path into the document being validated
  179. * @param schema_path The schema path, without its leaf element
  180. * @param name The leaf schema path (i.e. the rule being evaluated).
  181. * @pre name.empty() == schema_path.empty()
  182. *
  183. * @returns An Annotation for the given path info provided, or nullptr if no annotation exists
  184. */
  185. Annotation const * error(detail::Pointer const & where, detail::Pointer const & schema_path,
  186. std::string const & name) const {
  187. if (not results_.contains(where)) {
  188. return nullptr;
  189. }
  190. auto const & by_schema = results_.at(where);
  191. if (not by_schema.contains(schema_path)) {
  192. return nullptr;
  193. }
  194. auto const & local = by_schema.at(schema_path);
  195. if (not local.errors.contains(name)) {
  196. return nullptr;
  197. }
  198. return &local.errors.at(name);
  199. }
  200. private:
  201. /**
  202. * @brief Transfer the contents of another ValidationResult into this one using
  203. * {@see std::map::merge} to transfer the data minimizing the need for copy/move.
  204. *
  205. * @param result The ValidationResult being consumed
  206. */
  207. void merge(ValidationResult && result) & {
  208. for (auto && [where, by_schema] : result.results_) {
  209. for (auto && [schema_path, local] : by_schema) {
  210. results_[where][schema_path].annotations.merge(local.annotations);
  211. results_[where][schema_path].errors.merge(local.errors);
  212. }
  213. }
  214. }
  215. /**
  216. * @brief Declare that the document is accepted/rejected by the given schema
  217. *
  218. * @param where A path into the document being validated
  219. * @param schema_path The schema path
  220. * @param valid Is this location valid according to the schema
  221. */
  222. void valid(detail::Pointer const & where, detail::Pointer const & schema_path, bool valid) {
  223. if (has(where, schema_path)) {
  224. results_[where][schema_path].valid = valid;
  225. }
  226. if (where.empty() && schema_path.empty()) {
  227. valid_ = valid;
  228. }
  229. }
  230. /**
  231. * @brief Attach an error message for part of the document.
  232. * Because of the existance of things like "not" schemas, error() can also be
  233. * called to add an Annotation for a gate that is passed, but was within a
  234. * "not" schema.
  235. *
  236. * @param where A path into the document being validated
  237. * @param schema_path The schema path, without its leaf element
  238. * @param name The leaf schema path (i.e. the rule being evaluated).
  239. * @pre name.empty() == schema_path.empty()
  240. * @param message The annotation(s) being placed as an error
  241. */
  242. void error(detail::Pointer const & where, detail::Pointer const & schema_path,
  243. std::string const & name, Annotation message) {
  244. if (std::visit([](auto const & v) { return v.empty(); }, message)) {
  245. return;
  246. }
  247. results_[where][schema_path].errors.emplace(name, std::move(message));
  248. }
  249. /**
  250. * @brief Attach some contextual annotations for part of the document
  251. *
  252. * @param where A path into the document being validated
  253. * @param schema_path The schema path, without its leaf element
  254. * @param name The leaf schema path (i.e. the rule being evaluated).
  255. * @pre name.empty() == schema_path.empty()
  256. * @param message The annotation(s) being placed for context
  257. */
  258. void annotate(detail::Pointer const & where, detail::Pointer const & schema_path,
  259. std::string const & name, Annotation message) {
  260. if (std::visit([](auto const & v) { return v.empty(); }, message)) {
  261. return;
  262. }
  263. results_[where][schema_path].annotations.emplace(name, std::move(message));
  264. }
  265. };
  266. }