mirror of
https://github.com/Telecominfraproject/wlan-cloud-lib-valijson.git
synced 2026-01-27 02:22:38 +00:00
2325 lines
100 KiB
C++
2325 lines
100 KiB
C++
#pragma once
|
|
|
|
#include <stdexcept>
|
|
#include <iostream>
|
|
#include <vector>
|
|
#include <memory>
|
|
#include <functional>
|
|
|
|
#include <valijson/constraints/concrete_constraints.hpp>
|
|
#include <valijson/internal/adapter.hpp>
|
|
#include <valijson/internal/debug.hpp>
|
|
#include <valijson/internal/json_pointer.hpp>
|
|
#include <valijson/internal/json_reference.hpp>
|
|
#include <valijson/internal/uri.hpp>
|
|
#include <valijson/constraint_builder.hpp>
|
|
#include <valijson/schema.hpp>
|
|
#include <valijson/exceptions.hpp>
|
|
|
|
namespace valijson {
|
|
|
|
/**
|
|
* @brief Parser for populating a Schema based on a JSON Schema document.
|
|
*
|
|
* The SchemaParser class supports Drafts 3 and 4 of JSON Schema, however
|
|
* Draft 3 support should be considered deprecated.
|
|
*
|
|
* The functions provided by this class have been templated so that they can
|
|
* be used with different Adapter types.
|
|
*/
|
|
class SchemaParser
|
|
{
|
|
public:
|
|
/// Supported versions of JSON Schema
|
|
enum Version {
|
|
kDraft3, ///< @deprecated JSON Schema v3 has been superseded by v4
|
|
kDraft4,
|
|
kDraft7
|
|
};
|
|
|
|
/**
|
|
* @brief Construct a new SchemaParser for a given version of JSON Schema
|
|
*
|
|
* @param version Version of JSON Schema that will be expected
|
|
*/
|
|
explicit SchemaParser(const Version version = kDraft7)
|
|
: m_version(version) { }
|
|
|
|
/**
|
|
* @brief Release memory associated with custom ConstraintBuilders
|
|
*/
|
|
virtual ~SchemaParser()
|
|
{
|
|
for (const auto& entry : constraintBuilders) {
|
|
delete entry.second;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Struct to contain templated function type for fetching documents
|
|
*/
|
|
template<typename AdapterType>
|
|
struct FunctionPtrs
|
|
{
|
|
typedef typename adapters::AdapterTraits<AdapterType>::DocumentType DocumentType;
|
|
|
|
/// Templated function pointer type for fetching remote documents
|
|
typedef std::function<const DocumentType* (const std::string &uri)> FetchDoc;
|
|
|
|
/// Templated function pointer type for freeing fetched documents
|
|
typedef std::function<void (const DocumentType *)> FreeDoc;
|
|
};
|
|
|
|
/**
|
|
* @brief Add a custom contraint to this SchemaParser
|
|
|
|
* @param key name that will be used to identify relevant constraints
|
|
* while parsing a schema document
|
|
* @param builder pointer to a subclass of ConstraintBuilder that can
|
|
* parse custom constraints found in a schema document,
|
|
* and return an appropriate instance of Constraint; this
|
|
* class guarantees that it will take ownership of this
|
|
* pointer - unless this function throws an exception
|
|
*
|
|
* @todo consider accepting a list of custom ConstraintBuilders in
|
|
* constructor, so that this class remains immutable after
|
|
* construction
|
|
*
|
|
* @todo Add additional checks for key conflicts, empty keys, and
|
|
* potential restrictions relating to case sensitivity
|
|
*/
|
|
void addConstraintBuilder(const std::string &key, const ConstraintBuilder *builder)
|
|
{
|
|
constraintBuilders.push_back(std::make_pair(key, builder));
|
|
}
|
|
|
|
/**
|
|
* @brief Populate a Schema object from JSON Schema document
|
|
*
|
|
* When processing Draft 3 schemas, the parentSubschema and ownName pointers
|
|
* should be set in contexts where a 'required' constraint would be valid.
|
|
* These are used to add a RequiredConstraint object to the Schema that
|
|
* contains the required property.
|
|
*
|
|
* @param node Reference to node to parse
|
|
* @param schema Reference to Schema to populate
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
*/
|
|
template<typename AdapterType>
|
|
void populateSchema(
|
|
const AdapterType &node,
|
|
Schema &schema,
|
|
typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc = nullptr ,
|
|
typename FunctionPtrs<AdapterType>::FreeDoc freeDoc = nullptr )
|
|
{
|
|
if ((fetchDoc == nullptr ) ^ (freeDoc == nullptr)) {
|
|
throwRuntimeError("Remote document fetching can't be enabled without both fetch and free functions");
|
|
}
|
|
|
|
typename DocumentCache<AdapterType>::Type docCache;
|
|
SchemaCache schemaCache;
|
|
#if VALIJSON_USE_EXCEPTIONS
|
|
try {
|
|
#endif
|
|
resolveThenPopulateSchema(schema, node, node, schema, opt::optional<std::string>(), "", fetchDoc, nullptr,
|
|
nullptr, docCache, schemaCache);
|
|
#if VALIJSON_USE_EXCEPTIONS
|
|
} catch (...) {
|
|
freeDocumentCache<AdapterType>(docCache, freeDoc);
|
|
throw;
|
|
}
|
|
#endif
|
|
|
|
freeDocumentCache<AdapterType>(docCache, freeDoc);
|
|
}
|
|
|
|
private:
|
|
|
|
typedef std::vector<std::pair<std::string, const ConstraintBuilder *>>
|
|
ConstraintBuilders;
|
|
|
|
ConstraintBuilders constraintBuilders;
|
|
|
|
template<typename AdapterType>
|
|
struct DocumentCache
|
|
{
|
|
typedef typename adapters::AdapterTraits<AdapterType>::DocumentType DocumentType;
|
|
|
|
typedef std::map<std::string, const DocumentType*> Type;
|
|
};
|
|
|
|
typedef std::map<std::string, const Subschema *> SchemaCache;
|
|
|
|
/**
|
|
* @brief Free memory used by fetched documents
|
|
*
|
|
* If a custom 'free' function has not been provided, then the default
|
|
* delete operator will be used.
|
|
*
|
|
* @param docCache collection of fetched documents to free
|
|
* @param freeDoc optional custom free function
|
|
*/
|
|
template<typename AdapterType>
|
|
void freeDocumentCache(const typename DocumentCache<AdapterType>::Type
|
|
&docCache, typename FunctionPtrs<AdapterType>::FreeDoc freeDoc)
|
|
{
|
|
typedef typename DocumentCache<AdapterType>::Type DocCacheType;
|
|
|
|
for (const typename DocCacheType::value_type &v : docCache) {
|
|
freeDoc(v.second);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Find the complete URI for a document, within a resolution scope
|
|
*
|
|
* This function captures five different cases that can occur when
|
|
* attempting to resolve a document URI within a particular resolution
|
|
* scope:
|
|
*
|
|
* (1) resolution scope not present, but URN or absolute document URI is
|
|
* => document URI as-is
|
|
* (2) resolution scope not present, and document URI is relative or absent
|
|
* => document URI, if present, otherwise no result
|
|
* (3) resolution scope is present, and document URI is a relative path
|
|
* => resolve document URI relative to resolution scope
|
|
* (4) resolution scope is present, and document URI is absolute
|
|
* => document URI as-is
|
|
* (5) resolution scope is present, but document URI is not
|
|
* => resolution scope as-is
|
|
*
|
|
* This function assumes that the resolution scope is absolute.
|
|
*
|
|
* When resolving a document URI relative to the resolution scope, the
|
|
* document URI should be used to replace the path, query and fragment
|
|
* portions of URI provided by the resolution scope.
|
|
*/
|
|
virtual opt::optional<std::string> resolveDocumentUri(
|
|
const opt::optional<std::string>& resolutionScope,
|
|
const opt::optional<std::string>& documentUri)
|
|
{
|
|
if (resolutionScope) {
|
|
if (documentUri) {
|
|
if (internal::uri::isUriAbsolute(*documentUri) || internal::uri::isUrn(*documentUri)) {
|
|
// (4) resolution scope is present, and document URI is absolute
|
|
// => document URI as-is
|
|
return *documentUri;
|
|
} else {
|
|
// (3) resolution scope is present, and document URI is a relative path
|
|
// => resolve document URI relative to resolution scope
|
|
return internal::uri::resolveRelativeUri(*resolutionScope, *documentUri);
|
|
}
|
|
} else {
|
|
// (5) resolution scope is present, but document URI is not
|
|
// => resolution scope as-is
|
|
return *resolutionScope;
|
|
}
|
|
} else if (documentUri && internal::uri::isUriAbsolute(*documentUri)) {
|
|
// (1a) resolution scope not present, but absolute document URI is
|
|
// => document URI as-is
|
|
return *documentUri;
|
|
} else if (documentUri && internal::uri::isUrn(*documentUri)) {
|
|
// (1b) resolution scope not present, but URN is
|
|
// => document URI as-is
|
|
return *documentUri;
|
|
} else {
|
|
// (2) resolution scope not present, and document URI is relative or absent
|
|
// => document URI, if present, otherwise no result
|
|
// documentUri is already std::optional
|
|
return documentUri;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Extract a JSON Reference string from a node
|
|
*
|
|
* @param node node to extract the JSON Reference from
|
|
* @param result reference to string to set with the result
|
|
*
|
|
* @throws std::invalid_argument if node is an object containing a `$ref`
|
|
* property but with a value that cannot be interpreted as a string
|
|
*
|
|
* @return \c true if a JSON Reference was extracted; \c false otherwise
|
|
*/
|
|
template<typename AdapterType>
|
|
bool extractJsonReference(const AdapterType &node, std::string &result)
|
|
{
|
|
if (!node.isObject()) {
|
|
return false;
|
|
}
|
|
|
|
const typename AdapterType::Object o = node.getObject();
|
|
const typename AdapterType::Object::const_iterator itr = o.find("$ref");
|
|
if (itr == o.end()) {
|
|
return false;
|
|
} else if (!itr->second.getString(result)) {
|
|
throwRuntimeError("$ref property expected to contain string value.");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Sanitise an optional JSON Pointer, trimming trailing slashes
|
|
*/
|
|
static std::string sanitiseJsonPointer(const opt::optional<std::string>& input)
|
|
{
|
|
if (input) {
|
|
// Trim trailing slash(es)
|
|
std::string sanitised = *input;
|
|
sanitised.erase(sanitised.find_last_not_of('/') + 1,
|
|
std::string::npos);
|
|
|
|
return sanitised;
|
|
}
|
|
|
|
// If the JSON Pointer is not set, assume that the URI points to
|
|
// the root of the document
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* @brief Search the schema cache for a schema matching a given key
|
|
*
|
|
* If the key is not present in the query cache, a nullptr will be
|
|
* returned, and the contents of the cache will remain unchanged. This is
|
|
* in contrast to the behaviour of the std::map [] operator, which would
|
|
* add the nullptr to the cache.
|
|
*
|
|
* @param schemaCache schema cache to query
|
|
* @param queryKey key to search for
|
|
*
|
|
* @return shared pointer to Schema if found, nullptr otherwise
|
|
*/
|
|
static const Subschema * querySchemaCache(SchemaCache &schemaCache,
|
|
const std::string &queryKey)
|
|
{
|
|
const SchemaCache::iterator itr = schemaCache.find(queryKey);
|
|
if (itr == schemaCache.end()) {
|
|
return nullptr;
|
|
}
|
|
|
|
return itr->second;
|
|
}
|
|
|
|
/**
|
|
* @brief Add entries to the schema cache for a given list of keys
|
|
*
|
|
* @param schemaCache schema cache to update
|
|
* @param keysToCreate list of keys to create entries for
|
|
* @param schema shared pointer to schema that keys will map to
|
|
*
|
|
* @throws std::logic_error if any of the keys are already present in the
|
|
* schema cache. This behaviour is intended to help detect incorrect
|
|
* usage of the schema cache during development, and is not expected
|
|
* to occur otherwise, even for malformed schemas.
|
|
*/
|
|
static void updateSchemaCache(SchemaCache &schemaCache,
|
|
const std::vector<std::string> &keysToCreate,
|
|
const Subschema *schema)
|
|
{
|
|
for (const std::string &keyToCreate : keysToCreate) {
|
|
const SchemaCache::value_type value(keyToCreate, schema);
|
|
if (!schemaCache.insert(value).second) {
|
|
throwLogicError("Key '" + keyToCreate + "' already in schema cache.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Recursive helper function for retrieving or creating schemas
|
|
*
|
|
* This function will be applied recursively until a concrete node is found.
|
|
* A concrete node is a node that contains actual schema constraints rather
|
|
* than a JSON Reference.
|
|
*
|
|
* This termination condition may be trigged by visiting the concrete node
|
|
* at the end of a series of $ref nodes, or by finding a schema for one of
|
|
* those $ref nodes in the schema cache. An entry will be added to the
|
|
* schema cache for each node visited on the path to the concrete node.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and
|
|
* modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document
|
|
* @param node Reference to the node to parse
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param parentSchema Optional pointer to the parent schema, used to
|
|
* support required keyword in Draft 3
|
|
* @param ownName Optional pointer to a node name, used to support
|
|
* the 'required' keyword in Draft 3
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
* @param newCacheKeys A list of keys that should be added to the cache
|
|
* when recursion terminates
|
|
*/
|
|
template<typename AdapterType>
|
|
const Subschema * makeOrReuseSchema(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
const Subschema *parentSubschema,
|
|
const std::string *ownName,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache,
|
|
std::vector<std::string> &newCacheKeys)
|
|
{
|
|
std::string jsonRef;
|
|
|
|
// Check for the first termination condition (found a non-$ref node)
|
|
if (!extractJsonReference(node, jsonRef)) {
|
|
|
|
// Construct a key that we can use to search the schema cache for
|
|
// a schema corresponding to the current node
|
|
const std::string schemaCacheKey = currentScope ? (*currentScope + nodePath) : nodePath;
|
|
|
|
// Retrieve an existing schema from the cache if possible
|
|
const Subschema *cachedPtr = querySchemaCache(schemaCache, schemaCacheKey);
|
|
|
|
// Create a new schema otherwise
|
|
const Subschema *subschema = cachedPtr ? cachedPtr : rootSchema.createSubschema();
|
|
|
|
// Add cache entries for keys belonging to any $ref nodes that were
|
|
// visited before arriving at the current node
|
|
updateSchemaCache(schemaCache, newCacheKeys, subschema);
|
|
|
|
// Schema cache did not contain a pre-existing schema corresponding
|
|
// to the current node, so the schema that was returned will need
|
|
// to be populated
|
|
if (!cachedPtr) {
|
|
populateSchema(rootSchema, rootNode, node, *subschema,
|
|
currentScope, nodePath, fetchDoc, parentSubschema,
|
|
ownName, docCache, schemaCache);
|
|
}
|
|
|
|
return subschema;
|
|
}
|
|
|
|
// Returns a document URI if the reference points somewhere
|
|
// other than the current document
|
|
const opt::optional<std::string> documentUri = internal::json_reference::getJsonReferenceUri(jsonRef);
|
|
|
|
// Extract JSON Pointer from JSON Reference, with any trailing
|
|
// slashes removed so that keys in the schema cache end
|
|
// consistently
|
|
const std::string actualJsonPointer = sanitiseJsonPointer(
|
|
internal::json_reference::getJsonReferencePointer(jsonRef));
|
|
|
|
// Determine the actual document URI based on the resolution
|
|
// scope. An absolute document URI will take precedence when
|
|
// present, otherwise we need to resolve the URI relative to
|
|
// the current resolution scope
|
|
const opt::optional<std::string> actualDocumentUri = resolveDocumentUri(currentScope, documentUri);
|
|
|
|
// Construct a key to search the schema cache for an existing schema
|
|
const std::string queryKey = actualDocumentUri ? (*actualDocumentUri + actualJsonPointer) : actualJsonPointer;
|
|
|
|
// Check for the second termination condition (found a $ref node that
|
|
// already has an entry in the schema cache)
|
|
const Subschema *cachedPtr = querySchemaCache(schemaCache, queryKey);
|
|
if (cachedPtr) {
|
|
updateSchemaCache(schemaCache, newCacheKeys, cachedPtr);
|
|
return cachedPtr;
|
|
}
|
|
|
|
if (actualDocumentUri && (!currentScope || *actualDocumentUri != *currentScope)) {
|
|
const typename FunctionPtrs<AdapterType>::DocumentType *newDoc = nullptr;
|
|
|
|
// Have we seen this document before?
|
|
typename DocumentCache<AdapterType>::Type::iterator docCacheItr =
|
|
docCache.find(*actualDocumentUri);
|
|
if (docCacheItr == docCache.end()) {
|
|
// Resolve reference against remote document
|
|
if (!fetchDoc) {
|
|
throwRuntimeError("Fetching of remote JSON References not enabled.");
|
|
}
|
|
|
|
// Returns a pointer to the remote document that was
|
|
// retrieved, or null if retrieval failed. This class
|
|
// will take ownership of the pointer, and call freeDoc
|
|
// when it is no longer needed.
|
|
newDoc = fetchDoc(*actualDocumentUri);
|
|
|
|
// Can't proceed without the remote document
|
|
if (!newDoc) {
|
|
throwRuntimeError("Failed to fetch referenced schema document: " + *actualDocumentUri);
|
|
}
|
|
|
|
typedef typename DocumentCache<AdapterType>::Type::value_type
|
|
DocCacheValueType;
|
|
|
|
docCache.insert(DocCacheValueType(*actualDocumentUri, newDoc));
|
|
|
|
} else {
|
|
newDoc = docCacheItr->second;
|
|
}
|
|
|
|
const AdapterType newRootNode(*newDoc);
|
|
|
|
// Find where we need to be in the document
|
|
const AdapterType &referencedAdapter =
|
|
internal::json_pointer::resolveJsonPointer(newRootNode,
|
|
actualJsonPointer);
|
|
|
|
newCacheKeys.push_back(queryKey);
|
|
|
|
// Populate the schema, starting from the referenced node, with
|
|
// nested JSON References resolved relative to the new root node
|
|
return makeOrReuseSchema(rootSchema, newRootNode, referencedAdapter,
|
|
currentScope, actualJsonPointer, fetchDoc, parentSubschema,
|
|
ownName, docCache, schemaCache, newCacheKeys);
|
|
|
|
}
|
|
|
|
// JSON References in nested schema will be resolved relative to the
|
|
// current document
|
|
const AdapterType &referencedAdapter =
|
|
internal::json_pointer::resolveJsonPointer(
|
|
rootNode, actualJsonPointer);
|
|
|
|
newCacheKeys.push_back(queryKey);
|
|
|
|
// Populate the schema, starting from the referenced node, with
|
|
// nested JSON References resolved relative to the new root node
|
|
return makeOrReuseSchema(rootSchema, rootNode, referencedAdapter,
|
|
currentScope, actualJsonPointer, fetchDoc, parentSubschema,
|
|
ownName, docCache, schemaCache, newCacheKeys);
|
|
}
|
|
|
|
/**
|
|
* @brief Return pointer for the schema corresponding to a given node
|
|
*
|
|
* This function makes use of a schema cache, so that if the path to the
|
|
* current node is the same as one that has already been parsed and
|
|
* populated, a pointer to the existing Subschema will be returned.
|
|
*
|
|
* Should a series of $ref, or reference, nodes be resolved before reaching
|
|
* a concrete node, an entry will be added to the schema cache for each of
|
|
* the nodes in that path.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and
|
|
* modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document
|
|
* @param node Reference to the node to parse
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param parentSchema Optional pointer to the parent schema, used to
|
|
* support required keyword in Draft 3
|
|
* @param ownName Optional pointer to a node name, used to support
|
|
* the 'required' keyword in Draft 3
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*/
|
|
template<typename AdapterType>
|
|
const Subschema * makeOrReuseSchema(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
const Subschema *parentSubschema,
|
|
const std::string *ownName,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
std::vector<std::string> schemaCacheKeysToCreate;
|
|
|
|
return makeOrReuseSchema(rootSchema, rootNode, node, currentScope,
|
|
nodePath, fetchDoc, parentSubschema, ownName, docCache,
|
|
schemaCache, schemaCacheKeysToCreate);
|
|
}
|
|
|
|
/**
|
|
* @brief Populate a Schema object from JSON Schema document
|
|
*
|
|
* When processing Draft 3 schemas, the parentSubschema and ownName pointers
|
|
* should be set in contexts where a 'required' constraint would be valid.
|
|
* These are used to add a RequiredConstraint object to the Schema that
|
|
* contains the required property.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and
|
|
* modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document
|
|
* @param node Reference to node to parse
|
|
* @param schema Reference to Schema to populate
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Optional function to fetch remote JSON documents
|
|
* @param parentSubschema Optional pointer to the parent schema, used to
|
|
* support required keyword in Draft 3
|
|
* @param ownName Optional pointer to a node name, used to support
|
|
* the 'required' keyword in Draft 3
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*/
|
|
template<typename AdapterType>
|
|
void populateSchema(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const Subschema &subschema,
|
|
const opt::optional<std::string>& currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
const Subschema *parentSubschema,
|
|
const std::string *ownName,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
static_assert((std::is_convertible<AdapterType,
|
|
const valijson::adapters::Adapter &>::value),
|
|
"SchemaParser::populateSchema must be invoked with an "
|
|
"appropriate Adapter implementation");
|
|
|
|
if (!node.isObject()) {
|
|
if (m_version == kDraft7 && node.maybeBool()) {
|
|
// Boolean schema
|
|
if (!node.asBool()) {
|
|
rootSchema.setAlwaysInvalid(&subschema, true);
|
|
}
|
|
return;
|
|
} else {
|
|
std::string s;
|
|
s += "Expected node at ";
|
|
s += nodePath;
|
|
if (m_version == kDraft7) {
|
|
s += " to contain schema object or boolean value; actual node type is: ";
|
|
} else {
|
|
s += " to contain schema object; actual node type is: ";
|
|
}
|
|
s += internal::nodeTypeAsString(node);
|
|
throwRuntimeError(s);
|
|
}
|
|
}
|
|
|
|
const typename AdapterType::Object object = node.asObject();
|
|
typename AdapterType::Object::const_iterator itr(object.end());
|
|
|
|
// Check for 'id' attribute and update current scope
|
|
opt::optional<std::string> updatedScope;
|
|
if ((itr = object.find("id")) != object.end() && itr->second.maybeString()) {
|
|
const std::string id = itr->second.asString();
|
|
rootSchema.setSubschemaId(&subschema, itr->second.asString());
|
|
if (!currentScope || internal::uri::isUriAbsolute(id) || internal::uri::isUrn(id)) {
|
|
updatedScope = id;
|
|
} else {
|
|
updatedScope = internal::uri::resolveRelativeUri(*currentScope, id);
|
|
}
|
|
} else {
|
|
updatedScope = currentScope;
|
|
}
|
|
|
|
if ((itr = object.find("allOf")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeAllOfConstraint(rootSchema, rootNode, itr->second,
|
|
updatedScope, nodePath + "/allOf", fetchDoc,
|
|
docCache, schemaCache),
|
|
&subschema);
|
|
}
|
|
|
|
if ((itr = object.find("anyOf")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeAnyOfConstraint(rootSchema, rootNode, itr->second,
|
|
updatedScope, nodePath + "/anyOf", fetchDoc,
|
|
docCache, schemaCache),
|
|
&subschema);
|
|
}
|
|
|
|
if ((itr = object.find("const")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(makeConstConstraint(itr->second), &subschema);
|
|
}
|
|
|
|
if ((itr = object.find("contains")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeContainsConstraint(rootSchema, rootNode, itr->second,
|
|
updatedScope, nodePath + "/contains", fetchDoc,
|
|
docCache, schemaCache), &subschema);
|
|
}
|
|
|
|
if ((itr = object.find("dependencies")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeDependenciesConstraint(rootSchema, rootNode,
|
|
itr->second, updatedScope,
|
|
nodePath + "/dependencies", fetchDoc, docCache,
|
|
schemaCache),
|
|
&subschema);
|
|
}
|
|
|
|
if ((itr = object.find("description")) != object.end()) {
|
|
if (itr->second.maybeString()) {
|
|
rootSchema.setSubschemaDescription(&subschema,
|
|
itr->second.asString());
|
|
} else {
|
|
throwRuntimeError(
|
|
"'description' attribute should have a string value");
|
|
}
|
|
}
|
|
|
|
if ((itr = object.find("divisibleBy")) != object.end()) {
|
|
if (m_version == kDraft3) {
|
|
if (itr->second.maybeInteger()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMultipleOfIntConstraint(itr->second),
|
|
&subschema);
|
|
} else if (itr->second.maybeDouble()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMultipleOfDoubleConstraint(itr->second),
|
|
&subschema);
|
|
} else {
|
|
throwRuntimeError("Expected an numeric value for "
|
|
" 'divisibleBy' constraint.");
|
|
}
|
|
} else {
|
|
throwRuntimeError(
|
|
"'divisibleBy' constraint not valid after draft 3");
|
|
}
|
|
}
|
|
|
|
if ((itr = object.find("enum")) != object.end()) {
|
|
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");
|
|
|
|
if (object.end() != itemsItr) {
|
|
if (!itemsItr->second.isArray()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeSingularItemsConstraint(rootSchema, rootNode,
|
|
itemsItr->second, updatedScope,
|
|
nodePath + "/items", fetchDoc, docCache,
|
|
schemaCache),
|
|
&subschema);
|
|
|
|
} else {
|
|
const typename AdapterType::Object::const_iterator
|
|
additionalItemsItr = object.find("additionalItems");
|
|
rootSchema.addConstraintToSubschema(
|
|
makeLinearItemsConstraint(rootSchema, rootNode,
|
|
itemsItr != object.end() ? &itemsItr->second : nullptr,
|
|
additionalItemsItr != object.end() ? &additionalItemsItr->second : nullptr,
|
|
updatedScope, nodePath + "/items",
|
|
nodePath + "/additionalItems", fetchDoc,
|
|
docCache, schemaCache),
|
|
&subschema);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
const typename AdapterType::Object::const_iterator ifItr = object.find("if");
|
|
const typename AdapterType::Object::const_iterator thenItr = object.find("then");
|
|
const typename AdapterType::Object::const_iterator elseItr = object.find("else");
|
|
|
|
if (object.end() != ifItr) {
|
|
if (m_version == kDraft7) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeConditionalConstraint(rootSchema, rootNode,
|
|
ifItr->second,
|
|
thenItr == object.end() ? nullptr : &thenItr->second,
|
|
elseItr == object.end() ? nullptr : &elseItr->second,
|
|
updatedScope, nodePath, fetchDoc, docCache, schemaCache),
|
|
&subschema);
|
|
} else {
|
|
throwRuntimeError("Not supported");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_version == kDraft7) {
|
|
if ((itr = object.find("exclusiveMaximum")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMaximumConstraintExclusive(itr->second),
|
|
&subschema);
|
|
}
|
|
|
|
if ((itr = object.find("maximum")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMaximumConstraint<AdapterType>(itr->second, nullptr),
|
|
&subschema);
|
|
}
|
|
} else if ((itr = object.find("maximum")) != object.end()) {
|
|
typename AdapterType::Object::const_iterator exclusiveMaximumItr =
|
|
object.find("exclusiveMaximum");
|
|
if (exclusiveMaximumItr == object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMaximumConstraint<AdapterType>(itr->second, nullptr),
|
|
&subschema);
|
|
} else {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMaximumConstraint(itr->second, &exclusiveMaximumItr->second),
|
|
&subschema);
|
|
}
|
|
} else if (object.find("exclusiveMaximum") != object.end()) {
|
|
throwRuntimeError("'exclusiveMaximum' constraint only valid if a 'maximum' "
|
|
"constraint is also present");
|
|
}
|
|
|
|
if ((itr = object.find("maxItems")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMaxItemsConstraint(itr->second), &subschema);
|
|
}
|
|
|
|
if ((itr = object.find("maxLength")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMaxLengthConstraint(itr->second), &subschema);
|
|
}
|
|
|
|
if ((itr = object.find("maxProperties")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMaxPropertiesConstraint(itr->second), &subschema);
|
|
}
|
|
|
|
if (m_version == kDraft7) {
|
|
if ((itr = object.find("exclusiveMinimum")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMinimumConstraintExclusive(itr->second), &subschema);
|
|
}
|
|
|
|
if ((itr = object.find("minimum")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMinimumConstraint<AdapterType>(itr->second, nullptr),
|
|
&subschema);
|
|
}
|
|
} else if ((itr = object.find("minimum")) != object.end()) {
|
|
typename AdapterType::Object::const_iterator exclusiveMinimumItr = object.find("exclusiveMinimum");
|
|
if (exclusiveMinimumItr == object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMinimumConstraint<AdapterType>(itr->second, nullptr),
|
|
&subschema);
|
|
} else {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMinimumConstraint<AdapterType>(itr->second, &exclusiveMinimumItr->second),
|
|
&subschema);
|
|
}
|
|
} else if (object.find("exclusiveMinimum") != object.end()) {
|
|
throwRuntimeError("'exclusiveMinimum' constraint only valid if a 'minimum' "
|
|
"constraint is also present");
|
|
}
|
|
|
|
if ((itr = object.find("minItems")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMinItemsConstraint(itr->second), &subschema);
|
|
}
|
|
|
|
if ((itr = object.find("minLength")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMinLengthConstraint(itr->second), &subschema);
|
|
}
|
|
|
|
if ((itr = object.find("minProperties")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMinPropertiesConstraint(itr->second), &subschema);
|
|
}
|
|
|
|
if ((itr = object.find("multipleOf")) != object.end()) {
|
|
if (m_version == kDraft3) {
|
|
throwRuntimeError("'multipleOf' constraint not available in draft 3");
|
|
} else if (itr->second.maybeInteger()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMultipleOfIntConstraint(itr->second),
|
|
&subschema);
|
|
} else if (itr->second.maybeDouble()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeMultipleOfDoubleConstraint(itr->second),
|
|
&subschema);
|
|
} else {
|
|
throwRuntimeError("Expected an numeric value for 'divisibleBy' constraint.");
|
|
}
|
|
}
|
|
|
|
if ((itr = object.find("not")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeNotConstraint(rootSchema, rootNode, itr->second, updatedScope, nodePath + "/not", fetchDoc,
|
|
docCache, schemaCache),
|
|
&subschema);
|
|
}
|
|
|
|
if ((itr = object.find("oneOf")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeOneOfConstraint(rootSchema, rootNode, itr->second, updatedScope, nodePath + "/oneOf", fetchDoc,
|
|
docCache, schemaCache),
|
|
&subschema);
|
|
}
|
|
|
|
if ((itr = object.find("pattern")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makePatternConstraint(itr->second), &subschema);
|
|
}
|
|
|
|
{
|
|
// Check for schema keywords that require the creation of a
|
|
// PropertiesConstraint instance.
|
|
const typename AdapterType::Object::const_iterator
|
|
propertiesItr = object.find("properties"),
|
|
patternPropertiesItr = object.find("patternProperties"),
|
|
additionalPropertiesItr = object.find("additionalProperties");
|
|
if (object.end() != propertiesItr ||
|
|
object.end() != patternPropertiesItr ||
|
|
object.end() != additionalPropertiesItr) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makePropertiesConstraint(rootSchema, rootNode,
|
|
propertiesItr != object.end() ? &propertiesItr->second : nullptr,
|
|
patternPropertiesItr != object.end() ? &patternPropertiesItr->second : nullptr,
|
|
additionalPropertiesItr != object.end() ? &additionalPropertiesItr->second : nullptr,
|
|
updatedScope, nodePath + "/properties",
|
|
nodePath + "/patternProperties",
|
|
nodePath + "/additionalProperties",
|
|
fetchDoc, &subschema, docCache, schemaCache),
|
|
&subschema);
|
|
}
|
|
}
|
|
|
|
if ((itr = object.find("propertyNames")) != object.end()) {
|
|
if (m_version == kDraft7) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makePropertyNamesConstraint(rootSchema, rootNode, itr->second, updatedScope,
|
|
nodePath, fetchDoc, docCache, schemaCache),
|
|
&subschema);
|
|
} else {
|
|
throwRuntimeError("Not supported");
|
|
}
|
|
}
|
|
|
|
if ((itr = object.find("required")) != object.end()) {
|
|
if (m_version == kDraft3) {
|
|
if (parentSubschema && ownName) {
|
|
opt::optional<constraints::RequiredConstraint> constraint =
|
|
makeRequiredConstraintForSelf(itr->second, *ownName);
|
|
if (constraint) {
|
|
rootSchema.addConstraintToSubschema(*constraint, parentSubschema);
|
|
}
|
|
} else {
|
|
throwRuntimeError("'required' constraint not valid here");
|
|
}
|
|
} else {
|
|
rootSchema.addConstraintToSubschema(makeRequiredConstraint(itr->second), &subschema);
|
|
}
|
|
}
|
|
|
|
if ((itr = object.find("title")) != object.end()) {
|
|
if (itr->second.maybeString()) {
|
|
rootSchema.setSubschemaTitle(&subschema, itr->second.asString());
|
|
} else {
|
|
throwRuntimeError("'title' attribute should have a string value");
|
|
}
|
|
}
|
|
|
|
if ((itr = object.find("type")) != object.end()) {
|
|
rootSchema.addConstraintToSubschema(
|
|
makeTypeConstraint(rootSchema, rootNode, itr->second, updatedScope, nodePath + "/type", fetchDoc,
|
|
docCache, schemaCache),
|
|
&subschema);
|
|
}
|
|
|
|
if ((itr = object.find("uniqueItems")) != object.end()) {
|
|
opt::optional<constraints::UniqueItemsConstraint> constraint = makeUniqueItemsConstraint(itr->second);
|
|
if (constraint) {
|
|
rootSchema.addConstraintToSubschema(*constraint, &subschema);
|
|
}
|
|
}
|
|
|
|
for (const auto & constraintBuilder : constraintBuilders) {
|
|
if ((itr = object.find(constraintBuilder.first)) != object.end()) {
|
|
constraints::Constraint *constraint = nullptr;
|
|
#if VALIJSON_USE_EXCEPTIONS
|
|
try {
|
|
#endif
|
|
constraint = constraintBuilder.second->make(itr->second);
|
|
rootSchema.addConstraintToSubschema(*constraint, &subschema);
|
|
delete constraint;
|
|
#if VALIJSON_USE_EXCEPTIONS
|
|
} catch (...) {
|
|
delete constraint;
|
|
throw;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Resolves a chain of JSON References before populating a schema
|
|
*
|
|
* This helper function is used directly by the publicly visible
|
|
* populateSchema function. It ensures that the node being parsed is a
|
|
* concrete node, and not a JSON Reference. This function will call itself
|
|
* recursively to resolve references until a concrete node is found.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document
|
|
* @param node Reference to node to parse
|
|
* @param subschema Reference to Schema to populate
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param parentSchema Optional pointer to the parent schema, used to
|
|
* support required keyword in Draft 3
|
|
* @param ownName Optional pointer to a node name, used to support
|
|
* the 'required' keyword in Draft 3
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*/
|
|
template<typename AdapterType>
|
|
void resolveThenPopulateSchema(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const Subschema &subschema,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
const Subschema *parentSchema,
|
|
const std::string *ownName,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
std::string jsonRef;
|
|
if (!extractJsonReference(node, jsonRef)) {
|
|
populateSchema(rootSchema, rootNode, node, subschema, currentScope, nodePath, fetchDoc, parentSchema,
|
|
ownName, docCache, schemaCache);
|
|
return;
|
|
}
|
|
|
|
// Returns a document URI if the reference points somewhere
|
|
// other than the current document
|
|
const opt::optional<std::string> documentUri = internal::json_reference::getJsonReferenceUri(jsonRef);
|
|
|
|
// Extract JSON Pointer from JSON Reference
|
|
const std::string actualJsonPointer = sanitiseJsonPointer(
|
|
internal::json_reference::getJsonReferencePointer(jsonRef));
|
|
|
|
if (documentUri && (internal::uri::isUriAbsolute(*documentUri) || internal::uri::isUrn(*documentUri))) {
|
|
// Resolve reference against remote document
|
|
if (!fetchDoc) {
|
|
throwRuntimeError("Fetching of remote JSON References not enabled.");
|
|
}
|
|
|
|
const typename DocumentCache<AdapterType>::DocumentType *newDoc = fetchDoc(*documentUri);
|
|
|
|
// Can't proceed without the remote document
|
|
if (!newDoc) {
|
|
throwRuntimeError("Failed to fetch referenced schema document: " + *documentUri);
|
|
}
|
|
|
|
// Add to document cache
|
|
typedef typename DocumentCache<AdapterType>::Type::value_type DocCacheValueType;
|
|
|
|
docCache.insert(DocCacheValueType(*documentUri, newDoc));
|
|
|
|
const AdapterType newRootNode(*newDoc);
|
|
|
|
const AdapterType &referencedAdapter =
|
|
internal::json_pointer::resolveJsonPointer(newRootNode, actualJsonPointer);
|
|
|
|
// TODO: Need to detect degenerate circular references
|
|
resolveThenPopulateSchema(rootSchema, newRootNode, referencedAdapter, subschema, {}, actualJsonPointer,
|
|
fetchDoc, parentSchema, ownName, docCache, schemaCache);
|
|
|
|
} else {
|
|
const AdapterType &referencedAdapter =
|
|
internal::json_pointer::resolveJsonPointer(rootNode, actualJsonPointer);
|
|
|
|
// TODO: Need to detect degenerate circular references
|
|
resolveThenPopulateSchema(rootSchema, rootNode, referencedAdapter, subschema, {}, actualJsonPointer,
|
|
fetchDoc, parentSchema, ownName, docCache, schemaCache);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new AllOfConstraint object
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document; used for recursive parsing of schemas
|
|
* @param node JSON node containing an array of child schemas
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new AllOfConstraint object that belongs to the
|
|
* caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::AllOfConstraint makeAllOfConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
if (!node.maybeArray()) {
|
|
throwRuntimeError("Expected array value for 'allOf' constraint.");
|
|
}
|
|
|
|
constraints::AllOfConstraint constraint;
|
|
|
|
int index = 0;
|
|
for (const AdapterType schemaNode : node.asArray()) {
|
|
if (schemaNode.maybeObject() || (m_version == kDraft7 && schemaNode.isBool())) {
|
|
const std::string childPath = nodePath + "/" + std::to_string(index);
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, schemaNode, currentScope,
|
|
childPath, fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraint.addSubschema(subschema);
|
|
index++;
|
|
} else {
|
|
throwRuntimeError("Expected element to be a valid schema in 'allOf' constraint.");
|
|
}
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new AnyOfConstraint object
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document; used for recursive parsing of schemas
|
|
* @param node JSON node containing an array of child schemas
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new AnyOfConstraint object that belongs to the
|
|
* caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::AnyOfConstraint makeAnyOfConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
if (!node.maybeArray()) {
|
|
throwRuntimeError("Expected array value for 'anyOf' constraint.");
|
|
}
|
|
|
|
constraints::AnyOfConstraint constraint;
|
|
|
|
int index = 0;
|
|
for (const AdapterType schemaNode : node.asArray()) {
|
|
if (schemaNode.maybeObject() || (m_version == kDraft7 && schemaNode.isBool())) {
|
|
const std::string childPath = nodePath + "/" + std::to_string(index);
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, schemaNode, currentScope,
|
|
childPath, fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraint.addSubschema(subschema);
|
|
index++;
|
|
} else {
|
|
throwRuntimeError("Expected array element to be a valid schema in 'anyOf' constraint.");
|
|
}
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new ConditionalConstraint object.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema,
|
|
* through which other subschemas can be
|
|
* created and modified
|
|
* @param rootNode Reference to the node from which JSON
|
|
* References will be resolved when they refer
|
|
* to the current document; used for recursive
|
|
* parsing of schemas
|
|
* @param ifNode Schema that will be used to evaluate the
|
|
* conditional.
|
|
* @param thenNode Optional pointer to a JSON node containing
|
|
* a schema that will be used when the conditional
|
|
* evaluates to true.
|
|
* @param elseNode Optional pointer to a JSON node containing
|
|
* a schema that will be used when the conditional
|
|
* evaluates to false.
|
|
* @param currentScope URI for current resolution scope
|
|
* @param containsPath JSON Pointer representing the path to
|
|
* the 'contains' node
|
|
* @param fetchDoc Function to fetch remote JSON documents
|
|
* (optional)
|
|
* @param docCache Cache of resolved and fetched remote
|
|
* documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new ContainsConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::ConditionalConstraint makeConditionalConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &ifNode,
|
|
const AdapterType *thenNode,
|
|
const AdapterType *elseNode,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
constraints::ConditionalConstraint constraint;
|
|
|
|
const Subschema *ifSubschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, ifNode, currentScope,
|
|
nodePath + "/if", fetchDoc, nullptr, nullptr, docCache,
|
|
schemaCache);
|
|
constraint.setIfSubschema(ifSubschema);
|
|
|
|
if (thenNode) {
|
|
const Subschema *thenSubschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, *thenNode, currentScope, nodePath + "/then", fetchDoc, nullptr,
|
|
nullptr, docCache, schemaCache);
|
|
constraint.setThenSubschema(thenSubschema);
|
|
}
|
|
|
|
if (elseNode) {
|
|
const Subschema *elseSubschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, *elseNode, currentScope, nodePath + "/else", fetchDoc, nullptr,
|
|
nullptr, docCache, schemaCache);
|
|
constraint.setElseSubschema(elseSubschema);
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new ConstConstraint object.
|
|
*
|
|
* @param node JSON node containing an arbitrary value
|
|
*
|
|
* @return pointer to a new MinimumConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::ConstConstraint makeConstConstraint(const AdapterType &node)
|
|
{
|
|
constraints::ConstConstraint constraint;
|
|
constraint.setValue(node);
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new ContainsConstraint object.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema,
|
|
* through which other subschemas can be
|
|
* created and modified
|
|
* @param rootNode Reference to the node from which JSON
|
|
* References will be resolved when they refer
|
|
* to the current document; used for recursive
|
|
* parsing of schemas
|
|
* @param contains Optional pointer to a JSON node containing
|
|
* an object mapping property names to
|
|
* schemas.
|
|
* @param currentScope URI for current resolution scope
|
|
* @param containsPath JSON Pointer representing the path to
|
|
* the 'contains' node
|
|
* @param fetchDoc Function to fetch remote JSON documents
|
|
* (optional)
|
|
* @param docCache Cache of resolved and fetched remote
|
|
* documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new ContainsConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::ContainsConstraint makeContainsConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &contains,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &containsPath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
constraints::ContainsConstraint constraint;
|
|
|
|
if (contains.isObject() || (m_version == kDraft7 && contains.maybeBool())) {
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, contains, currentScope, containsPath,
|
|
fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraint.setSubschema(subschema);
|
|
|
|
} else if (contains.maybeObject()) {
|
|
// If a loosely-typed Adapter type is being used, then we'll
|
|
// assume that an empty schema has been provided.
|
|
constraint.setSubschema(rootSchema.emptySubschema());
|
|
|
|
} else {
|
|
// All other formats will result in an exception being thrown.
|
|
throwRuntimeError("Expected valid schema for 'contains' constraint.");
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new DependenciesConstraint object
|
|
*
|
|
* The dependencies for a property can be defined several ways. When parsing
|
|
* a Draft 4 schema, the following can be used:
|
|
* - an array that lists the name of each property that must be present
|
|
* if the dependent property is present
|
|
* - an object that specifies a schema which must be satisfied if the
|
|
* dependent property is present
|
|
*
|
|
* When parsing a Draft 3 schema, in addition to the formats above, the
|
|
* following format can be used:
|
|
* - a string that names a single property that must be present if the
|
|
* dependent property is presnet
|
|
*
|
|
* Multiple methods can be used in the same dependency constraint.
|
|
*
|
|
* If the format of any part of the the dependency node does not match one
|
|
* of these formats, an exception will be thrown.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document; used for recursive parsing of schemas
|
|
* @param node JSON node containing an object that defines a
|
|
* mapping of properties to their dependencies.
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new DependencyConstraint that belongs to the
|
|
* caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::DependenciesConstraint makeDependenciesConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
if (!node.maybeObject()) {
|
|
throwRuntimeError("Expected valid subschema for 'dependencies' constraint.");
|
|
}
|
|
|
|
constraints::DependenciesConstraint dependenciesConstraint;
|
|
|
|
// Process each of the dependency mappings defined by the object
|
|
for (const typename AdapterType::ObjectMember member : node.asObject()) {
|
|
|
|
// First, we attempt to parse the value of the dependency mapping
|
|
// as an array of strings. If the Adapter type does not support
|
|
// strict types, then an empty string or empty object will be cast
|
|
// to an array, and the resulting dependency list will be empty.
|
|
// This is equivalent to using an empty object, but does mean that
|
|
// if the user provides an actual string then this error will not
|
|
// be detected.
|
|
if (member.second.maybeArray()) {
|
|
// Parse an array of dependency names
|
|
std::vector<std::string> dependentPropertyNames;
|
|
for (const AdapterType dependencyName : member.second.asArray()) {
|
|
if (dependencyName.maybeString()) {
|
|
dependentPropertyNames.push_back(dependencyName.getString());
|
|
} else {
|
|
throwRuntimeError("Expected string value in dependency list of property '" +
|
|
member.first + "' in 'dependencies' constraint.");
|
|
}
|
|
}
|
|
|
|
dependenciesConstraint.addPropertyDependencies(member.first,
|
|
dependentPropertyNames);
|
|
|
|
// If the value of dependency mapping could not be processed as an
|
|
// array, we'll try to process it as an object instead. Note that
|
|
// strict type comparison is used here, since we've already
|
|
// exercised the flexibility by loosely-typed Adapter types. If the
|
|
// value of the dependency mapping is an object, then we'll try to
|
|
// process it as a dependent schema.
|
|
} else if (member.second.isObject() || (m_version == kDraft7 && member.second.maybeBool())) {
|
|
// Parse dependent subschema
|
|
const Subschema *childSubschema =
|
|
makeOrReuseSchema<AdapterType>(rootSchema, rootNode,
|
|
member.second, currentScope, nodePath, fetchDoc,
|
|
nullptr, nullptr, docCache, schemaCache);
|
|
dependenciesConstraint.addSchemaDependency(member.first,
|
|
childSubschema);
|
|
|
|
// If we're supposed to be parsing a Draft3 schema, then the value
|
|
// of the dependency mapping can also be a string containing the
|
|
// name of a single dependency.
|
|
} else if (m_version == kDraft3 && member.second.isString()) {
|
|
dependenciesConstraint.addPropertyDependency(member.first,
|
|
member.second.getString());
|
|
|
|
// All other types result in an exception being thrown.
|
|
} else {
|
|
throwRuntimeError("Invalid dependencies definition.");
|
|
}
|
|
}
|
|
|
|
return dependenciesConstraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new EnumConstraint object.
|
|
*
|
|
* @param node JSON node containing an array of values permitted by the
|
|
* constraint.
|
|
*
|
|
* @return pointer to a new EnumConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::EnumConstraint makeEnumConstraint(
|
|
const AdapterType &node)
|
|
{
|
|
// Make a copy of each value in the enum array
|
|
constraints::EnumConstraint constraint;
|
|
for (const AdapterType value : node.getArray()) {
|
|
constraint.addValue(value);
|
|
}
|
|
|
|
/// @todo This will make another copy of the values while constructing
|
|
/// the EnumConstraint. Move semantics in C++11 should make it possible
|
|
/// to avoid these copies without complicating the implementation of the
|
|
/// EnumConstraint class.
|
|
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<typename AdapterType>
|
|
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.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema,
|
|
* through which other subschemas can be
|
|
* created and modified
|
|
* @param rootNode Reference to the node from which JSON
|
|
* References will be resolved when they refer
|
|
* to the current document; used for recursive
|
|
* parsing of schemas
|
|
* @param items Optional pointer to a JSON node containing
|
|
* an object mapping property names to
|
|
* schemas.
|
|
* @param additionalItems Optional pointer to a JSON node containing
|
|
* an additional properties schema or a
|
|
* boolean value.
|
|
* @param currentScope URI for current resolution scope
|
|
* @param itemsPath JSON Pointer representing the path to
|
|
* the 'items' node
|
|
* @param additionalItemsPath JSON Pointer representing the path to
|
|
* the 'additionalItems' node
|
|
* @param fetchDoc Function to fetch remote JSON documents
|
|
* (optional)
|
|
* @param docCache Cache of resolved and fetched remote
|
|
* documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new ItemsConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::LinearItemsConstraint makeLinearItemsConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType *items,
|
|
const AdapterType *additionalItems,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &itemsPath,
|
|
const std::string &additionalItemsPath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
constraints::LinearItemsConstraint constraint;
|
|
|
|
// Construct a Schema object for the the additionalItems constraint,
|
|
// if the additionalItems property is present
|
|
if (additionalItems) {
|
|
if (additionalItems->maybeBool()) {
|
|
// If the value of the additionalItems property is a boolean
|
|
// and is set to true, then additional array items do not need
|
|
// to satisfy any constraints.
|
|
if (additionalItems->asBool()) {
|
|
constraint.setAdditionalItemsSubschema(rootSchema.emptySubschema());
|
|
}
|
|
} else if (additionalItems->maybeObject()) {
|
|
// If the value of the additionalItems property is an object,
|
|
// then it should be parsed into a Schema object, which will be
|
|
// used to validate additional array items.
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, *additionalItems, currentScope,
|
|
additionalItemsPath, fetchDoc, nullptr, nullptr, docCache,
|
|
schemaCache);
|
|
constraint.setAdditionalItemsSubschema(subschema);
|
|
} else {
|
|
// Any other format for the additionalItems property will result
|
|
// in an exception being thrown.
|
|
throwRuntimeError("Expected bool or object value for 'additionalItems'");
|
|
}
|
|
} else {
|
|
// The default value for the additionalItems property is an empty
|
|
// object, which means that additional array items do not need to
|
|
// satisfy any constraints.
|
|
constraint.setAdditionalItemsSubschema(rootSchema.emptySubschema());
|
|
}
|
|
|
|
// Construct a Schema object for each item in the items array.
|
|
// If the items constraint is not provided, then array items
|
|
// will be validated against the additionalItems schema.
|
|
if (items) {
|
|
if (items->isArray()) {
|
|
// If the items constraint contains an array, then it should
|
|
// contain a list of child schemas which will be used to
|
|
// validate the values at the corresponding indexes in a target
|
|
// array.
|
|
int index = 0;
|
|
for (const AdapterType v : items->getArray()) {
|
|
const std::string childPath = itemsPath + "/" +
|
|
std::to_string(index);
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, v, currentScope, childPath,
|
|
fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraint.addItemSubschema(subschema);
|
|
index++;
|
|
}
|
|
} else {
|
|
throwRuntimeError("Expected array value for non-singular 'items' constraint.");
|
|
}
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new ItemsConstraint object.
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema,
|
|
* through which other subschemas can be
|
|
* created and modified
|
|
* @param rootNode Reference to the node from which JSON
|
|
* References will be resolved when they refer
|
|
* to the current document; used for recursive
|
|
* parsing of schemas
|
|
* @param items Optional pointer to a JSON node containing
|
|
* an object mapping property names to
|
|
* schemas.
|
|
* @param additionalItems Optional pointer to a JSON node containing
|
|
* an additional properties schema or a
|
|
* boolean value.
|
|
* @param currentScope URI for current resolution scope
|
|
* @param itemsPath JSON Pointer representing the path to
|
|
* the 'items' node
|
|
* @param additionalItemsPath JSON Pointer representing the path to
|
|
* the 'additionalItems' node
|
|
* @param fetchDoc Function to fetch remote JSON documents
|
|
* (optional)
|
|
* @param docCache Cache of resolved and fetched remote
|
|
* documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new ItemsConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::SingularItemsConstraint makeSingularItemsConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &items,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &itemsPath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
constraints::SingularItemsConstraint constraint;
|
|
|
|
// Construct a Schema object for each item in the items array, if an
|
|
// array is provided, or a single Schema object, in an object value is
|
|
// provided. If the items constraint is not provided, then array items
|
|
// will be validated against the additionalItems schema.
|
|
if (items.isObject() || (m_version == kDraft7 && items.maybeBool())) {
|
|
// If the items constraint contains an object value, then it
|
|
// should contain a Schema that will be used to validate all
|
|
// items in a target array. Any schema defined by the
|
|
// additionalItems constraint will be ignored.
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, items, currentScope, itemsPath,
|
|
fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraint.setItemsSubschema(subschema);
|
|
|
|
} else if (items.maybeObject()) {
|
|
// If a loosely-typed Adapter type is being used, then we'll
|
|
// assume that an empty schema has been provided.
|
|
constraint.setItemsSubschema(rootSchema.emptySubschema());
|
|
|
|
} else {
|
|
// All other formats will result in an exception being thrown.
|
|
throwRuntimeError("Expected valid schema for singular 'items' constraint.");
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MaximumConstraint object (draft 3 and 4).
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema,
|
|
* through which other subschemas can be
|
|
* created and modified
|
|
* @param rootNode Reference to the node from which JSON
|
|
* References will be resolved when they refer
|
|
* to the current document; used for recursive
|
|
* parsing of schemas
|
|
* @param node JSON node containing the maximum value.
|
|
* @param exclusiveMaximum Optional pointer to a JSON boolean value that
|
|
* indicates whether maximum value is excluded
|
|
* from the range of permitted values.
|
|
*
|
|
* @return pointer to a new MaximumConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MaximumConstraint makeMaximumConstraint(
|
|
const AdapterType &node,
|
|
const AdapterType *exclusiveMaximum)
|
|
{
|
|
if (!node.maybeDouble()) {
|
|
throwRuntimeError("Expected numeric value for maximum constraint.");
|
|
}
|
|
|
|
constraints::MaximumConstraint constraint;
|
|
constraint.setMaximum(node.asDouble());
|
|
|
|
if (exclusiveMaximum) {
|
|
if (!exclusiveMaximum->maybeBool()) {
|
|
throwRuntimeError("Expected boolean value for exclusiveMaximum constraint.");
|
|
}
|
|
|
|
constraint.setExclusiveMaximum(exclusiveMaximum->asBool());
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MaximumConstraint object that is always exclusive (draft 7).
|
|
*
|
|
* @param node JSON node containing an integer, representing the maximum value.
|
|
*
|
|
* @param exclusive Optional pointer to a JSON boolean value that indicates whether the
|
|
* maximum value is excluded from the range of permitted values.
|
|
*
|
|
* @return pointer to a new Maximum that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MaximumConstraint makeMaximumConstraintExclusive(const AdapterType &node)
|
|
{
|
|
if (!node.maybeDouble()) {
|
|
throwRuntimeError("Expected numeric value for exclusiveMaximum constraint.");
|
|
}
|
|
|
|
constraints::MaximumConstraint constraint;
|
|
constraint.setMaximum(node.asDouble());
|
|
constraint.setExclusiveMaximum(true);
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MaxItemsConstraint object.
|
|
*
|
|
* @param node JSON node containing an integer value representing the
|
|
* maximum number of items that may be contaned by an array.
|
|
*
|
|
* @return pointer to a new MaxItemsConstraint that belongs to the caller.
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MaxItemsConstraint makeMaxItemsConstraint(
|
|
const AdapterType &node)
|
|
{
|
|
if (node.maybeInteger()) {
|
|
const int64_t value = node.asInteger();
|
|
if (value >= 0) {
|
|
constraints::MaxItemsConstraint constraint;
|
|
constraint.setMaxItems(value);
|
|
return constraint;
|
|
}
|
|
}
|
|
|
|
throwRuntimeError("Expected non-negative integer value for 'maxItems' constraint.");
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MaxLengthConstraint object.
|
|
*
|
|
* @param node JSON node containing an integer value representing the
|
|
* maximum length of a string.
|
|
*
|
|
* @return pointer to a new MaxLengthConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MaxLengthConstraint makeMaxLengthConstraint(
|
|
const AdapterType &node)
|
|
{
|
|
if (node.maybeInteger()) {
|
|
const int64_t value = node.asInteger();
|
|
if (value >= 0) {
|
|
constraints::MaxLengthConstraint constraint;
|
|
constraint.setMaxLength(value);
|
|
return constraint;
|
|
}
|
|
}
|
|
|
|
throwRuntimeError("Expected a non-negative integer value for 'maxLength' constraint.");
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MaxPropertiesConstraint object.
|
|
*
|
|
* @param node JSON node containing an integer value representing the
|
|
* maximum number of properties that may be contained by an
|
|
* object.
|
|
*
|
|
* @return pointer to a new MaxPropertiesConstraint that belongs to the
|
|
* caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MaxPropertiesConstraint makeMaxPropertiesConstraint(
|
|
const AdapterType &node)
|
|
{
|
|
if (node.maybeInteger()) {
|
|
int64_t value = node.asInteger();
|
|
if (value >= 0) {
|
|
constraints::MaxPropertiesConstraint constraint;
|
|
constraint.setMaxProperties(value);
|
|
return constraint;
|
|
}
|
|
}
|
|
|
|
throwRuntimeError("Expected a non-negative integer for 'maxProperties' constraint.");
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MinimumConstraint object (draft 3 and 4).
|
|
*
|
|
* @param node JSON node containing an integer, representing
|
|
* the minimum value.
|
|
*
|
|
* @param exclusiveMaximum Optional pointer to a JSON boolean value that
|
|
* indicates whether the minimum value is
|
|
* excluded from the range of permitted values.
|
|
*
|
|
* @return pointer to a new MinimumConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MinimumConstraint makeMinimumConstraint(
|
|
const AdapterType &node,
|
|
const AdapterType *exclusiveMinimum)
|
|
{
|
|
if (!node.maybeDouble()) {
|
|
throwRuntimeError("Expected numeric value for minimum constraint.");
|
|
}
|
|
|
|
constraints::MinimumConstraint constraint;
|
|
constraint.setMinimum(node.asDouble());
|
|
|
|
if (exclusiveMinimum) {
|
|
if (!exclusiveMinimum->maybeBool()) {
|
|
throwRuntimeError("Expected boolean value for 'exclusiveMinimum' constraint.");
|
|
}
|
|
|
|
constraint.setExclusiveMinimum(exclusiveMinimum->asBool());
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MinimumConstraint object that is always exclusive (draft 7).
|
|
*
|
|
* @param node JSON node containing an integer, representing the minimum value.
|
|
*
|
|
* @param exclusive Optional pointer to a JSON boolean value that indicates whether the
|
|
* minimum value is excluded from the range of permitted values.
|
|
*
|
|
* @return pointer to a new MinimumConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MinimumConstraint makeMinimumConstraintExclusive(const AdapterType &node)
|
|
{
|
|
if (!node.maybeDouble()) {
|
|
throwRuntimeError("Expected numeric value for exclusiveMinimum constraint.");
|
|
}
|
|
|
|
constraints::MinimumConstraint constraint;
|
|
constraint.setMinimum(node.asDouble());
|
|
constraint.setExclusiveMinimum(true);
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MinItemsConstraint object.
|
|
*
|
|
* @param node JSON node containing an integer value representing the
|
|
* minimum number of items that may be contained by an array.
|
|
*
|
|
* @return pointer to a new MinItemsConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MinItemsConstraint makeMinItemsConstraint(const AdapterType &node)
|
|
{
|
|
if (node.maybeInteger()) {
|
|
const int64_t value = node.asInteger();
|
|
if (value >= 0) {
|
|
constraints::MinItemsConstraint constraint;
|
|
constraint.setMinItems(value);
|
|
return constraint;
|
|
}
|
|
}
|
|
|
|
throwRuntimeError("Expected a non-negative integer value for 'minItems' constraint.");
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MinLengthConstraint object.
|
|
*
|
|
* @param node JSON node containing an integer value representing the
|
|
* minimum length of a string.
|
|
*
|
|
* @return pointer to a new MinLengthConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MinLengthConstraint makeMinLengthConstraint(const AdapterType &node)
|
|
{
|
|
if (node.maybeInteger()) {
|
|
const int64_t value = node.asInteger();
|
|
if (value >= 0) {
|
|
constraints::MinLengthConstraint constraint;
|
|
constraint.setMinLength(value);
|
|
return constraint;
|
|
}
|
|
}
|
|
|
|
throwRuntimeError("Expected a non-negative integer value for 'minLength' constraint.");
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief Make a new MaxPropertiesConstraint object.
|
|
*
|
|
* @param node JSON node containing an integer value representing the
|
|
* minimum number of properties that may be contained by an
|
|
* object.
|
|
*
|
|
* @return pointer to a new MinPropertiesConstraint that belongs to the
|
|
* caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MinPropertiesConstraint makeMinPropertiesConstraint(const AdapterType &node)
|
|
{
|
|
if (node.maybeInteger()) {
|
|
int64_t value = node.asInteger();
|
|
if (value >= 0) {
|
|
constraints::MinPropertiesConstraint constraint;
|
|
constraint.setMinProperties(value);
|
|
return constraint;
|
|
}
|
|
}
|
|
|
|
throwRuntimeError("Expected a non-negative integer for 'minProperties' constraint.");
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MultipleOfDoubleConstraint object
|
|
*
|
|
* @param node JSON node containing an numeric value that a target value
|
|
* must divide by in order to satisfy this constraint
|
|
*
|
|
* @return a MultipleOfConstraint
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MultipleOfDoubleConstraint makeMultipleOfDoubleConstraint(const AdapterType &node)
|
|
{
|
|
constraints::MultipleOfDoubleConstraint constraint;
|
|
constraint.setDivisor(node.asDouble());
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new MultipleOfIntConstraint object
|
|
*
|
|
* @param node JSON node containing a numeric value that a target value
|
|
* must divide by in order to satisfy this constraint
|
|
*
|
|
* @return a MultipleOfIntConstraint
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::MultipleOfIntConstraint makeMultipleOfIntConstraint(const AdapterType &node)
|
|
{
|
|
constraints::MultipleOfIntConstraint constraint;
|
|
constraint.setDivisor(node.asInteger());
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new NotConstraint object
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document; used for recursive parsing of schemas
|
|
* @param node JSON node containing a schema
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new NotConstraint object that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::NotConstraint makeNotConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
if (node.maybeObject() || (m_version == kDraft7 && node.maybeBool())) {
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, node, currentScope, nodePath,
|
|
fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraints::NotConstraint constraint;
|
|
constraint.setSubschema(subschema);
|
|
return constraint;
|
|
}
|
|
|
|
throwRuntimeError("Expected object value for 'not' constraint.");
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new OneOfConstraint object
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document; used for recursive parsing of schemas
|
|
* @param node JSON node containing an array of child schemas
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new OneOfConstraint that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::OneOfConstraint makeOneOfConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
constraints::OneOfConstraint constraint;
|
|
|
|
int index = 0;
|
|
for (const AdapterType schemaNode : node.getArray()) {
|
|
const std::string childPath = nodePath + "/" + std::to_string(index);
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, schemaNode, currentScope, childPath,
|
|
fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraint.addSubschema(subschema);
|
|
index++;
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new PatternConstraint object.
|
|
*
|
|
* @param node JSON node containing a pattern string
|
|
*
|
|
* @return pointer to a new PatternConstraint object that belongs to the
|
|
* caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::PatternConstraint makePatternConstraint(
|
|
const AdapterType &node)
|
|
{
|
|
constraints::PatternConstraint constraint;
|
|
constraint.setPattern(node.getString());
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new Properties object.
|
|
*
|
|
* @param rootSchema The Schema instance, and root
|
|
* subschema, through which other
|
|
* subschemas can be created and modified
|
|
* @param rootNode Reference to the node from which JSON
|
|
* References will be resolved when they
|
|
* refer to the current document; used
|
|
* for recursive parsing of schemas
|
|
* @param properties Optional pointer to a JSON node
|
|
* containing an object mapping property
|
|
* names to schemas.
|
|
* @param patternProperties Optional pointer to a JSON node
|
|
* containing an object mapping pattern
|
|
* property names to schemas.
|
|
* @param additionalProperties Optional pointer to a JSON node
|
|
* containing an additional properties
|
|
* schema or a boolean value.
|
|
* @param currentScope URI for current resolution scope
|
|
* @param propertiesPath JSON Pointer representing the path to
|
|
* the 'properties' node
|
|
* @param patternPropertiesPath JSON Pointer representing the path to
|
|
* the 'patternProperties' node
|
|
* @param additionalPropertiesPath JSON Pointer representing the path to
|
|
* the 'additionalProperties' node
|
|
* @param fetchDoc Function to fetch remote JSON
|
|
* documents (optional)
|
|
* @param parentSubschema Optional pointer to the Schema of the
|
|
* parent object, needed to support the
|
|
* 'required' keyword in Draft 3
|
|
* @param docCache Cache of resolved and fetched remote
|
|
* documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new Properties that belongs to the caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::PropertiesConstraint makePropertiesConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType *properties,
|
|
const AdapterType *patternProperties,
|
|
const AdapterType *additionalProperties,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &propertiesPath,
|
|
const std::string &patternPropertiesPath,
|
|
const std::string &additionalPropertiesPath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
const Subschema *parentSubschema,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
typedef typename AdapterType::ObjectMember Member;
|
|
|
|
constraints::PropertiesConstraint constraint;
|
|
|
|
// Create subschemas for 'properties' constraint
|
|
if (properties) {
|
|
for (const Member m : properties->getObject()) {
|
|
const std::string &property = m.first;
|
|
const std::string childPath = propertiesPath + "/" + property;
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, m.second, currentScope, childPath,
|
|
fetchDoc, parentSubschema, &property, docCache,
|
|
schemaCache);
|
|
constraint.addPropertySubschema(property, subschema);
|
|
}
|
|
}
|
|
|
|
// Create subschemas for 'patternProperties' constraint
|
|
if (patternProperties) {
|
|
for (const Member m : patternProperties->getObject()) {
|
|
const std::string &pattern = m.first;
|
|
const std::string childPath = patternPropertiesPath + "/" + pattern;
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, m.second, currentScope, childPath,
|
|
fetchDoc, parentSubschema, &pattern, docCache,
|
|
schemaCache);
|
|
constraint.addPatternPropertySubschema(pattern, subschema);
|
|
}
|
|
}
|
|
|
|
// Create an additionalItems subschema if required
|
|
if (additionalProperties) {
|
|
// If additionalProperties has been set, check for a boolean value.
|
|
// Setting 'additionalProperties' to true allows the values of
|
|
// additional properties to take any form. Setting it false
|
|
// prohibits the use of additional properties.
|
|
// If additionalProperties is instead an object, it should be
|
|
// parsed as a schema. If additionalProperties has any other type,
|
|
// then the schema is not valid.
|
|
if (additionalProperties->isBool() ||
|
|
additionalProperties->maybeBool()) {
|
|
// If it has a boolean value that is 'true', then an empty
|
|
// schema should be used.
|
|
if (additionalProperties->asBool()) {
|
|
constraint.setAdditionalPropertiesSubschema(rootSchema.emptySubschema());
|
|
}
|
|
} else if (additionalProperties->isObject()) {
|
|
// If additionalProperties is an object, it should be used as
|
|
// a child schema.
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(
|
|
rootSchema, rootNode, *additionalProperties,
|
|
currentScope, additionalPropertiesPath, fetchDoc, nullptr,
|
|
nullptr, docCache, schemaCache);
|
|
constraint.setAdditionalPropertiesSubschema(subschema);
|
|
} else {
|
|
// All other types are invalid
|
|
throwRuntimeError("Invalid type for 'additionalProperties' constraint.");
|
|
}
|
|
} else {
|
|
// If an additionalProperties constraint is not provided, then the
|
|
// default value is an empty schema.
|
|
constraint.setAdditionalPropertiesSubschema(rootSchema.emptySubschema());
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
template<typename AdapterType>
|
|
constraints::PropertyNamesConstraint makePropertyNamesConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType ¤tNode,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(rootSchema, rootNode, currentNode, currentScope,
|
|
nodePath, fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraints::PropertyNamesConstraint constraint;
|
|
constraint.setSubschema(subschema);
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new RequiredConstraint.
|
|
*
|
|
* This function is used to create new RequiredContraint objects for
|
|
* Draft 3 schemas.
|
|
*
|
|
* @param node Node containing a boolean value.
|
|
* @param name Name of the required attribute.
|
|
*
|
|
* @return pointer to a new RequiredConstraint object that belongs to the
|
|
* caller
|
|
*/
|
|
template<typename AdapterType>
|
|
opt::optional<constraints::RequiredConstraint>
|
|
makeRequiredConstraintForSelf(const AdapterType &node,
|
|
const std::string &name)
|
|
{
|
|
if (!node.maybeBool()) {
|
|
throwRuntimeError("Expected boolean value for 'required' attribute.");
|
|
}
|
|
|
|
if (node.asBool()) {
|
|
constraints::RequiredConstraint constraint;
|
|
constraint.addRequiredProperty(name);
|
|
return constraint;
|
|
}
|
|
|
|
return opt::optional<constraints::RequiredConstraint>();
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new RequiredConstraint.
|
|
*
|
|
* This function is used to create new RequiredContraint objects for
|
|
* Draft 4 schemas.
|
|
*
|
|
* @param node Node containing an array of strings.
|
|
*
|
|
* @return pointer to a new RequiredConstraint object that belongs to the
|
|
* caller
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::RequiredConstraint makeRequiredConstraint(
|
|
const AdapterType &node)
|
|
{
|
|
constraints::RequiredConstraint constraint;
|
|
|
|
for (const AdapterType v : node.getArray()) {
|
|
if (!v.maybeString()) {
|
|
throwRuntimeError("Expected required property name to be a string value");
|
|
}
|
|
|
|
constraint.addRequiredProperty(v.getString());
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new TypeConstraint object
|
|
*
|
|
* @param rootSchema The Schema instance, and root subschema, through
|
|
* which other subschemas can be created and modified
|
|
* @param rootNode Reference to the node from which JSON References
|
|
* will be resolved when they refer to the current
|
|
* document; used for recursive parsing of schemas
|
|
* @param node Node containing the name of a JSON type
|
|
* @param currentScope URI for current resolution scope
|
|
* @param nodePath JSON Pointer representing path to current node
|
|
* @param fetchDoc Function to fetch remote JSON documents (optional)
|
|
* @param docCache Cache of resolved and fetched remote documents
|
|
* @param schemaCache Cache of populated schemas
|
|
*
|
|
* @return pointer to a new TypeConstraint object.
|
|
*/
|
|
template<typename AdapterType>
|
|
constraints::TypeConstraint makeTypeConstraint(
|
|
Schema &rootSchema,
|
|
const AdapterType &rootNode,
|
|
const AdapterType &node,
|
|
const opt::optional<std::string> currentScope,
|
|
const std::string &nodePath,
|
|
const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
|
|
typename DocumentCache<AdapterType>::Type &docCache,
|
|
SchemaCache &schemaCache)
|
|
{
|
|
typedef constraints::TypeConstraint TypeConstraint;
|
|
|
|
TypeConstraint constraint;
|
|
|
|
if (node.maybeString()) {
|
|
const TypeConstraint::JsonType type = TypeConstraint::jsonTypeFromString(node.getString());
|
|
if (type == TypeConstraint::kAny && m_version == kDraft4) {
|
|
throwRuntimeError("'any' type is not supported in version 4 schemas.");
|
|
}
|
|
|
|
constraint.addNamedType(type);
|
|
|
|
} else if (node.maybeArray()) {
|
|
int index = 0;
|
|
for (const AdapterType v : node.getArray()) {
|
|
if (v.maybeString()) {
|
|
const TypeConstraint::JsonType type = TypeConstraint::jsonTypeFromString(v.getString());
|
|
if (type == TypeConstraint::kAny && m_version == kDraft4) {
|
|
throwRuntimeError("'any' type is not supported in version 4 schemas.");
|
|
}
|
|
|
|
constraint.addNamedType(type);
|
|
|
|
} else if (v.maybeObject() && m_version == kDraft3) {
|
|
const std::string childPath = nodePath + "/" + std::to_string(index);
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(rootSchema, rootNode, v, currentScope,
|
|
childPath, fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraint.addSchemaType(subschema);
|
|
|
|
} else {
|
|
throwRuntimeError("Type name should be a string.");
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
} else if (node.maybeObject() && m_version == kDraft3) {
|
|
const Subschema *subschema = makeOrReuseSchema<AdapterType>(rootSchema, rootNode, node, currentScope,
|
|
nodePath, fetchDoc, nullptr, nullptr, docCache, schemaCache);
|
|
constraint.addSchemaType(subschema);
|
|
|
|
} else {
|
|
throwRuntimeError("Type name should be a string.");
|
|
}
|
|
|
|
return constraint;
|
|
}
|
|
|
|
/**
|
|
* @brief Make a new UniqueItemsConstraint object.
|
|
*
|
|
* @param node Node containing a boolean value.
|
|
*
|
|
* @return pointer to a new UniqueItemsConstraint object that belongs to
|
|
* the caller, or nullptr if the boolean value is false.
|
|
*/
|
|
template<typename AdapterType>
|
|
opt::optional<constraints::UniqueItemsConstraint> makeUniqueItemsConstraint(const AdapterType &node)
|
|
{
|
|
if (node.isBool() || node.maybeBool()) {
|
|
// If the boolean value is true, this function will return a pointer
|
|
// to a new UniqueItemsConstraint object. If it is value, then the
|
|
// constraint is redundant, so nullptr is returned instead.
|
|
if (node.asBool()) {
|
|
return constraints::UniqueItemsConstraint();
|
|
} else {
|
|
return opt::optional<constraints::UniqueItemsConstraint>();
|
|
}
|
|
}
|
|
|
|
throwRuntimeError("Expected boolean value for 'uniqueItems' constraint.");
|
|
}
|
|
|
|
private:
|
|
|
|
/// Version of JSON Schema that should be expected when parsing
|
|
Version m_version;
|
|
};
|
|
|
|
} // namespace valijson
|