diff --git a/.gitignore b/.gitignore index c4a612d..67a1212 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ doc/html .idea cmake-build-* CMakeFiles/ +.vs diff --git a/CMakeLists.txt b/CMakeLists.txt index 70ebf31..9b63b68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,10 @@ if(valijson_BUILD_TESTS) set_target_properties(test_suite PROPERTIES COMPILE_FLAGS " -pedantic -Werror -Wshadow -Wunused") endif() + if (MSVC) + target_compile_options(test_suite PRIVATE "/bigobj") + endif() + # Definition for using picojson set_target_properties(test_suite PROPERTIES COMPILE_DEFINITIONS "PICOJSON_USE_INT64") diff --git a/include/valijson/constraints/concrete_constraints.hpp b/include/valijson/constraints/concrete_constraints.hpp index ebbdfb2..536f7ed 100644 --- a/include/valijson/constraints/concrete_constraints.hpp +++ b/include/valijson/constraints/concrete_constraints.hpp @@ -427,6 +427,33 @@ private: EnumValues m_enumValues; }; +/** + * @brief Represent a 'format' constraint + * + * A format constraint restricts the content of string values, as defined by a set of commonly used formats. + * + * As this is an optional feature in JSON Schema, unrecognised formats will be treated as valid for any string value. + */ +class FormatConstraint: public BasicConstraint +{ +public: + FormatConstraint() + : m_format() { } + + const std::string & getFormat() const + { + return m_format; + } + + void setFormat(const std::string & format) + { + m_format = format; + } + +private: + std::string m_format; +}; + /** * @brief Represents non-singular 'items' and 'additionalItems' constraints * diff --git a/include/valijson/constraints/constraint_visitor.hpp b/include/valijson/constraints/constraint_visitor.hpp index da3f3d7..6faffbd 100644 --- a/include/valijson/constraints/constraint_visitor.hpp +++ b/include/valijson/constraints/constraint_visitor.hpp @@ -10,6 +10,7 @@ class ConstConstraint; class ContainsConstraint; class DependenciesConstraint; class EnumConstraint; +class FormatConstraint; class LinearItemsConstraint; class MaxItemsConstraint; class MaximumConstraint; @@ -46,6 +47,7 @@ protected: typedef constraints::ContainsConstraint ContainsConstraint; typedef constraints::DependenciesConstraint DependenciesConstraint; typedef constraints::EnumConstraint EnumConstraint; + typedef constraints::FormatConstraint FormatConstraint; typedef constraints::LinearItemsConstraint LinearItemsConstraint; typedef constraints::MaximumConstraint MaximumConstraint; typedef constraints::MaxItemsConstraint MaxItemsConstraint; @@ -77,6 +79,7 @@ public: virtual bool visit(const ContainsConstraint &) = 0; virtual bool visit(const DependenciesConstraint &) = 0; virtual bool visit(const EnumConstraint &) = 0; + virtual bool visit(const FormatConstraint &) = 0; virtual bool visit(const LinearItemsConstraint &) = 0; virtual bool visit(const MaximumConstraint &) = 0; virtual bool visit(const MaxItemsConstraint &) = 0; diff --git a/include/valijson/schema_parser.hpp b/include/valijson/schema_parser.hpp index 346a93c..0c0d5da 100644 --- a/include/valijson/schema_parser.hpp +++ b/include/valijson/schema_parser.hpp @@ -694,6 +694,10 @@ private: rootSchema.addConstraintToSubschema(makeEnumConstraint(itr->second), &subschema); } + if ((itr = object.find("format")) != object.end()) { + rootSchema.addConstraintToSubschema(makeFormatConstraint(itr->second), &subschema); + } + { const typename AdapterType::Object::const_iterator itemsItr = object.find("items"); @@ -1421,6 +1425,29 @@ private: return constraint; } + /** + * @brief Make a new FormatConstraint object + * + * @param node JSON node containing the configuration for this constraint + * + * @return pointer to a new FormatConstraint that belongs to the caller + */ + template + constraints::FormatConstraint makeFormatConstraint( + const AdapterType &node) + { + if (node.isString()) { + const std::string value = node.asString(); + if (!value.empty()) { + constraints::FormatConstraint constraint; + constraint.setFormat(value); + return constraint; + } + } + + throwRuntimeError("Expected a string value for 'format' constraint."); + } + /** * @brief Make a new ItemsConstraint object. * diff --git a/include/valijson/validation_visitor.hpp b/include/valijson/validation_visitor.hpp index 16db8c1..3adb0d1 100644 --- a/include/valijson/validation_visitor.hpp +++ b/include/valijson/validation_visitor.hpp @@ -347,6 +347,64 @@ public: return numValidated > 0; } + /** + * @brief Validate current node against a FormatConstraint + * + * @param constraint Constraint that the target must validate against + * + * @return \c true if validation succeeds; \c false otherwise + */ + bool visit(const FormatConstraint &constraint) override + { + const std::string s = m_target.asString(); + const std::string format = constraint.getFormat(); + if (format == "date") { + // Matches dates like: 2022-07-18 + std::regex date_regex("^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$"); + std::smatch matches; + if (std::regex_match(s, matches, date_regex)) { + const auto month = std::stoi(matches[2].str()); + const auto day = std::stoi(matches[3].str()); + return validate_date_range(month, day); + } else { + if (m_results) { + m_results->pushError(m_context, + "String should be a valid date"); + } + return false; + } + } else if (format == "time") { + // Matches times like: 16:52:45Z, 16:52:45+02:00 + std::regex time_regex("^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$"); + if (std::regex_match(s, time_regex)) { + return true; + } else { + if (m_results) { + m_results->pushError(m_context, + "String should be a valid time"); + } + return false; + } + } else if (format == "date-time") { + // Matches data times like: 2022-07-18T16:52:45Z, 2022-07-18T16:52:45+02:00 + std::regex datetime_regex("^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$"); + std::smatch matches; + if (std::regex_match(s, matches, datetime_regex)) { + const auto month = std::stoi(matches[2].str()); + const auto day = std::stoi(matches[3].str()); + return validate_date_range(month, day); + } else { + if (m_results) { + m_results->pushError(m_context, + "String should be a valid date-time"); + } + return false; + } + } + + return true; + } + /** * @brief Validate a value against a LinearItemsConstraint * @@ -1769,6 +1827,47 @@ private: return constraint.accept(visitor); } + /** + * @brief Helper function to validate if day is valid for given month + * + * @param month Month, 1-12 + * @param day Day, 1-31 + * + * @return true if day is valid for given month, false otherwise. + */ + bool validate_date_range(int month, int day) + { + if (month == 2) { + if (day < 0 || day > 29) { + if (m_results) { + m_results->pushError(m_context, + "String should be a valid date-time"); + } + return false; + } + } else { + int limit = 31; + if (month <= 7) { + if (month % 2 == 0) { + limit = 30; + } + } else { + if (month % 2 != 0) { + limit = 30; + } + } + if (day < 0 || day > limit) { + if (m_results) { + m_results->pushError(m_context, + "String should be a valid date-time"); + } + return false; + } + + } + return true; + } + /// The JSON value being validated AdapterType m_target; diff --git a/tests/test_validator.cpp b/tests/test_validator.cpp index 6e8d971..e2bf78e 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -15,10 +15,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -176,6 +178,7 @@ protected: processTestFile(testFile, version); processTestFile(testFile, version); processTestFile(testFile, version); + processTestFile(testFile, version); #ifdef VALIJSON_BUILD_POCO_ADAPTER processTestFile(testFile, version); @@ -605,3 +608,18 @@ TEST_F(TestValidator, Draft7_UniqueItems) { processDraft7TestFile(TEST_SUITE_DIR "draft7/uniqueItems.json"); } + +TEST_F(TestValidator, Draft7_OptionalFormatDate) +{ + processDraft7TestFile(TEST_SUITE_DIR "draft7/optional/format/date.json"); +} + +TEST_F(TestValidator, Draft7_OptionalFormatTime) +{ + processDraft7TestFile(TEST_SUITE_DIR "draft7/optional/format/time.json"); +} + +TEST_F(TestValidator, Draft7_OptionalFormatDateTime) +{ + processDraft7TestFile(TEST_SUITE_DIR "draft7/optional/format/date-time.json"); +} \ No newline at end of file