瀏覽代碼

docs: README.md

Sam Jaffe 2 周之前
父節點
當前提交
684ea3fd55
共有 3 個文件被更改,包括 247 次插入3 次删除
  1. 228 0
      README.md
  2. 18 3
      include/jvalidate/constraint.h
  3. 1 0
      include/jvalidate/enum.h

+ 228 - 0
README.md

@@ -0,0 +1,228 @@
+# JSON Validator
+
+A Header-Only JSON Schema Validator library written in C++20.
+Provides json schema validation compliant with the [json schema][1]
+  specification.  
+Is compatible with the following draft versions:
+* [draft2020-12][2]
+* [draft2019-09][3]
+* [draft-07][4]
+* [draft-06][5]
+* [draft-05][6]
+* [draft-04][7]
+* [draft-03][8]
+
+## Building
+
+``` bash
+cmake -S . -B build [options...]
+make -C build
+ctest --test-dir build/tests
+```
+
+If, for example, you have ICU provided via homebrew,
+  you will need to add the homebrew paths to your cmake command.
+
+For example: `-DICU_ROOT=/opt/homebrew/opt/icu4c`
+
+## Usage
+
+There are three main components for json validation,
+  plus an additional customization point:
+1. The adapter between `jvalidate` and the user's JSON type: `jvalidate::Adapter`
+1. The schema object: `jvalidate::Schema`
+1. The validator object: `jvalidate::Validator`
+1. User-defined constraints: `jvalidate::extension::ConstraintBase` and
+   `jvalidate::extension::Visitor`.
+
+### Creating an Adapter for custom JSON Types
+
+An adapter represents the following interface/contract for
+  a json implementation `MyJsonType`:
+* A free function with the signature
+  `bool load_stream(std::istream &, MyJsonType &, std::string &error) noexcept`
+* An implementation of `jvalidate::adapter::AdapterTraits<MyJsonType>`,
+  which must provide the following:
+    * `template <typename T> using Adapter = MyJsonTypeAdapter<T>`
+    * `using ConstAdapter = MyJsonTypeAdapter<MyJsonType const>`
+    * `static MyJsonType const &const_empty() { ... }`
+* An adapter class implementation that subclasses
+  `jvalidate::adapter::Adapter` and
+  fulfills the contract `jvalidate::Adapter`.
+    * By convention, the adapter class should not have ownership of the
+      underlying object.
+* An adapter class implementation that fulfills the contract
+  `jvalidate::ArrayAdapter`
+* An adapter class implementation that fulfills the contract
+  `jvalidate::ObjectAdapter`
+
+### Constructing a Schema
+
+The schema class describes how to validate a json document,
+  but does not contain the business logic to perform the validation.
+
+The simplest constructor for a schema has the following signature,
+  and uses `jvalidate::adapter::AdapterTraits` to deduce the adapter
+  that should wrap the document.
+``` c++
+MyJsonType schema_document = ...;
+jvalidate::Schema schema(schema_document, jvalidate::schema::Version::Draft2020_12);
+```
+
+Schemas also support additional components, which can be provided in any order.
+
+#### URI Resolver
+
+A URIResolver is a free function with the following signature:
+  `bool (*)(jvalidate::URI const &, MyJsonType &, std::string &error) noexcept`.
+
+If no uri resolver is provided,
+  then it is not possible to read schemas from the internet or from files.
+It is also not possible to read custom vocabularies.
+
+A curl based resolver is provided in `include/jvalidate/compat/curl.h`.
+
+#### Constraint Factory
+
+A ConstraintFactory is the object that maps keywords to Constraint objects.
+Because it is possible to change schema versions when following a reference,
+  ConstraintFactory *MUST* provide information on all schema drafts at once.
+
+There are two ways to provide user-defined keywords:
+1. The constructor `ConstraintFactory(std::initializer_list<std::pair<std::string_view, Versioned>> init)`
+1. An append function `ConstraintFactory::with_user_keyword(std::string_view, Versioned)`
+
+For example:
+``` c++
+jvalidate::Schema schema(
+    schema_document, jvalidate::schema::Version::Draft2020_12,
+    jvalidate::ConstraintFactory()
+      .with_user_keyword("my_keyword", create_my_constraint));
+```
+
+<!-- @see jvalidate::ConstraintFactory::Versioned -->
+In order to support multiple schema versions in a single instance of
+a ConstraintFactory, we need to be able to describe which version a keyword
+becomes part of the language vocabulary, and what (if any) version it
+leaves the vocabulary after.
+
+To do this, we store an ordered map of Version enum onto a vocabulary Metadata
+object and then use `std::map::lower_bound` to determine which one is the most
+appropriate for the schema version being evaluated.
+
+For example:
+* The "additionalProperties" constraint is the same across all versions, and
+  so can be represented using only a function pointer.
+  ```
+  {"additionalProperties", &Self::additionalProperties}
+  ```
+* The "const" constraint was not added until Draft06, so we include the
+  version when constructing its constraint bindings like so:
+  ```
+  {"const", {schema::Version::Draft06, &Self::isConstant}}
+  ```
+* The "divisibleBy" constraint was removed in favor of "multipleOf" in
+  Draft04, and therefore is represented as:
+  ```
+  {"divisibleBy", {{schema::Version::Earliest, &Self::multipleOf},
+                   {schema::Version::Draft04, Removed}}},
+  {"multipleOf",  {schema::Version::Draft04, &Self::multipleOf}}
+  ```
+* A small number of rare constraints change their meaning when moving from
+  one draft version to another in such a significant way that it makes more
+  sense to use different MakeConstraint functions for them.
+  ```
+  {"items", {{schema::Version::Earliest, &Self::itemsTupleOrVector},
+             {schema::Version::Draft2020_12, &Self::additionalItems}}}
+  ```
+* Reserved keywords that have no meaning by themselves can use the Literal
+  rule:
+  ```
+  {"contains", {schema::Version::Draft06, &Self::contains}},
+  {"maxContains", {schema::Version::Draft06, Literal}},
+  {"minContains", {schema::Version::Draft06, Literal}},
+  ```
+* Since some special words like "default", "examples", "enum", etc. may
+  contain objects which should not be evaluated as JSON schemas for things
+  like "$id" tokens, the rules Keyword and KeywordMap allow marking only
+  those keywords that are expected to hold more json schemas to be evaluated:
+  ```
+  {"$defs", {schema::Version::Draft2019_09, KeywordMap}},
+  {"additionalProperties", {{&Self::additionalProperties, Keyword}}},
+  {"allOf", {schema::Version::Draft04, {&Self::allOf, Keyword}}},
+  ```
+
+### Validating a Document
+
+Validation is performed by constructing a Validator object,
+  and calling the `validate()` function.
+The validator does not own the schema node that it operates on,
+  but it does own a copy of the provided ExtensionVisitor (if any),
+  and of the RegexEngine that it maintains internally.
+Reusing the Validator can allow for caching of regular expressions,
+  if they are commonly used in the schema.
+
+There are two main ways to call `Validator::validate`,
+  with or without a `jvalidate::ValidationResult` object.
+If validate is called without a result,
+  then the evaluation will end immediately after
+  the first rejecting constraint.
+Otherwise,
+  it will return all evaluation reasons.
+
+### Creating custom extensions
+
+Custom extensions allow the implementation of user-specific constraints that
+  either cannot naturally be described in the json schema specification
+  or are used commonly enough that the DRY principle applies.
+Some virtual function magic is used to unwrap all of the type-erasure that
+  occurs does not require the consumer to write any boilerplate.
+
+A user constraint is defined as a simple struct with the following form:
+``` c++
+struct MyCustomConstraint : jvalidate::extension::ConstraintBase<MyCustomConstraint> {
+  MyCustomConstraint(...) { ... } // Required b/c we're a subclass
+
+  // fields...
+};
+```
+
+It is added to the Schema as a new keyword through the ConstraintFactory.
+Much like builtin constraints,
+  context can be used to extract values from the schema json,
+  or to evaluate child schemas.
+``` c++
+jvalidate::ConstraintFactory factory{
+  {"my_keyword", [](auto const & context) {
+      return ExtensionConstraint::make<MyCustomConstraint>(...);
+   }},
+};
+```
+
+A validator is created as follows,
+  and then is passed in as the ExtensionVisitor argument to Validator.
+``` c++
+class Visitor : jvalidate::extension::Visitor<Visitor, MyCustomConstraint, ...> {
+public:
+  // One of these for every constraint in the template signature
+  Status visit(MyCustomConstraint const & cons,
+               jvalidate::Adapter auto const & document,
+               auto const &validator) const;
+};
+```
+
+In `tests/extension_test.cxx`,
+  an example demonstrating a json schema for a graph
+  which requires each edge's source and destination are
+  nodes in the graph.
+
+<!-- Footnotes/Appendix -->
+
+[1]: https://json-schema.org/specification
+[2]: https://json-schema.org/draft/2020-12
+[3]: https://json-schema.org/draft/2019-09
+[4]: https://json-schema.org/draft-07
+[5]: https://json-schema.org/draft-06
+[6]: https://json-schema.org/draft-05
+[7]: https://json-schema.org/draft-04/draft-zyp-json-schema-04
+[8]: https://json-schema.org/draft-03/draft-zyp-json-schema-03.pdf

+ 18 - 3
include/jvalidate/constraint.h

@@ -57,9 +57,10 @@ public:
    * becomes part of the language vocabulary, and what (if any) version it
    * leaves the vocabulary after.
    *
-   * To do this, we store an ordered map of Version enum onto Make (see above),
-   * and then use {@see std::map::lower_bound} to determine which Make object is
-   * the most approriate for the schema version being evaluated.
+   * To do this, we store an ordered map of Version enum onto
+   * {@see jvalidate::vocabulary::Metadata} and then use
+   * {@see std::map::lower_bound} to determine which Metadata object is the
+   * most appropriate for the schema version being evaluated.
    *
    * For example:
    * The "additionalProperties" constraint is the same across all versions, and
@@ -81,6 +82,20 @@ public:
    * sense to use different MakeConstraint functions for them.
    *   {"items", {{schema::Version::Earliest, &Self::itemsTupleOrVector},
    *              {schema::Version::Draft2020_12, &Self::additionalItems}}}
+   *
+   * Reserved keywords that have no meaning by themselves can use the Literal
+   * rule:
+   *   {"contains", {schema::Version::Draft06, &Self::contains}},
+   *   {"maxContains", {schema::Version::Draft06, Literal}},
+   *   {"minContains", {schema::Version::Draft06, Literal}},
+   *
+   * Since some special words like "default", "examples", "enum", etc. may
+   * contain objects which should not be evaluated as JSON schemas for things
+   * like "$id" tokens, the rules Keyword and KeywordMap allow marking only
+   * those keywords that are expected to hold more json schemas to be evaluated:
+   *   {"$defs", {schema::Version::Draft2019_09, KeywordMap}},
+   *   {"additionalProperties", {{&Self::additionalProperties, Keyword}}},
+   *   {"allOf", {schema::Version::Draft04, {&Self::allOf, Keyword}}},
    */
   struct Versioned {
     template <typename M = vocabulary::Metadata<A>>

+ 1 - 0
include/jvalidate/enum.h

@@ -29,6 +29,7 @@ enum class Version : int {
   // Formats: date-time, email, hostname, ipv4, ipv6, uri
   // https://json-schema.org/draft-04/schema
   Draft04,
+  Draft05 = Draft04, // draft-05 simply clarifies the terms of draft-04
 
   // New: "boolean as schema", propertyNames, contains, const
   // Changed: $ref, exclusiveMinimum, exclusiveMaximum, type, required, dependencies, examples