validation_result.h 11 KB

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