Sfoglia il codice sorgente

feat: add support for Draft03-format keywords: date-time, ip-address, host-name (alt. name) and time, utc-millisec, color (new/changed)

Sam Jaffe 2 settimane fa
parent
commit
704864c10e

+ 1 - 1
include/jvalidate/constraint.h

@@ -730,7 +730,7 @@ public:
    * @throws If the contained value is not interpretable as a string
    */
   static auto format(detail::ParserContext<A> const & context) {
-    return ptr(constraint::FormatConstraint{context.schema.as_string(),
+    return ptr(constraint::FormatConstraint{context.schema.as_string(), context.vocab->version(),
                                             context.vocab->is_format_assertion()});
   }
 

+ 2 - 0
include/jvalidate/constraint/string_constraint.h

@@ -3,6 +3,7 @@
 #include <string>
 
 #include <jvalidate/detail/string.h>
+#include <jvalidate/enum.h>
 #include <jvalidate/forward.h>
 
 namespace jvalidate::constraint {
@@ -53,6 +54,7 @@ struct PatternConstraint {
  */
 struct FormatConstraint {
   std::string format;
+  schema::Version for_version;
   bool is_assertion;
 };
 }

+ 79 - 9
include/jvalidate/format.h

@@ -8,7 +8,9 @@
 #include <ctime>
 #include <string>
 #include <string_view>
+#include <system_error>
 #include <unordered_map>
+#include <unordered_set>
 #include <utility>
 
 #ifdef JVALIDATE_HAS_IDNA
@@ -21,6 +23,7 @@
 #include <jvalidate/detail/pointer.h>
 #include <jvalidate/detail/relative_pointer.h>
 #include <jvalidate/detail/string.h>
+#include <jvalidate/enum.h>
 #include <jvalidate/forward.h>
 
 #define CONSTRUCTS(TYPE) format::ctor_as_valid<detail::TYPE>
@@ -46,6 +49,11 @@ template <typename CharT = char> bool email(std::basic_string_view<CharT> em);
 }
 
 namespace jvalidate::format::detail {
+inline bool is_hex(std::string_view s) {
+  constexpr char const * g_hex_digits = "0123456789ABCDEFabcdef";
+  return s.find_first_not_of(g_hex_digits) == std::string::npos;
+}
+
 struct result {
   ptrdiff_t consumed;
   bool valid;
@@ -216,6 +224,41 @@ template <typename CharT> bool test_uri_part(std::basic_string_view<CharT> & uri
 };
 }
 
+namespace jvalidate::format::draft03 {
+namespace detail = jvalidate::format::detail;
+
+inline bool time(std::string_view dt) {
+  std::tm tm;
+  char const * end = strptime(dt.data(), "%T", &tm);
+  if (end == nullptr || (end - dt.data()) < 8) {
+    return false;
+  }
+  return end == dt.end();
+}
+
+inline bool utc_millisec(std::string_view utc) {
+  int64_t itime;
+  if (auto [end, ec] = std::from_chars(utc.begin(), utc.end(), itime);
+      ec == std::errc{} && end == utc.end()) {
+    return true;
+  }
+  double dtime;
+  auto [end, ec] = std::from_chars(utc.begin(), utc.end(), dtime);
+  return ec == std::errc{} && end == utc.end();
+}
+
+inline bool css_2_1_color(std::string_view color) {
+  constexpr char const * g_hex_digits = "0123456789ABCDEFabcdef";
+  if (color[0] == '#') {
+    return color.size() <= 7 && detail::is_hex(color.substr(1));
+  }
+  static std::unordered_set<std::string_view> g_color_codes{
+      "maroon", "red",  "orange", "yellow", "olive", "purple", "fuchsia", "white", "lime",
+      "green",  "navy", "blue",   "aqua",   "teal",  "black",  "silver",  "gray"};
+  return g_color_codes.contains(color);
+}
+}
+
 namespace jvalidate::format {
 inline bool date(std::string_view dt) {
   auto [consumed, valid] = detail::date(dt);
@@ -337,17 +380,14 @@ inline bool uri_template(std::u32string_view uri) {
 }
 
 inline bool uuid(std::string_view id) {
-  constexpr char const * g_hex_digits = "0123456789ABCDEFabcdef";
   constexpr size_t g_uuid_len = 36;
   constexpr size_t g_uuid_tokens = 5;
   char tok0[9], tok1[5], tok2[5], tok3[5], tok4[13];
 
-  auto is_hex = [](std::string_view s) {
-    return s.find_first_not_of(g_hex_digits) == std::string::npos;
-  };
   return id.size() == g_uuid_len &&
          sscanf(id.data(), "%8s-%4s-%4s-%4s-%12s", tok0, tok1, tok2, tok3, tok4) == g_uuid_tokens &&
-         is_hex(tok0) && is_hex(tok1) && is_hex(tok2) && is_hex(tok3) && is_hex(tok4);
+         detail::is_hex(tok0) && detail::is_hex(tok1) && detail::is_hex(tok2) &&
+         detail::is_hex(tok3) && detail::is_hex(tok4);
 }
 
 inline bool duration(std::string_view dur) {
@@ -639,6 +679,8 @@ public:
   enum class Status { Unknown, Unimplemented, Valid, Invalid };
 
 private:
+  std::unordered_map<std::string, Predicate> user_formats_{{"regex", nullptr}};
+
   std::unordered_map<std::string, Predicate> supported_formats_{
       {"date", &format::date},
       {"date-time", &format::date_time},
@@ -653,7 +695,6 @@ private:
       {"iri-reference", UTF32(uri_reference)},
       {"json-pointer", CONSTRUCTS(Pointer)},
       {"relative-json-pointer", CONSTRUCTS(RelativePointer)},
-      {"regex", nullptr},
       {"time", &format::time},
       {"uri", &format::uri},
       {"uri-reference", &format::uri_reference},
@@ -661,12 +702,41 @@ private:
       {"uuid", &format::uuid},
   };
 
+  std::unordered_map<std::string, Predicate> draft03_supported_formats_{
+      {"date", &format::date},
+      // One of the weird things about draft03 - date-time allows for timezone
+      // and fraction-of-second in the argument, but time only allows hh:mm:ss.
+      {"date-time", &format::date_time},
+      {"time", &format::draft03::time},
+      {"utc-millisec", &format::draft03::utc_millisec},
+      {"color", &format::draft03::css_2_1_color},
+      {"style", nullptr},
+      {"phone", nullptr},
+      {"uri", &format::uri},
+      {"email", &format::email},
+      {"ip-address", &format::ipv4},
+      {"ipv6", &format::ipv6},
+      {"host-name", &format::hostname},
+  };
+
 public:
   FormatValidator() = default;
-  FormatValidator(Predicate is_regex) { supported_formats_.insert_or_assign("regex", is_regex); }
+  FormatValidator(Predicate is_regex) { user_formats_.insert_or_assign("regex", is_regex); }
+
+  Status operator()(std::string const & format, schema::Version for_version,
+                    std::string_view text) const {
+    auto const & supported =
+        for_version == schema::Version::Draft03 ? draft03_supported_formats_ : supported_formats_;
+    if (Status rval = (*this)(supported, format, text); rval != Status::Unknown) {
+      return rval;
+    }
+    return (*this)(user_formats_, format, text);
+  }
 
-  Status operator()(std::string const & format, std::string_view text) const {
-    if (auto it = supported_formats_.find(format); it != supported_formats_.end() && it->second) {
+private:
+  Status operator()(auto const & supported, std::string const & format,
+                    std::string_view text) const {
+    if (auto it = supported.find(format); it != supported.end()) {
       if (not it->second) {
         return Status::Unimplemented;
       }

+ 1 - 1
include/jvalidate/validation_visitor.h

@@ -320,7 +320,7 @@ public:
       return true; // TODO: I think this can be made into Noop
     }
 
-    switch (FormatValidator(&RE::is_regex)(cons.format, document.as_string())) {
+    switch (FormatValidator(&RE::is_regex)(cons.format, cons.for_version, document.as_string())) {
     case FormatValidator::Status::Unimplemented:
       return result(Status::Reject, "unimplemented format '", cons.format, "'");
     case FormatValidator::Status::Invalid: