Coverage Report

Created: 2026-10-15 07:16

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/valijson/include/valijson/schema_parser.hpp
Line
Count
Source
1
#pragma once
2
3
#include <functional>
4
#include <iostream>
5
#include <optional>
6
#include <vector>
7
8
#include <valijson/constraints/concrete_constraints.hpp>
9
#include <valijson/internal/adapter.hpp>
10
#include <valijson/internal/debug.hpp>
11
#include <valijson/internal/json_pointer.hpp>
12
#include <valijson/internal/json_reference.hpp>
13
#include <valijson/internal/uri.hpp>
14
#include <valijson/constraint_builder.hpp>
15
#include <valijson/schema.hpp>
16
#include <valijson/schema_registry.hpp>
17
#include <valijson/exceptions.hpp>
18
19
namespace valijson {
20
21
/**
22
 * @brief  Parser for populating a Schema based on a JSON Schema document.
23
 *
24
 * The SchemaParser class supports Drafts 3 and 4 of JSON Schema, however
25
 * Draft 3 support should be considered deprecated.
26
 *
27
 * The functions provided by this class have been templated so that they can
28
 * be used with different Adapter types.
29
 */
30
class SchemaParser
31
{
32
public:
33
    /// Supported versions of JSON Schema
34
    enum Version {
35
        kDraft3,      ///< @deprecated JSON Schema v3 has been superseded by v4
36
        kDraft4,
37
        kDraft7
38
    };
39
40
    /**
41
     * @brief  Construct a new SchemaParser for a given version of JSON Schema
42
     *
43
     * @param  version  Version of JSON Schema that will be expected
44
     */
45
    explicit SchemaParser(const Version version = kDraft7)
46
56.9k
      : m_version(version) { }
47
48
    /**
49
     * @brief  Release memory associated with custom ConstraintBuilders
50
     */
51
    virtual ~SchemaParser()
52
56.9k
    {
53
56.9k
        for (const auto& entry : constraintBuilders) {
54
0
            delete entry.second;
55
0
        }
56
56.9k
    }
57
58
    /**
59
     * @brief  Struct to contain templated function type for fetching documents
60
     */
61
    template<typename AdapterType>
62
    struct FunctionPtrs
63
    {
64
        typedef typename adapters::AdapterTraits<AdapterType>::DocumentType DocumentType;
65
66
        /// Templated function pointer type for fetching remote documents
67
        typedef std::function<const DocumentType* (const std::string &uri)> FetchDoc;
68
69
        /// Templated function pointer type for freeing fetched documents
70
        typedef std::function<void (const DocumentType *)> FreeDoc;
71
    };
72
73
    /**
74
     * @brief  Add a custom constraint to this SchemaParser
75
76
     * @param  key      name that will be used to identify relevant constraints
77
     *                  while parsing a schema document
78
     * @param  builder  pointer to a subclass of ConstraintBuilder that can
79
     *                  parse custom constraints found in a schema document,
80
     *                  and return an appropriate instance of Constraint; this
81
     *                  class guarantees that it will take ownership of this
82
     *                  pointer - unless this function throws an exception
83
     *
84
     * @todo   consider accepting a list of custom ConstraintBuilders in
85
     *         constructor, so that this class remains immutable after
86
     *         construction
87
     *
88
     * @todo   Add additional checks for key conflicts, empty keys, and
89
     *         potential restrictions relating to case sensitivity
90
     */
91
    void addConstraintBuilder(const std::string &key, const ConstraintBuilder *builder)
92
0
    {
93
0
        constraintBuilders.push_back(std::make_pair(key, builder));
94
0
    }
95
96
    /**
97
     * @brief  Populate a Schema object from JSON Schema document
98
     *
99
     * When processing Draft 3 schemas, the parentSubschema and ownName pointers
100
     * should be set in contexts where a 'required' constraint would be valid.
101
     * These are used to add a RequiredConstraint object to the Schema that
102
     * contains the required property.
103
     *
104
     * @param  node          Reference to node to parse
105
     * @param  schema        Reference to Schema to populate
106
     * @param  fetchDoc      Function to fetch remote JSON documents (optional)
107
     */
108
    template<typename AdapterType>
109
    void populateSchema(
110
        const AdapterType &node,
111
        Schema &schema,
112
        typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc = nullptr,
113
        typename FunctionPtrs<AdapterType>::FreeDoc freeDoc = nullptr)
114
56.9k
    {
115
56.9k
        if ((fetchDoc == nullptr ) ^ (freeDoc == nullptr)) {
116
0
            throwRuntimeError("Remote document fetching can't be enabled without both fetch and free functions");
117
0
        }
118
119
56.9k
        typename DocumentCache<AdapterType>::Type docCache;
120
56.9k
        SchemaRegistry schemaRegistry;
121
56.9k
#if VALIJSON_USE_EXCEPTIONS
122
56.9k
        try {
123
56.9k
#endif
124
56.9k
            resolveThenPopulateSchema(schema, node, node, schema, std::optional<std::string>(), "", fetchDoc, nullptr,
125
56.9k
                    nullptr, docCache, schemaRegistry);
126
56.9k
#if VALIJSON_USE_EXCEPTIONS
127
56.9k
        } catch (...) {
128
37.0k
            freeDocumentCache<AdapterType>(docCache, freeDoc);
129
37.0k
            throw;
130
37.0k
        }
131
0
#endif
132
133
19.9k
        freeDocumentCache<AdapterType>(docCache, freeDoc);
134
19.9k
    }
135
136
private:
137
138
    typedef std::vector<std::pair<std::string, const ConstraintBuilder *>>
139
        ConstraintBuilders;
140
141
    ConstraintBuilders constraintBuilders;
142
143
    template<typename AdapterType>
144
    struct DocumentCache
145
    {
146
        typedef typename adapters::AdapterTraits<AdapterType>::DocumentType DocumentType;
147
148
        typedef std::map<std::string, const DocumentType*> Type;
149
    };
150
151
    /**
152
     * @brief  Free memory used by fetched documents
153
     *
154
     * If a custom 'free' function has not been provided, then the default
155
     * delete operator will be used.
156
     *
157
     * @param  docCache  collection of fetched documents to free
158
     * @param  freeDoc   optional custom free function
159
     */
160
    template<typename AdapterType>
161
    void freeDocumentCache(const typename DocumentCache<AdapterType>::Type
162
            &docCache, typename FunctionPtrs<AdapterType>::FreeDoc freeDoc)
163
56.9k
    {
164
56.9k
        typedef typename DocumentCache<AdapterType>::Type DocCacheType;
165
166
56.9k
        for (const typename DocCacheType::value_type &v : docCache) {
167
0
            freeDoc(v.second);
168
0
        }
169
56.9k
    }
170
171
    /**
172
     * @brief  Find the complete URI for a document, within a resolution scope
173
     *
174
     * This function captures five different cases that can occur when
175
     * attempting to resolve a document URI within a particular resolution
176
     * scope:
177
     *
178
     *  (1) resolution scope not present, but URN or absolute document URI is
179
     *       => document URI as-is
180
     *  (2) resolution scope not present, and document URI is relative or absent
181
     *       => document URI, if present, otherwise no result
182
     *  (3) resolution scope is present, and document URI is a relative path
183
     *       => resolve document URI relative to resolution scope
184
     *  (4) resolution scope is present, and document URI is absolute
185
     *       => document URI as-is
186
     *  (5) resolution scope is present, but document URI is not
187
     *       => resolution scope as-is
188
     *
189
     * This function assumes that the resolution scope is absolute.
190
     *
191
     * When resolving a document URI relative to the resolution scope, the
192
     * document URI should be used to replace the path, query and fragment
193
     * portions of URI provided by the resolution scope.
194
     */
195
    virtual std::optional<std::string> resolveDocumentUri(
196
            const std::optional<std::string>& resolutionScope,
197
            const std::optional<std::string>& documentUri)
198
240k
    {
199
240k
        if (resolutionScope) {
200
78.1k
            if (documentUri) {
201
9.56k
                if (internal::uri::isUriAbsolute(*documentUri) || internal::uri::isUrn(*documentUri)) {
202
                    // (4) resolution scope is present, and document URI is absolute
203
                    //      => document URI as-is
204
3.20k
                    return *documentUri;
205
6.36k
                } else {
206
                    // (3) resolution scope is present, and document URI is a relative path
207
                    //      => resolve document URI relative to resolution scope
208
6.36k
                    return internal::uri::resolveRelativeUri(*resolutionScope, *documentUri);
209
6.36k
                }
210
68.5k
            } else {
211
                // (5) resolution scope is present, but document URI is not
212
                //      => resolution scope as-is
213
68.5k
                return *resolutionScope;
214
68.5k
            }
215
162k
        } else if (documentUri && internal::uri::isUriAbsolute(*documentUri)) {
216
            // (1a) resolution scope not present, but absolute document URI is
217
            //      => document URI as-is
218
1.36k
            return *documentUri;
219
161k
        } else if (documentUri && internal::uri::isUrn(*documentUri)) {
220
            // (1b) resolution scope not present, but URN is
221
            //       => document URI as-is
222
1.09k
            return *documentUri;
223
159k
        } else {
224
            // (2) resolution scope not present, and document URI is relative or absent
225
            //      => document URI, if present, otherwise no result
226
            // documentUri is already std::optional
227
159k
            return documentUri;
228
159k
        }
229
240k
    }
230
231
    /**
232
     * @brief  Extract a JSON Reference string from a node
233
     *
234
     * @param  node    node to extract the JSON Reference from
235
     * @param  result  reference to string to set with the result
236
     *
237
     * @throws std::invalid_argument if node is an object containing a `$ref`
238
     *         property but with a value that cannot be interpreted as a string
239
     *
240
     * @return \c true if a JSON Reference was extracted; \c false otherwise
241
     */
242
    template<typename AdapterType>
243
    bool extractJsonReference(const AdapterType &node, std::string &result)
244
794k
    {
245
794k
        if (!node.isObject()) {
246
4.76k
            return false;
247
4.76k
        }
248
249
789k
        const typename AdapterType::Object o = node.getObject();
250
789k
        const typename AdapterType::Object::const_iterator itr = o.find("$ref");
251
789k
        if (itr == o.end()) {
252
548k
            return false;
253
548k
        } else if (!itr->second.getString(result)) {
254
245
            throwRuntimeError("$ref property expected to contain string value.");
255
245
        }
256
257
240k
        return true;
258
789k
    }
259
260
    /**
261
     * Sanitise an optional JSON Pointer, trimming trailing slashes
262
     */
263
    static std::string sanitiseJsonPointer(const std::optional<std::string>& input)
264
240k
    {
265
240k
        if (input) {
266
234k
            if (*input == "/") {
267
13.3k
                return "";
268
13.3k
            }
269
270
220k
            return *input;
271
234k
        }
272
273
        // If the JSON Pointer is not set, assume that the URI points to
274
        // the root of the document
275
6.27k
        return "";
276
240k
    }
277
278
    static std::string escapeJsonPointerToken(const std::string &token)
279
1.47M
    {
280
1.47M
        std::string result;
281
52.5M
        for (const char c : token) {
282
52.5M
            if (c == '~') {
283
59.8k
                result += "~0";
284
52.5M
            } else if (c == '/') {
285
908k
                result += "~1";
286
51.6M
            } else {
287
51.6M
                result += c;
288
51.6M
            }
289
52.5M
        }
290
291
1.47M
        return result;
292
1.47M
    }
293
294
    const char *idKeyword() const
295
1.82M
    {
296
1.82M
        return m_version == kDraft7 ? "$id" : "id";
297
1.82M
    }
298
299
    std::optional<std::string> resolveId(
300
            const std::optional<std::string> &currentScope,
301
            const std::string &id) const
302
113k
    {
303
113k
        if (!currentScope || internal::uri::isUriAbsolute(id) ||
304
84.0k
                internal::uri::isUrn(id)) {
305
29.8k
            return id;
306
29.8k
        }
307
308
83.8k
        return internal::uri::resolveRelativeUri(*currentScope, id);
309
113k
    }
310
311
    template<typename AdapterType>
312
    bool findSchemaById(
313
            const AdapterType &node,
314
            const std::optional<std::string> &currentScope,
315
            const std::string &targetUri,
316
            const std::string &nodePath,
317
            std::string &foundPath,
318
            std::optional<std::string> &foundScope) const
319
1.05M
    {
320
1.05M
        std::optional<std::string> updatedScope = currentScope;
321
322
1.05M
        if (node.isObject()) {
323
587k
            const typename AdapterType::Object object = node.asObject();
324
587k
            const typename AdapterType::Object::const_iterator itr =
325
587k
                    object.find(idKeyword());
326
587k
            if (itr != object.end() && itr->second.maybeString()) {
327
50.3k
                updatedScope = resolveId(currentScope, itr->second.asString());
328
50.3k
                if (updatedScope && *updatedScope == targetUri) {
329
2.95k
                    foundPath = nodePath;
330
2.95k
                    foundScope = currentScope;
331
2.95k
                    return true;
332
2.95k
                }
333
50.3k
            }
334
335
611k
            for (const typename AdapterType::ObjectMember member : object) {
336
611k
                const std::string childPath =
337
611k
                        nodePath + "/" + escapeJsonPointerToken(member.first);
338
611k
                if (findSchemaById(member.second, updatedScope, targetUri,
339
611k
                            childPath, foundPath, foundScope)) {
340
8.14k
                    return true;
341
8.14k
                }
342
611k
            }
343
584k
        } else if (node.maybeArray()) {
344
30.4k
            size_t index = 0;
345
419k
            for (const AdapterType item : node.asArray()) {
346
419k
                const std::string childPath = nodePath + "/" +
347
419k
                        std::to_string(index++);
348
419k
                if (findSchemaById(item, updatedScope, targetUri, childPath,
349
419k
                            foundPath, foundScope)) {
350
7.10k
                    return true;
351
7.10k
                }
352
419k
            }
353
30.4k
        }
354
355
1.03M
        return false;
356
1.05M
    }
357
358
    template<typename AdapterType>
359
    bool findScopeForPath(
360
            const AdapterType &node,
361
            const std::optional<std::string> &currentScope,
362
            const std::string &targetPath,
363
            const std::string &nodePath,
364
            std::optional<std::string> &foundScope) const
365
1.34M
    {
366
1.34M
        if (nodePath == targetPath) {
367
5.06k
            foundScope = currentScope;
368
5.06k
            return true;
369
5.06k
        }
370
371
1.33M
        std::optional<std::string> updatedScope = currentScope;
372
373
1.33M
        if (node.isObject()) {
374
744k
            const typename AdapterType::Object object = node.asObject();
375
744k
            const typename AdapterType::Object::const_iterator itr =
376
744k
                    object.find(idKeyword());
377
744k
            if (itr != object.end() && itr->second.maybeString()) {
378
28.7k
                updatedScope = resolveId(currentScope, itr->second.asString());
379
28.7k
            }
380
381
834k
            for (const typename AdapterType::ObjectMember member : object) {
382
834k
                const std::string childPath =
383
834k
                        nodePath + "/" + escapeJsonPointerToken(member.first);
384
834k
                if (findScopeForPath(member.second, updatedScope, targetPath,
385
834k
                            childPath, foundScope)) {
386
2.63k
                    return true;
387
2.63k
                }
388
834k
            }
389
744k
        } else {
390
595k
            if (node.maybeArray()) {
391
88.6k
                size_t index = 0;
392
493k
                for (const AdapterType item : node.asArray()) {
393
493k
                    const std::string childPath = nodePath + "/" +
394
493k
                            std::to_string(index++);
395
493k
                    if (findScopeForPath(item, updatedScope, targetPath,
396
493k
                                childPath, foundScope)) {
397
25
                        return true;
398
25
                    }
399
493k
                }
400
88.6k
            }
401
595k
        }
402
403
1.33M
        return false;
404
1.33M
    }
405
406
    /**
407
     * @brief  Search the schema registry for a schema matching a given key
408
     *
409
     * If the key is not present in the schema registry, a nullptr will be
410
     * returned, and the contents of the registry will remain unchanged. This is
411
     * in contrast to the behaviour of the std::map [] operator, which would
412
     * add the nullptr to the registry.
413
     *
414
     * @param  schemaRegistry  schema registry to query
415
     * @param  queryKey        key to search for
416
     *
417
     * @return shared pointer to Schema if found, nullptr otherwise
418
     */
419
    static const Subschema * querySchemaRegistry(SchemaRegistry &schemaRegistry,
420
            const std::string &queryKey)
421
736k
    {
422
736k
        const SchemaRegistry::iterator itr = schemaRegistry.find(queryKey);
423
736k
        if (itr == schemaRegistry.end()) {
424
472k
            return nullptr;
425
472k
        }
426
427
263k
        return itr->second;
428
736k
    }
429
430
    /**
431
     * @brief  Add entries to the schema registry for a given list of keys
432
     *
433
     * @param  schemaRegistry   schema registry to update
434
     * @param  keysToRegister   list of keys to register
435
     * @param  schema           shared pointer to schema that keys will map to
436
     *
437
     * @throws std::logic_error if any of the keys are already present in the
438
     *         schema registry. This behaviour is intended to help detect incorrect
439
     *         usage of the schema registry during development, and is not expected
440
     *         to occur otherwise, even for malformed schemas.
441
     */
442
    static void updateSchemaRegistry(SchemaRegistry &schemaRegistry,
443
            const std::vector<std::string> &keysToRegister,
444
            const Subschema *schema)
445
710k
    {
446
710k
        for (const std::string &keyToRegister : keysToRegister) {
447
15.2k
            const SchemaRegistry::value_type value(keyToRegister, schema);
448
15.2k
            if (!schemaRegistry.insert(value).second) {
449
0
                throwLogicError("Key '" + keyToRegister + "' already in schema registry.");
450
0
            }
451
15.2k
        }
452
710k
    }
453
454
    /**
455
     * @brief  Recursive helper function for retrieving or creating schemas
456
     *
457
     * This function will be applied recursively until a concrete node is found.
458
     * A concrete node is a node that contains actual schema constraints rather
459
     * than a JSON Reference.
460
     *
461
     * This termination condition may be trigged by visiting the concrete node
462
     * at the end of a series of $ref nodes, or by finding a schema for one of
463
     * those $ref nodes in the schema registry. An entry will be added to the
464
     * schema registry for each node visited on the path to the concrete node.
465
     *
466
     * @param  rootSchema      The Schema instance, and root subschema, through
467
     *                         which other subschemas can be created and
468
     *                         modified
469
     * @param  rootNode        Reference to the node from which JSON References
470
     *                         will be resolved when they refer to the current
471
     *                         document
472
     * @param  node            Reference to the node to parse
473
     * @param  currentScope    URI for current resolution scope
474
     * @param  nodePath        JSON Pointer representing path to current node
475
     * @param  fetchDoc        Function to fetch remote JSON documents (optional)
476
     * @param  parentSubschema Optional pointer to the parent schema, used to
477
     *                         support required keyword in Draft 3
478
     * @param  ownName         Optional pointer to a node name, used to support
479
     *                         the 'required' keyword in Draft 3
480
     * @param  docCache        Cache of resolved and fetched remote documents
481
     * @param  schemaRegistry  Registry of populated schemas
482
     * @param  newRegistryKeys A list of keys that should be added to the registry
483
     *                         when recursion terminates
484
     */
485
    template<typename AdapterType>
486
    const Subschema * makeOrReuseSchema(
487
        Schema &rootSchema,
488
        const AdapterType &rootNode,
489
        const AdapterType &node,
490
        const std::optional<std::string> currentScope,
491
        const std::string &nodePath,
492
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
493
        const Subschema *parentSubschema,
494
        const std::string *ownName,
495
        typename DocumentCache<AdapterType>::Type &docCache,
496
        SchemaRegistry &schemaRegistry,
497
        std::vector<std::string> &newRegistryKeys)
498
736k
    {
499
736k
        std::string jsonRef;
500
501
        // Check for the first termination condition (found a non-$ref node)
502
736k
        if (!extractJsonReference(node, jsonRef)) {
503
504
            // Construct a key that we can use to search the schema registry for
505
            // a schema corresponding to the current node
506
505k
            const std::string schemaRegistryKey = currentScope ? (*currentScope + nodePath) : nodePath;
507
508
            // Retrieve an existing schema from the registry if possible
509
505k
            const Subschema *registeredPtr = querySchemaRegistry(schemaRegistry, schemaRegistryKey);
510
511
            // Create a new schema otherwise
512
505k
            const Subschema *subschema = registeredPtr ? registeredPtr : rootSchema.createSubschema();
513
514
            // Add registry entries for keys belonging to any $ref nodes that were
515
            // visited before arriving at the current node
516
505k
            updateSchemaRegistry(schemaRegistry, newRegistryKeys, subschema);
517
505k
            if (!registeredPtr) {
518
446k
                schemaRegistry.emplace(schemaRegistryKey, subschema);
519
446k
            }
520
521
            // Schema registry did not contain a preexisting schema corresponding
522
            // to the current node, so the schema that was returned will need
523
            // to be populated
524
505k
            if (!registeredPtr) {
525
446k
                populateSchema(rootSchema, rootNode, node, *subschema,
526
446k
                        currentScope, nodePath, fetchDoc, parentSubschema,
527
446k
                        ownName, docCache, schemaRegistry);
528
446k
            }
529
530
505k
            return subschema;
531
505k
        }
532
533
        // Returns a document URI if the reference points somewhere
534
        // other than the current document
535
230k
        const std::optional<std::string> documentUri = internal::json_reference::getJsonReferenceUri(jsonRef);
536
537
        // Extract JSON Pointer from JSON Reference, with any trailing
538
        // slashes removed so that keys in the schema registry end
539
        // consistently
540
230k
        const std::string actualJsonPointer = sanitiseJsonPointer(
541
230k
                internal::json_reference::getJsonReferencePointer(jsonRef));
542
543
        // Determine the actual document URI based on the resolution
544
        // scope. An absolute document URI will take precedence when
545
        // present, otherwise we need to resolve the URI relative to
546
        // the current resolution scope
547
230k
        const std::optional<std::string> actualDocumentUri = resolveDocumentUri(currentScope, documentUri);
548
549
        // Construct a key to search the schema registry for an existing schema
550
230k
        const std::string queryKey = actualDocumentUri ? (*actualDocumentUri + actualJsonPointer) : actualJsonPointer;
551
552
230k
        if (!actualJsonPointer.empty() && actualJsonPointer[0] != '/') {
553
12.8k
            const std::string idRef = actualDocumentUri ?
554
12.5k
                    (*actualDocumentUri + "#" + actualJsonPointer) :
555
12.8k
                    ("#" + actualJsonPointer);
556
12.8k
            std::string registeredPath;
557
12.8k
            std::optional<std::string> registeredScope;
558
12.8k
            if (findSchemaById(rootNode, currentScope, idRef,
559
12.8k
                        "", registeredPath, registeredScope)) {
560
2.53k
                const AdapterType registeredRoot =
561
2.53k
                        internal::json_pointer::resolveJsonPointerStrict(
562
2.53k
                                rootNode, registeredPath);
563
564
2.53k
                const Subschema *registeredPtr = querySchemaRegistry(schemaRegistry, idRef);
565
2.53k
                if (registeredPtr) {
566
2.43k
                    updateSchemaRegistry(schemaRegistry, newRegistryKeys, registeredPtr);
567
2.43k
                    return registeredPtr;
568
2.43k
                }
569
570
106
                const Subschema *subschema = rootSchema.createSubschema();
571
106
                newRegistryKeys.push_back(idRef);
572
106
                updateSchemaRegistry(schemaRegistry, newRegistryKeys, subschema);
573
574
106
                populateSchema(rootSchema, registeredRoot, registeredRoot,
575
106
                        *subschema, registeredScope, "", fetchDoc,
576
106
                        parentSubschema, ownName, docCache, schemaRegistry);
577
578
106
                return subschema;
579
2.53k
            }
580
12.8k
        }
581
582
        // Check for the second termination condition (found a $ref node that
583
        // already has an entry in the schema registry)
584
227k
        const Subschema *registeredPtr = querySchemaRegistry(schemaRegistry, queryKey);
585
227k
        if (registeredPtr) {
586
202k
            updateSchemaRegistry(schemaRegistry, newRegistryKeys, registeredPtr);
587
202k
            return registeredPtr;
588
202k
        }
589
590
25.4k
        if (actualDocumentUri && (!currentScope || *actualDocumentUri != *currentScope)) {
591
6.25k
            std::string registeredPath;
592
6.25k
            std::optional<std::string> registeredScope;
593
6.25k
            if (findSchemaById(rootNode, currentScope, *actualDocumentUri,
594
6.25k
                        "", registeredPath, registeredScope)) {
595
282
                const AdapterType registeredRoot =
596
282
                        internal::json_pointer::resolveJsonPointerStrict(
597
282
                                rootNode, registeredPath);
598
282
                const AdapterType referencedAdapter = actualJsonPointer.empty()
599
282
                        ? registeredRoot
600
282
                        : internal::json_pointer::resolveJsonPointerStrict(
601
109
                                registeredRoot, actualJsonPointer);
602
603
282
                std::optional<std::string> referencedScope = registeredScope;
604
282
                if (!actualJsonPointer.empty()) {
605
3
                    findScopeForPath(registeredRoot, registeredScope,
606
3
                            actualJsonPointer, "", referencedScope);
607
3
                }
608
609
282
                const Subschema *subschema = rootSchema.createSubschema();
610
282
                newRegistryKeys.push_back(queryKey);
611
282
                updateSchemaRegistry(schemaRegistry, newRegistryKeys, subschema);
612
613
282
                populateSchema(rootSchema, registeredRoot, referencedAdapter,
614
282
                        *subschema, referencedScope, actualJsonPointer,
615
282
                        fetchDoc, parentSubschema, ownName, docCache,
616
282
                        schemaRegistry);
617
618
282
                return subschema;
619
282
            }
620
621
5.97k
            const typename FunctionPtrs<AdapterType>::DocumentType *newDoc = nullptr;
622
623
            // Have we seen this document before?
624
5.97k
            typename DocumentCache<AdapterType>::Type::iterator docCacheItr =
625
5.97k
                    docCache.find(*actualDocumentUri);
626
5.97k
            if (docCacheItr == docCache.end()) {
627
                // Resolve reference against remote document
628
5.97k
                if (!fetchDoc) {
629
5.97k
                    throwRuntimeError("Fetching of remote JSON References not enabled.");
630
5.97k
                }
631
632
                // Returns a pointer to the remote document that was
633
                // retrieved, or null if retrieval failed. This class
634
                // will take ownership of the pointer, and call freeDoc
635
                // when it is no longer needed.
636
5.97k
                newDoc = fetchDoc(*actualDocumentUri);
637
638
                // Can't proceed without the remote document
639
5.97k
                if (!newDoc) {
640
0
                    throwRuntimeError("Failed to fetch referenced schema document: " + *actualDocumentUri);
641
0
                }
642
643
5.97k
                typedef typename DocumentCache<AdapterType>::Type::value_type
644
5.97k
                        DocCacheValueType;
645
646
5.97k
                docCache.insert(DocCacheValueType(*actualDocumentUri, newDoc));
647
648
5.97k
            } else {
649
0
                newDoc = docCacheItr->second;
650
0
            }
651
652
5.97k
            const AdapterType newRootNode(*newDoc);
653
654
            // Find where we need to be in the document
655
5.97k
            const AdapterType &referencedAdapter =
656
5.97k
                    internal::json_pointer::resolveJsonPointerStrict(newRootNode,
657
5.97k
                            actualJsonPointer);
658
659
5.97k
            newRegistryKeys.push_back(queryKey);
660
661
            // Populate the schema, starting from the referenced node, with
662
            // nested JSON References resolved relative to the new root node
663
5.97k
            return makeOrReuseSchema(rootSchema, newRootNode, referencedAdapter,
664
5.97k
                    actualDocumentUri, actualJsonPointer, fetchDoc, parentSubschema,
665
5.97k
                    ownName, docCache, schemaRegistry, newRegistryKeys);
666
667
6.25k
        }
668
669
19.2k
        if (std::find(newRegistryKeys.begin(), newRegistryKeys.end(), queryKey) != newRegistryKeys.end()) {
670
81
            throwRuntimeError("found cycle while resolving JSON reference");
671
81
        }
672
673
        // JSON References in nested schema will be resolved relative to the
674
        // current document
675
19.2k
        const AdapterType &referencedAdapter =
676
19.2k
                internal::json_pointer::resolveJsonPointerStrict(
677
19.2k
                        rootNode, actualJsonPointer);
678
679
19.2k
        newRegistryKeys.push_back(queryKey);
680
681
19.2k
        std::optional<std::string> referencedScope = currentScope;
682
19.2k
        findScopeForPath(rootNode, currentScope, actualJsonPointer, "",
683
19.2k
                referencedScope);
684
685
        // Populate the schema, starting from the referenced node, with
686
        // nested JSON References resolved relative to the new root node
687
19.2k
        return makeOrReuseSchema(rootSchema, rootNode, referencedAdapter,
688
19.2k
                referencedScope, actualJsonPointer, fetchDoc, parentSubschema,
689
19.2k
                ownName, docCache, schemaRegistry, newRegistryKeys);
690
25.4k
    }
691
692
    /**
693
     * @brief  Return pointer for the schema corresponding to a given node
694
     *
695
     * This function makes use of a schema registry, so that if the path to the
696
     * current node is the same as one that has already been parsed and
697
     * populated, a pointer to the existing Subschema will be returned.
698
     *
699
     * Should a series of $ref, or reference, nodes be resolved before reaching
700
     * a concrete node, an entry will be added to the schema registry for each of
701
     * the nodes in that path.
702
     *
703
     * @param  rootSchema      The Schema instance, and root subschema, through
704
     *                         which other subschemas can be created and
705
     *                         modified
706
     * @param  rootNode        Reference to the node from which JSON References
707
     *                         will be resolved when they refer to the current
708
     *                         document
709
     * @param  node            Reference to the node to parse
710
     * @param  currentScope    URI for current resolution scope
711
     * @param  nodePath        JSON Pointer representing path to current node
712
     * @param  fetchDoc        Function to fetch remote JSON documents (optional)
713
     * @param  parentSubschema Optional pointer to the parent schema, used to
714
     *                         support required keyword in Draft 3
715
     * @param  ownName         Optional pointer to a node name, used to support
716
     *                         the 'required' keyword in Draft 3
717
     * @param  docCache        Cache of resolved and fetched remote documents
718
     * @param  schemaRegistry  Registry of populated schemas
719
     */
720
    template<typename AdapterType>
721
    const Subschema * makeOrReuseSchema(
722
        Schema &rootSchema,
723
        const AdapterType &rootNode,
724
        const AdapterType &node,
725
        const std::optional<std::string> currentScope,
726
        const std::string &nodePath,
727
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
728
        const Subschema *parentSubschema,
729
        const std::string *ownName,
730
        typename DocumentCache<AdapterType>::Type &docCache,
731
        SchemaRegistry &schemaRegistry)
732
721k
    {
733
721k
        std::vector<std::string> schemaRegistryKeysToCreate;
734
735
721k
        return makeOrReuseSchema(rootSchema, rootNode, node, currentScope,
736
721k
                nodePath, fetchDoc, parentSubschema, ownName, docCache,
737
721k
                schemaRegistry, schemaRegistryKeysToCreate);
738
721k
    }
739
740
    /**
741
     * @brief  Populate a Schema object from JSON Schema document
742
     *
743
     * When processing Draft 3 schemas, the parentSubschema and ownName pointers
744
     * should be set in contexts where a 'required' constraint would be valid.
745
     * These are used to add a RequiredConstraint object to the Schema that
746
     * contains the required property.
747
     *
748
     * @param  rootSchema       The Schema instance, and root subschema, through
749
     *                          which other subschemas can be created and
750
     *                          modified
751
     * @param  rootNode         Reference to the node from which JSON References
752
     *                          will be resolved when they refer to the current
753
     *                          document
754
     * @param  node             Reference to node to parse
755
     * @param  subschema        Reference to Schema to populate
756
     * @param  currentScope     URI for current resolution scope
757
     * @param  nodePath         JSON Pointer representing path to current node
758
     * @param  fetchDoc         Optional function to fetch remote JSON documents
759
     * @param  parentSubschema  Optional pointer to the parent schema, used to
760
     *                          support required keyword in Draft 3
761
     * @param  ownName          Optional pointer to a node name, used to support
762
     *                          the 'required' keyword in Draft 3
763
     * @param  docCache         Cache of resolved and fetched remote documents
764
     * @param  schemaRegistry   Registry of populated schemas
765
     */
766
    template<typename AdapterType>
767
    void populateSchema(
768
        Schema &rootSchema,
769
        const AdapterType &rootNode,
770
        const AdapterType &node,
771
        const Subschema &subschema,
772
        const std::optional<std::string>& currentScope,
773
        const std::string &nodePath,
774
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
775
        const Subschema *parentSubschema,
776
        const std::string *ownName,
777
        typename DocumentCache<AdapterType>::Type &docCache,
778
        SchemaRegistry &schemaRegistry)
779
494k
    {
780
494k
        static_assert((std::is_convertible<AdapterType,
781
494k
            const valijson::adapters::Adapter &>::value),
782
494k
            "SchemaParser::populateSchema must be invoked with an "
783
494k
            "appropriate Adapter implementation");
784
785
494k
        if (!node.isObject()) {
786
4.48k
            if (m_version == kDraft7 && node.maybeBool()) {
787
                // Boolean schema
788
2.40k
                if (!node.asBool()) {
789
985
                    rootSchema.setAlwaysInvalid(&subschema, true);
790
985
                }
791
2.40k
                return;
792
2.40k
            } else {
793
2.07k
                std::string s;
794
2.07k
                s += "Expected node at ";
795
2.07k
                s += nodePath;
796
2.07k
                if (m_version == kDraft7) {
797
1.29k
                    s += " to contain schema object or boolean value; actual node type is: ";
798
1.29k
                } else {
799
782
                    s += " to contain schema object; actual node type is: ";
800
782
                }
801
2.07k
                s += internal::nodeTypeAsString(node);
802
2.07k
                throwRuntimeError(s);
803
2.07k
            }
804
4.48k
        }
805
806
492k
        const typename AdapterType::Object object = node.asObject();
807
492k
        typename AdapterType::Object::const_iterator itr(object.end());
808
809
        // Check for schema identifier attribute and update current scope.
810
492k
        std::optional<std::string> updatedScope;
811
492k
        bool foundId = false;
812
492k
        if ((itr = object.find(idKeyword())) != object.end() && itr->second.maybeString()) {
813
34.5k
            foundId = true;
814
34.5k
            const std::string id = itr->second.asString();
815
34.5k
            rootSchema.setSubschemaId(&subschema, itr->second.asString());
816
34.5k
            updatedScope = resolveId(currentScope, id);
817
457k
        } else {
818
457k
            updatedScope = currentScope;
819
457k
        }
820
821
492k
        if (foundId && updatedScope) {
822
34.5k
            schemaRegistry.emplace(*updatedScope, &subschema);
823
34.5k
        }
824
825
        // Add the type constraint first to be the first one to check because other constraints may rely on it
826
492k
        if ((itr = object.find("type")) != object.end()) {
827
27.3k
            rootSchema.addConstraintToSubschema(
828
27.3k
                    makeTypeConstraint(rootSchema, rootNode, itr->second, updatedScope, nodePath + "/type", fetchDoc,
829
27.3k
                            docCache, schemaRegistry),
830
27.3k
                    &subschema);
831
27.3k
        }
832
833
492k
        if ((itr = object.find("allOf")) != object.end()) {
834
19.2k
            rootSchema.addConstraintToSubschema(
835
19.2k
                    makeAllOfConstraint(rootSchema, rootNode, itr->second,
836
19.2k
                            updatedScope, nodePath + "/allOf", fetchDoc,
837
19.2k
                            docCache, schemaRegistry),
838
19.2k
                    &subschema);
839
19.2k
        }
840
841
492k
        if ((itr = object.find("anyOf")) != object.end()) {
842
21.1k
            rootSchema.addConstraintToSubschema(
843
21.1k
                    makeAnyOfConstraint(rootSchema, rootNode, itr->second,
844
21.1k
                            updatedScope, nodePath + "/anyOf", fetchDoc,
845
21.1k
                            docCache, schemaRegistry),
846
21.1k
                    &subschema);
847
21.1k
        }
848
849
492k
        if ((itr = object.find("const")) != object.end()) {
850
10.9k
            rootSchema.addConstraintToSubschema(makeConstConstraint(itr->second), &subschema);
851
10.9k
        }
852
853
492k
        if ((itr = object.find("contains")) != object.end()) {
854
18.8k
            rootSchema.addConstraintToSubschema(
855
18.8k
                    makeContainsConstraint(rootSchema, rootNode, itr->second,
856
18.8k
                            updatedScope, nodePath + "/contains", fetchDoc,
857
18.8k
                            docCache, schemaRegistry), &subschema);
858
18.8k
        }
859
860
492k
        if ((itr = object.find("dependencies")) != object.end()) {
861
17.0k
            rootSchema.addConstraintToSubschema(
862
17.0k
                    makeDependenciesConstraint(rootSchema, rootNode,
863
17.0k
                            itr->second, updatedScope,
864
17.0k
                            nodePath + "/dependencies", fetchDoc, docCache,
865
17.0k
                            schemaRegistry),
866
17.0k
                    &subschema);
867
17.0k
        }
868
869
492k
        if ((itr = object.find("description")) != object.end()) {
870
2.21k
            if (itr->second.maybeString()) {
871
2.17k
                rootSchema.setSubschemaDescription(&subschema,
872
2.17k
                        itr->second.asString());
873
2.17k
            } else {
874
41
                throwRuntimeError(
875
41
                        "'description' attribute should have a string value");
876
41
            }
877
2.21k
        }
878
879
492k
        if ((itr = object.find("divisibleBy")) != object.end()) {
880
74
            if (m_version == kDraft3) {
881
0
                if (itr->second.maybeInteger()) {
882
0
                    rootSchema.addConstraintToSubschema(
883
0
                            makeMultipleOfIntConstraint(itr->second),
884
0
                            &subschema);
885
0
                } else if (itr->second.maybeDouble()) {
886
0
                    rootSchema.addConstraintToSubschema(
887
0
                            makeMultipleOfDoubleConstraint(itr->second),
888
0
                            &subschema);
889
0
                } else {
890
0
                    throwRuntimeError("Expected a numeric value for "
891
0
                            " 'divisibleBy' constraint.");
892
0
                }
893
74
            } else {
894
74
                throwRuntimeError(
895
74
                        "'divisibleBy' constraint not valid after draft 3");
896
74
            }
897
74
        }
898
899
492k
        if ((itr = object.find("enum")) != object.end()) {
900
8.82k
            rootSchema.addConstraintToSubschema(makeEnumConstraint(itr->second), &subschema);
901
8.82k
        }
902
903
        // Draft 3's 'extends' keyword is equivalent to 'allOf' in later drafts
904
        // and accepts either a single schema or an array of schemas.
905
492k
        if (m_version == kDraft3 && (itr = object.find("extends")) != object.end()) {
906
0
            rootSchema.addConstraintToSubschema(
907
0
                    makeExtendsConstraint(rootSchema, rootNode, itr->second,
908
0
                            updatedScope, nodePath + "/extends", fetchDoc,
909
0
                            docCache, schemaRegistry),
910
0
                    &subschema);
911
0
        }
912
913
492k
        if ((itr = object.find("format")) != object.end()) {
914
6.04k
            rootSchema.addConstraintToSubschema(makeFormatConstraint(itr->second), &subschema);
915
6.04k
        }
916
917
492k
        {
918
492k
            const typename AdapterType::Object::const_iterator itemsItr =
919
492k
                    object.find("items");
920
921
492k
            if (object.end() != itemsItr) {
922
43.6k
                if (!itemsItr->second.isArray()) {
923
15.9k
                    rootSchema.addConstraintToSubschema(
924
15.9k
                            makeSingularItemsConstraint(rootSchema, rootNode,
925
15.9k
                                    itemsItr->second, updatedScope,
926
15.9k
                                    nodePath + "/items", fetchDoc, docCache,
927
15.9k
                                    schemaRegistry),
928
15.9k
                            &subschema);
929
930
27.6k
                } else {
931
27.6k
                    const typename AdapterType::Object::const_iterator
932
27.6k
                            additionalItemsItr = object.find("additionalItems");
933
27.6k
                    rootSchema.addConstraintToSubschema(
934
27.6k
                            makeLinearItemsConstraint(rootSchema, rootNode,
935
27.6k
                                    itemsItr != object.end() ? &itemsItr->second : nullptr,
936
27.6k
                                    additionalItemsItr != object.end() ? &additionalItemsItr->second : nullptr,
937
27.6k
                                    updatedScope, nodePath + "/items",
938
27.6k
                                    nodePath + "/additionalItems", fetchDoc,
939
27.6k
                                    docCache, schemaRegistry),
940
27.6k
                            &subschema);
941
27.6k
                }
942
43.6k
            }
943
492k
        }
944
945
492k
        {
946
492k
            const typename AdapterType::Object::const_iterator ifItr = object.find("if");
947
492k
            const typename AdapterType::Object::const_iterator thenItr = object.find("then");
948
492k
            const typename AdapterType::Object::const_iterator elseItr = object.find("else");
949
950
492k
            if (object.end() != ifItr) {
951
4.85k
                if (m_version == kDraft7) {
952
4.30k
                    rootSchema.addConstraintToSubschema(
953
4.30k
                          makeConditionalConstraint(rootSchema, rootNode,
954
4.30k
                                ifItr->second,
955
4.30k
                                thenItr == object.end() ? nullptr : &thenItr->second,
956
4.30k
                                elseItr == object.end() ? nullptr : &elseItr->second,
957
4.30k
                                updatedScope, nodePath, fetchDoc, docCache, schemaRegistry),
958
4.30k
                          &subschema);
959
4.30k
                } else {
960
553
                    throwRuntimeError("Not supported");
961
553
                }
962
4.85k
            }
963
492k
        }
964
965
492k
        if (m_version == kDraft7) {
966
216k
            if ((itr = object.find("exclusiveMaximum")) != object.end()) {
967
776
                rootSchema.addConstraintToSubschema(
968
776
                    makeMaximumConstraintExclusive(itr->second),
969
776
                    &subschema);
970
776
            }
971
972
216k
            if ((itr = object.find("maximum")) != object.end()) {
973
1.32k
                rootSchema.addConstraintToSubschema(
974
1.32k
                    makeMaximumConstraint<AdapterType>(itr->second, nullptr),
975
1.32k
                    &subschema);
976
1.32k
            }
977
275k
        } else if ((itr = object.find("maximum")) != object.end()) {
978
1.52k
            typename AdapterType::Object::const_iterator exclusiveMaximumItr =
979
1.52k
                    object.find("exclusiveMaximum");
980
1.52k
            if (exclusiveMaximumItr == object.end()) {
981
1.03k
                rootSchema.addConstraintToSubschema(
982
1.03k
                        makeMaximumConstraint<AdapterType>(itr->second, nullptr),
983
1.03k
                        &subschema);
984
1.03k
            } else {
985
486
                rootSchema.addConstraintToSubschema(
986
486
                        makeMaximumConstraint(itr->second, &exclusiveMaximumItr->second),
987
486
                        &subschema);
988
486
            }
989
274k
        } else if (object.find("exclusiveMaximum") != object.end()) {
990
43
            throwRuntimeError("'exclusiveMaximum' constraint only valid if a 'maximum' "
991
43
                    "constraint is also present");
992
43
        }
993
994
492k
        if ((itr = object.find("maxItems")) != object.end()) {
995
3.00k
            rootSchema.addConstraintToSubschema(
996
3.00k
                    makeMaxItemsConstraint(itr->second), &subschema);
997
3.00k
        }
998
999
492k
        if ((itr = object.find("maxLength")) != object.end()) {
1000
3.66k
            rootSchema.addConstraintToSubschema(
1001
3.66k
                    makeMaxLengthConstraint(itr->second), &subschema);
1002
3.66k
        }
1003
1004
492k
        if ((itr = object.find("maxProperties")) != object.end()) {
1005
2.32k
            rootSchema.addConstraintToSubschema(
1006
2.32k
                    makeMaxPropertiesConstraint(itr->second), &subschema);
1007
2.32k
        }
1008
1009
492k
        if (m_version == kDraft7) {
1010
215k
            if ((itr = object.find("exclusiveMinimum")) != object.end()) {
1011
1.02k
                rootSchema.addConstraintToSubschema(
1012
1.02k
                        makeMinimumConstraintExclusive(itr->second), &subschema);
1013
1.02k
            }
1014
1015
215k
            if ((itr = object.find("minimum")) != object.end()) {
1016
3.86k
                rootSchema.addConstraintToSubschema(
1017
3.86k
                        makeMinimumConstraint<AdapterType>(itr->second, nullptr),
1018
3.86k
                        &subschema);
1019
3.86k
            }
1020
276k
        } else if ((itr = object.find("minimum")) != object.end()) {
1021
2.66k
            typename AdapterType::Object::const_iterator exclusiveMinimumItr = object.find("exclusiveMinimum");
1022
2.66k
            if (exclusiveMinimumItr == object.end()) {
1023
2.64k
                rootSchema.addConstraintToSubschema(
1024
2.64k
                        makeMinimumConstraint<AdapterType>(itr->second, nullptr),
1025
2.64k
                        &subschema);
1026
2.64k
            } else {
1027
12
                rootSchema.addConstraintToSubschema(
1028
12
                        makeMinimumConstraint<AdapterType>(itr->second, &exclusiveMinimumItr->second),
1029
12
                        &subschema);
1030
12
            }
1031
273k
        } else if (object.find("exclusiveMinimum") != object.end()) {
1032
197
            throwRuntimeError("'exclusiveMinimum' constraint only valid if a 'minimum' "
1033
197
                    "constraint is also present");
1034
197
        }
1035
1036
492k
        if ((itr = object.find("minItems")) != object.end()) {
1037
3.46k
            rootSchema.addConstraintToSubschema(
1038
3.46k
                    makeMinItemsConstraint(itr->second), &subschema);
1039
3.46k
        }
1040
1041
492k
        if ((itr = object.find("minLength")) != object.end()) {
1042
4.80k
            rootSchema.addConstraintToSubschema(
1043
4.80k
                    makeMinLengthConstraint(itr->second), &subschema);
1044
4.80k
        }
1045
1046
492k
        if ((itr = object.find("minProperties")) != object.end()) {
1047
2.75k
            rootSchema.addConstraintToSubschema(
1048
2.75k
                    makeMinPropertiesConstraint(itr->second), &subschema);
1049
2.75k
        }
1050
1051
492k
        if ((itr = object.find("multipleOf")) != object.end()) {
1052
8.17k
            if (m_version == kDraft3) {
1053
0
                throwRuntimeError("'multipleOf' constraint not available in draft 3");
1054
8.17k
            } else if (itr->second.maybeInteger()) {
1055
6.16k
                rootSchema.addConstraintToSubschema(
1056
6.16k
                        makeMultipleOfIntConstraint(itr->second),
1057
6.16k
                        &subschema);
1058
6.16k
            } else if (itr->second.maybeDouble()) {
1059
1.92k
                rootSchema.addConstraintToSubschema(
1060
1.92k
                        makeMultipleOfDoubleConstraint(itr->second),
1061
1.92k
                        &subschema);
1062
1.92k
            } else {
1063
89
                throwRuntimeError("Expected a numeric value for 'multipleOf' constraint.");
1064
89
            }
1065
8.17k
        }
1066
1067
492k
        if ((itr = object.find("not")) != object.end()) {
1068
13.3k
            rootSchema.addConstraintToSubschema(
1069
13.3k
                    makeNotConstraint(rootSchema, rootNode, itr->second, updatedScope, nodePath + "/not", fetchDoc,
1070
13.3k
                            docCache, schemaRegistry),
1071
13.3k
                    &subschema);
1072
13.3k
        }
1073
1074
492k
        if ((itr = object.find("oneOf")) != object.end()) {
1075
14.3k
            rootSchema.addConstraintToSubschema(
1076
14.3k
                    makeOneOfConstraint(rootSchema, rootNode, itr->second, updatedScope, nodePath + "/oneOf", fetchDoc,
1077
14.3k
                            docCache, schemaRegistry),
1078
14.3k
                    &subschema);
1079
14.3k
        }
1080
1081
492k
        if ((itr = object.find("pattern")) != object.end()) {
1082
18.6k
            rootSchema.addConstraintToSubschema(
1083
18.6k
                    makePatternConstraint(itr->second), &subschema);
1084
18.6k
        }
1085
1086
492k
        {
1087
            // Check for schema keywords that require the creation of a
1088
            // PropertiesConstraint instance.
1089
492k
            const typename AdapterType::Object::const_iterator
1090
492k
                propertiesItr = object.find("properties"),
1091
492k
                patternPropertiesItr = object.find("patternProperties"),
1092
492k
                additionalPropertiesItr = object.find("additionalProperties");
1093
492k
            if (object.end() != propertiesItr ||
1094
433k
                object.end() != patternPropertiesItr ||
1095
414k
                object.end() != additionalPropertiesItr) {
1096
33.7k
                rootSchema.addConstraintToSubschema(
1097
33.7k
                        makePropertiesConstraint(rootSchema, rootNode,
1098
33.7k
                                propertiesItr != object.end() ? &propertiesItr->second : nullptr,
1099
33.7k
                                patternPropertiesItr != object.end() ? &patternPropertiesItr->second : nullptr,
1100
33.7k
                                additionalPropertiesItr != object.end() ? &additionalPropertiesItr->second : nullptr,
1101
33.7k
                                updatedScope, nodePath + "/properties",
1102
33.7k
                                nodePath + "/patternProperties",
1103
33.7k
                                nodePath + "/additionalProperties",
1104
33.7k
                                fetchDoc, &subschema, docCache, schemaRegistry),
1105
33.7k
                        &subschema);
1106
33.7k
            }
1107
492k
        }
1108
1109
492k
        if ((itr = object.find("propertyNames")) != object.end()) {
1110
4.42k
            if (m_version == kDraft7) {
1111
3.70k
                rootSchema.addConstraintToSubschema(
1112
3.70k
                      makePropertyNamesConstraint(rootSchema, rootNode, itr->second, updatedScope,
1113
3.70k
                              nodePath, fetchDoc, docCache, schemaRegistry),
1114
3.70k
                      &subschema);
1115
3.70k
            } else {
1116
727
                throwRuntimeError("Not supported");
1117
727
            }
1118
4.42k
        }
1119
1120
492k
        if ((itr = object.find("required")) != object.end()) {
1121
9.24k
            if (m_version == kDraft3) {
1122
0
                if (parentSubschema && ownName) {
1123
0
                    std::optional<constraints::RequiredConstraint> constraint =
1124
0
                            makeRequiredConstraintForSelf(itr->second, *ownName);
1125
0
                    if (constraint) {
1126
0
                        rootSchema.addConstraintToSubschema(*constraint, parentSubschema);
1127
0
                    }
1128
0
                } else {
1129
0
                    throwRuntimeError("'required' constraint not valid here");
1130
0
                }
1131
9.24k
            } else {
1132
9.24k
                rootSchema.addConstraintToSubschema(makeRequiredConstraint(itr->second), &subschema);
1133
9.24k
            }
1134
9.24k
        }
1135
1136
492k
        if ((itr = object.find("title")) != object.end()) {
1137
4.25k
            if (itr->second.maybeString()) {
1138
4.17k
                rootSchema.setSubschemaTitle(&subschema, itr->second.asString());
1139
4.17k
            } else {
1140
75
                throwRuntimeError("'title' attribute should have a string value");
1141
75
            }
1142
4.25k
        }
1143
1144
492k
        if ((itr = object.find("uniqueItems")) != object.end()) {
1145
1.69k
            std::optional<constraints::UniqueItemsConstraint> constraint = makeUniqueItemsConstraint(itr->second);
1146
1.69k
            if (constraint) {
1147
1.54k
                rootSchema.addConstraintToSubschema(*constraint, &subschema);
1148
1.54k
            }
1149
1.69k
        }
1150
1151
492k
        for (const auto & constraintBuilder : constraintBuilders) {
1152
0
            if ((itr = object.find(constraintBuilder.first)) != object.end()) {
1153
0
                constraints::Constraint *constraint = nullptr;
1154
0
#if VALIJSON_USE_EXCEPTIONS
1155
0
                try {
1156
0
#endif
1157
0
                    constraint = constraintBuilder.second->make(itr->second);
1158
0
                    rootSchema.addConstraintToSubschema(*constraint, &subschema);
1159
0
                    delete constraint;
1160
0
#if VALIJSON_USE_EXCEPTIONS
1161
0
                } catch (...) {
1162
0
                    delete constraint;
1163
0
                    throw;
1164
0
                }
1165
0
#endif
1166
0
            }
1167
0
        }
1168
492k
    }
1169
1170
    /**
1171
     * @brief  Resolves a chain of JSON References before populating a schema
1172
     *
1173
     * This helper function is used directly by the publicly visible
1174
     * populateSchema function. It ensures that the node being parsed is a
1175
     * concrete node, and not a JSON Reference. This function will call itself
1176
     * recursively to resolve references until a concrete node is found.
1177
     *
1178
     * @param  rootSchema     The Schema instance, and root subschema, through
1179
     *                        which other subschemas can be created and modified
1180
     * @param  rootNode       Reference to the node from which JSON References
1181
     *                        will be resolved when they refer to the current
1182
     *                        document
1183
     * @param  node           Reference to node to parse
1184
     * @param  subschema      Reference to Schema to populate
1185
     * @param  currentScope   URI for current resolution scope
1186
     * @param  nodePath       JSON Pointer representing path to current node
1187
     * @param  fetchDoc       Function to fetch remote JSON documents (optional)
1188
     * @param  parentSchema   Optional pointer to the parent schema, used to
1189
     *                        support required keyword in Draft 3
1190
     * @param  ownName        Optional pointer to a node name, used to support
1191
     *                        the 'required' keyword in Draft 3
1192
     * @param  docCache       Cache of resolved and fetched remote documents
1193
     * @param  schemaRegistry Registry of populated schemas
1194
     */
1195
    template<typename AdapterType>
1196
    void resolveThenPopulateSchema(
1197
        Schema &rootSchema,
1198
        const AdapterType &rootNode,
1199
        const AdapterType &node,
1200
        const Subschema &subschema,
1201
        const std::optional<std::string> currentScope,
1202
        const std::string &nodePath,
1203
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1204
        const Subschema *parentSchema,
1205
        const std::string *ownName,
1206
        typename DocumentCache<AdapterType>::Type &docCache,
1207
        SchemaRegistry &schemaRegistry)
1208
57.9k
    {
1209
57.9k
        std::string jsonRef;
1210
57.9k
        if (!extractJsonReference(node, jsonRef)) {
1211
47.5k
            populateSchema(rootSchema, rootNode, node, subschema, currentScope, nodePath, fetchDoc, parentSchema,
1212
47.5k
                    ownName, docCache, schemaRegistry);
1213
47.5k
            return;
1214
47.5k
        }
1215
1216
        // Returns a document URI if the reference points somewhere
1217
        // other than the current document
1218
10.3k
        const std::optional<std::string> documentUri = internal::json_reference::getJsonReferenceUri(jsonRef);
1219
1220
        // Extract JSON Pointer from JSON Reference
1221
10.3k
        const std::string actualJsonPointer = sanitiseJsonPointer(
1222
10.3k
                internal::json_reference::getJsonReferencePointer(jsonRef));
1223
1224
10.3k
        const std::optional<std::string> actualDocumentUri =
1225
10.3k
                resolveDocumentUri(currentScope, documentUri);
1226
1227
10.3k
        if (!actualJsonPointer.empty() && actualJsonPointer[0] != '/') {
1228
2.39k
            const std::string idRef = actualDocumentUri ?
1229
1.88k
                    (*actualDocumentUri + "#" + actualJsonPointer) :
1230
2.39k
                    ("#" + actualJsonPointer);
1231
2.39k
            std::string registeredPath;
1232
2.39k
            std::optional<std::string> registeredScope;
1233
2.39k
            if (findSchemaById(rootNode, currentScope, idRef,
1234
2.39k
                        "", registeredPath, registeredScope)) {
1235
0
                const AdapterType registeredRoot =
1236
0
                        internal::json_pointer::resolveJsonPointerStrict(
1237
0
                                rootNode, registeredPath);
1238
1239
0
                resolveThenPopulateSchema(rootSchema, registeredRoot,
1240
0
                        registeredRoot, subschema, registeredScope,
1241
0
                        "", fetchDoc, parentSchema, ownName,
1242
0
                        docCache, schemaRegistry);
1243
0
                return;
1244
0
            }
1245
2.39k
        }
1246
1247
10.3k
        if (actualDocumentUri && (!currentScope || *actualDocumentUri != *currentScope)) {
1248
5.07k
            std::string registeredPath;
1249
5.07k
            std::optional<std::string> registeredScope;
1250
5.07k
            if (findSchemaById(rootNode, currentScope, *actualDocumentUri,
1251
5.07k
                        "", registeredPath, registeredScope)) {
1252
129
                const AdapterType registeredRoot =
1253
129
                        internal::json_pointer::resolveJsonPointerStrict(
1254
129
                                rootNode, registeredPath);
1255
129
                const AdapterType referencedAdapter = actualJsonPointer.empty()
1256
129
                        ? registeredRoot
1257
129
                        : internal::json_pointer::resolveJsonPointerStrict(
1258
18
                                registeredRoot, actualJsonPointer);
1259
1260
129
                resolveThenPopulateSchema(rootSchema, registeredRoot,
1261
129
                        referencedAdapter, subschema, registeredScope,
1262
129
                        actualJsonPointer, fetchDoc,
1263
129
                        parentSchema, ownName, docCache, schemaRegistry);
1264
129
                return;
1265
129
            }
1266
1267
            // Resolve reference against remote document
1268
4.94k
            if (!fetchDoc) {
1269
4.94k
                throwRuntimeError("Fetching of remote JSON References not enabled.");
1270
4.94k
            }
1271
1272
4.94k
            const typename DocumentCache<AdapterType>::DocumentType *newDoc = fetchDoc(*actualDocumentUri);
1273
1274
            // Can't proceed without the remote document
1275
4.94k
            if (!newDoc) {
1276
0
                throwRuntimeError("Failed to fetch referenced schema document: " + *actualDocumentUri);
1277
0
            }
1278
1279
            // Add to document cache
1280
4.94k
            typedef typename DocumentCache<AdapterType>::Type::value_type DocCacheValueType;
1281
1282
4.94k
            docCache.insert(DocCacheValueType(*actualDocumentUri, newDoc));
1283
1284
4.94k
            const AdapterType newRootNode(*newDoc);
1285
1286
4.94k
            const AdapterType &referencedAdapter =
1287
4.94k
                internal::json_pointer::resolveJsonPointerStrict(newRootNode, actualJsonPointer);
1288
1289
            // TODO: Need to detect degenerate circular references
1290
4.94k
            resolveThenPopulateSchema(rootSchema, newRootNode, referencedAdapter,
1291
4.94k
                    subschema, actualDocumentUri, actualJsonPointer, fetchDoc,
1292
4.94k
                    parentSchema, ownName, docCache, schemaRegistry);
1293
1294
5.27k
        } else if (!actualJsonPointer.empty()) {
1295
5.06k
            const AdapterType &referencedAdapter =
1296
5.06k
                    internal::json_pointer::resolveJsonPointerStrict(rootNode, actualJsonPointer);
1297
1298
5.06k
            std::optional<std::string> referencedScope = currentScope;
1299
5.06k
            findScopeForPath(rootNode, currentScope, actualJsonPointer, "",
1300
5.06k
                    referencedScope);
1301
1302
5.06k
            resolveThenPopulateSchema(rootSchema, rootNode, referencedAdapter,
1303
5.06k
                    subschema, referencedScope, actualJsonPointer, fetchDoc,
1304
5.06k
                    parentSchema, ownName, docCache, schemaRegistry);
1305
5.06k
        } else {
1306
212
            throwRuntimeError("Cannot resolve reference \"" + jsonRef + "\".");
1307
212
        }
1308
10.3k
    }
1309
1310
    /**
1311
     * @brief   Make a new AllOfConstraint object
1312
     *
1313
     * @param   rootSchema     The Schema instance, and root subschema, through
1314
     *                         which other subschemas can be created and modified
1315
     * @param   rootNode       Reference to the node from which JSON References
1316
     *                         will be resolved when they refer to the current
1317
     *                         document; used for recursive parsing of schemas
1318
     * @param   node           JSON node containing an array of child schemas
1319
     * @param   currentScope   URI for current resolution scope
1320
     * @param   nodePath       JSON Pointer representing path to current node
1321
     * @param   fetchDoc       Function to fetch remote JSON documents (optional)
1322
     * @param   docCache       Cache of resolved and fetched remote documents
1323
     * @param   schemaRegistry Registry of populated schemas
1324
     *
1325
     * @return  pointer to a new AllOfConstraint object that belongs to the
1326
     *          caller
1327
     */
1328
    template<typename AdapterType>
1329
    constraints::AllOfConstraint makeAllOfConstraint(
1330
        Schema &rootSchema,
1331
        const AdapterType &rootNode,
1332
        const AdapterType &node,
1333
        const std::optional<std::string> currentScope,
1334
        const std::string &nodePath,
1335
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1336
        typename DocumentCache<AdapterType>::Type &docCache,
1337
        SchemaRegistry &schemaRegistry)
1338
19.2k
    {
1339
19.2k
        if (!node.maybeArray()) {
1340
72
            throwRuntimeError("Expected array value for 'allOf' constraint.");
1341
72
        }
1342
1343
19.2k
        constraints::AllOfConstraint constraint;
1344
1345
19.2k
        int index = 0;
1346
61.4k
        for (const AdapterType schemaNode : node.asArray()) {
1347
61.4k
            if (schemaNode.maybeObject() || (m_version == kDraft7 && schemaNode.isBool())) {
1348
61.2k
                const std::string childPath = nodePath + "/" + std::to_string(index);
1349
61.2k
                const Subschema *subschema = makeOrReuseSchema<AdapterType>(
1350
61.2k
                        rootSchema, rootNode, schemaNode, currentScope,
1351
61.2k
                        childPath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
1352
61.2k
                constraint.addSubschema(subschema);
1353
61.2k
                index++;
1354
61.2k
            } else {
1355
206
                throwRuntimeError("Expected element to be a valid schema in 'allOf' constraint.");
1356
206
            }
1357
61.4k
        }
1358
1359
19.2k
        return constraint;
1360
19.2k
    }
1361
1362
    /**
1363
     * @brief   Make a new constraint object for Draft 3's 'extends' keyword.
1364
     *
1365
     * The value of 'extends' may be either a single schema or an array of
1366
     * schemas. The instance is required to validate against every schema
1367
     * listed, so the keyword is modelled as an AllOfConstraint.
1368
     */
1369
    template<typename AdapterType>
1370
    constraints::AllOfConstraint makeExtendsConstraint(
1371
        Schema &rootSchema,
1372
        const AdapterType &rootNode,
1373
        const AdapterType &node,
1374
        const std::optional<std::string> currentScope,
1375
        const std::string &nodePath,
1376
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1377
        typename DocumentCache<AdapterType>::Type &docCache,
1378
        SchemaRegistry &schemaRegistry)
1379
0
    {
1380
0
        constraints::AllOfConstraint constraint;
1381
1382
0
        if (node.maybeArray()) {
1383
0
            int index = 0;
1384
0
            for (const AdapterType schemaNode : node.asArray()) {
1385
0
                if (!schemaNode.maybeObject()) {
1386
0
                    throwRuntimeError("Expected element to be a valid schema "
1387
0
                            "in 'extends' constraint.");
1388
0
                }
1389
0
                const std::string childPath = nodePath + "/"
1390
0
                        + std::to_string(index);
1391
0
                const Subschema *subschema = makeOrReuseSchema<AdapterType>(
1392
0
                        rootSchema, rootNode, schemaNode, currentScope,
1393
0
                        childPath, fetchDoc, nullptr, nullptr, docCache,
1394
0
                        schemaRegistry);
1395
0
                constraint.addSubschema(subschema);
1396
0
                index++;
1397
0
            }
1398
0
        } else if (node.maybeObject()) {
1399
0
            const Subschema *subschema = makeOrReuseSchema<AdapterType>(
1400
0
                    rootSchema, rootNode, node, currentScope, nodePath,
1401
0
                    fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
1402
0
            constraint.addSubschema(subschema);
1403
0
        } else {
1404
0
            throwRuntimeError("Expected schema or array of schemas for "
1405
0
                    "'extends' constraint.");
1406
0
        }
1407
1408
0
        return constraint;
1409
0
    }
1410
1411
    /**
1412
     * @brief   Make a new AnyOfConstraint object
1413
     *
1414
     * @param   rootSchema     The Schema instance, and root subschema, through
1415
     *                         which other subschemas can be created and modified
1416
     * @param   rootNode       Reference to the node from which JSON References
1417
     *                         will be resolved when they refer to the current
1418
     *                         document; used for recursive parsing of schemas
1419
     * @param   node           JSON node containing an array of child schemas
1420
     * @param   currentScope   URI for current resolution scope
1421
     * @param   nodePath       JSON Pointer representing path to current node
1422
     * @param   fetchDoc       Function to fetch remote JSON documents (optional)
1423
     * @param   docCache       Cache of resolved and fetched remote documents
1424
     * @param   schemaRegistry Registry of populated schemas
1425
     *
1426
     * @return  pointer to a new AnyOfConstraint object that belongs to the
1427
     *          caller
1428
     */
1429
    template<typename AdapterType>
1430
    constraints::AnyOfConstraint makeAnyOfConstraint(
1431
        Schema &rootSchema,
1432
        const AdapterType &rootNode,
1433
        const AdapterType &node,
1434
        const std::optional<std::string> currentScope,
1435
        const std::string &nodePath,
1436
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1437
        typename DocumentCache<AdapterType>::Type &docCache,
1438
        SchemaRegistry &schemaRegistry)
1439
21.1k
    {
1440
21.1k
        if (!node.maybeArray()) {
1441
369
            throwRuntimeError("Expected array value for 'anyOf' constraint.");
1442
369
        }
1443
1444
21.1k
        constraints::AnyOfConstraint constraint;
1445
1446
21.1k
        int index = 0;
1447
42.6k
        for (const AdapterType schemaNode : node.asArray()) {
1448
42.6k
            if (schemaNode.maybeObject() || (m_version == kDraft7 && schemaNode.isBool())) {
1449
42.4k
                const std::string childPath = nodePath + "/" + std::to_string(index);
1450
42.4k
                const Subschema *subschema = makeOrReuseSchema<AdapterType>(
1451
42.4k
                        rootSchema, rootNode, schemaNode, currentScope,
1452
42.4k
                        childPath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
1453
42.4k
                constraint.addSubschema(subschema);
1454
42.4k
                index++;
1455
42.4k
            } else {
1456
191
                throwRuntimeError("Expected array element to be a valid schema in 'anyOf' constraint.");
1457
191
            }
1458
42.6k
        }
1459
1460
21.1k
        return constraint;
1461
21.1k
    }
1462
1463
    /**
1464
     * @brief   Make a new ConditionalConstraint object.
1465
     *
1466
     * @param   rootSchema           The Schema instance, and root subschema,
1467
     *                               through which other subschemas can be
1468
     *                               created and modified
1469
     * @param   rootNode             Reference to the node from which JSON
1470
     *                               References will be resolved when they refer
1471
     *                               to the current document; used for recursive
1472
     *                               parsing of schemas
1473
     * @param   ifNode               Schema that will be used to evaluate the
1474
     *                               conditional.
1475
     * @param   thenNode             Optional pointer to a JSON node containing
1476
     *                               a schema that will be used when the conditional
1477
     *                               evaluates to true.
1478
     * @param   elseNode             Optional pointer to a JSON node containing
1479
     *                               a schema that will be used when the conditional
1480
     *                               evaluates to false.
1481
     * @param   currentScope         URI for current resolution scope
1482
     * @param   nodePath             JSON Pointer representing the path to
1483
     *                               the 'contains' node
1484
     * @param   fetchDoc             Function to fetch remote JSON documents
1485
     *                               (optional)
1486
     * @param   docCache             Cache of resolved and fetched remote
1487
     *                               documents
1488
     * @param   schemaRegistry       Registry of populated schemas
1489
     *
1490
     * @return  pointer to a new ContainsConstraint that belongs to the caller
1491
     */
1492
    template<typename AdapterType>
1493
    constraints::ConditionalConstraint makeConditionalConstraint(
1494
        Schema &rootSchema,
1495
        const AdapterType &rootNode,
1496
        const AdapterType &ifNode,
1497
        const AdapterType *thenNode,
1498
        const AdapterType *elseNode,
1499
        const std::optional<std::string> currentScope,
1500
        const std::string &nodePath,
1501
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1502
        typename DocumentCache<AdapterType>::Type &docCache,
1503
        SchemaRegistry &schemaRegistry)
1504
4.30k
    {
1505
4.30k
        constraints::ConditionalConstraint constraint;
1506
1507
4.30k
        const Subschema *ifSubschema = makeOrReuseSchema<AdapterType>(
1508
4.30k
                rootSchema, rootNode, ifNode, currentScope,
1509
4.30k
                nodePath + "/if", fetchDoc, nullptr, nullptr, docCache,
1510
4.30k
                schemaRegistry);
1511
4.30k
        constraint.setIfSubschema(ifSubschema);
1512
1513
4.30k
        if (thenNode) {
1514
743
            const Subschema *thenSubschema = makeOrReuseSchema<AdapterType>(
1515
743
                    rootSchema, rootNode, *thenNode, currentScope, nodePath + "/then", fetchDoc, nullptr,
1516
743
                    nullptr, docCache, schemaRegistry);
1517
743
            constraint.setThenSubschema(thenSubschema);
1518
743
        }
1519
1520
4.30k
        if (elseNode) {
1521
1.00k
            const Subschema *elseSubschema = makeOrReuseSchema<AdapterType>(
1522
1.00k
                    rootSchema, rootNode, *elseNode, currentScope, nodePath + "/else", fetchDoc, nullptr,
1523
1.00k
                    nullptr, docCache, schemaRegistry);
1524
1.00k
            constraint.setElseSubschema(elseSubschema);
1525
1.00k
        }
1526
1527
4.30k
        return constraint;
1528
4.30k
    }
1529
1530
    /**
1531
     * @brief   Make a new ConstConstraint object.
1532
     *
1533
     * @param   node  JSON node containing an arbitrary value
1534
     *
1535
     * @return  pointer to a new MinimumConstraint that belongs to the caller
1536
     */
1537
    template<typename AdapterType>
1538
    constraints::ConstConstraint makeConstConstraint(const AdapterType &node)
1539
10.9k
    {
1540
10.9k
        constraints::ConstConstraint constraint;
1541
10.9k
        constraint.setValue(node);
1542
10.9k
        return constraint;
1543
10.9k
    }
1544
1545
    /**
1546
     * @brief   Make a new ContainsConstraint object.
1547
     *
1548
     * @param   rootSchema           The Schema instance, and root subschema,
1549
     *                               through which other subschemas can be
1550
     *                               created and modified
1551
     * @param   rootNode             Reference to the node from which JSON
1552
     *                               References will be resolved when they refer
1553
     *                               to the current document; used for recursive
1554
     *                               parsing of schemas
1555
     * @param   contains             Optional pointer to a JSON node containing
1556
     *                               an object mapping property names to
1557
     *                               schemas.
1558
     * @param   currentScope         URI for current resolution scope
1559
     * @param   containsPath         JSON Pointer representing the path to
1560
     *                               the 'contains' node
1561
     * @param   fetchDoc             Function to fetch remote JSON documents
1562
     *                               (optional)
1563
     * @param   docCache             Cache of resolved and fetched remote
1564
     *                               documents
1565
     * @param   schemaRegistry       Registry of populated schemas
1566
     *
1567
     * @return  pointer to a new ContainsConstraint that belongs to the caller
1568
     */
1569
    template<typename AdapterType>
1570
    constraints::ContainsConstraint makeContainsConstraint(
1571
        Schema &rootSchema,
1572
        const AdapterType &rootNode,
1573
        const AdapterType &contains,
1574
        const std::optional<std::string> currentScope,
1575
        const std::string &containsPath,
1576
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1577
        typename DocumentCache<AdapterType>::Type &docCache,
1578
        SchemaRegistry &schemaRegistry)
1579
18.8k
    {
1580
18.8k
        constraints::ContainsConstraint constraint;
1581
1582
18.8k
        if (contains.isObject() || (m_version == kDraft7 && contains.maybeBool())) {
1583
12.9k
            const Subschema *subschema = makeOrReuseSchema<AdapterType>(
1584
12.9k
                    rootSchema, rootNode, contains, currentScope, containsPath,
1585
12.9k
                    fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
1586
12.9k
            constraint.setSubschema(subschema);
1587
1588
12.9k
        } else if (contains.maybeObject()) {
1589
            // If a loosely-typed Adapter type is being used, then we'll
1590
            // assume that an empty schema has been provided.
1591
5.74k
            constraint.setSubschema(rootSchema.emptySubschema());
1592
1593
5.74k
        } else {
1594
            // All other formats will result in an exception being thrown.
1595
114
            throwRuntimeError("Expected valid schema for 'contains' constraint.");
1596
114
        }
1597
1598
18.8k
        return constraint;
1599
18.8k
    }
1600
1601
    /**
1602
     * @brief   Make a new DependenciesConstraint object
1603
     *
1604
     * The dependencies for a property can be defined several ways. When parsing
1605
     * a Draft 4 schema, the following can be used:
1606
     *  - an array that lists the name of each property that must be present
1607
     *    if the dependent property is present
1608
     *  - an object that specifies a schema which must be satisfied if the
1609
     *    dependent property is present
1610
     *
1611
     * When parsing a Draft 3 schema, in addition to the formats above, the
1612
     * following format can be used:
1613
     *  - a string that names a single property that must be present if the
1614
     *    dependent property is present
1615
     *
1616
     * Multiple methods can be used in the same dependency constraint.
1617
     *
1618
     * If the format of any part of the dependency node does not match one
1619
     * of these formats, an exception will be thrown.
1620
     *
1621
     * @param   rootSchema     The Schema instance, and root subschema, through
1622
     *                         which other subschemas can be created and modified
1623
     * @param   rootNode       Reference to the node from which JSON References
1624
     *                         will be resolved when they refer to the current
1625
     *                         document; used for recursive parsing of schemas
1626
     * @param   node           JSON node containing an object that defines a
1627
     *                         mapping of properties to their dependencies.
1628
     * @param   currentScope   URI for current resolution scope
1629
     * @param   nodePath       JSON Pointer representing path to current node
1630
     * @param   fetchDoc       Function to fetch remote JSON documents (optional)
1631
     * @param   docCache       Cache of resolved and fetched remote documents
1632
     * @param   schemaRegistry Registry of populated schemas
1633
     *
1634
     * @return  pointer to a new DependencyConstraint that belongs to the
1635
     *          caller
1636
     */
1637
    template<typename AdapterType>
1638
    constraints::DependenciesConstraint makeDependenciesConstraint(
1639
        Schema &rootSchema,
1640
        const AdapterType &rootNode,
1641
        const AdapterType &node,
1642
        const std::optional<std::string> currentScope,
1643
        const std::string &nodePath,
1644
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1645
        typename DocumentCache<AdapterType>::Type &docCache,
1646
        SchemaRegistry &schemaRegistry)
1647
17.0k
    {
1648
17.0k
        if (!node.maybeObject()) {
1649
33
            throwRuntimeError("Expected valid subschema for 'dependencies' constraint.");
1650
33
        }
1651
1652
17.0k
        constraints::DependenciesConstraint dependenciesConstraint;
1653
1654
        // Process each of the dependency mappings defined by the object
1655
70.4k
        for (const typename AdapterType::ObjectMember member : node.asObject()) {
1656
1657
            // First, we attempt to parse the value of the dependency mapping
1658
            // as an array of strings. If the Adapter type does not support
1659
            // strict types, then an empty string or empty object will be cast
1660
            // to an array, and the resulting dependency list will be empty.
1661
            // This is equivalent to using an empty object, but does mean that
1662
            // if the user provides an actual string then this error will not
1663
            // be detected.
1664
70.4k
            if (member.second.maybeArray()) {
1665
                // Parse an array of dependency names
1666
39.0k
                std::vector<std::string> dependentPropertyNames;
1667
39.0k
                for (const AdapterType dependencyName : member.second.asArray()) {
1668
15.8k
                    if (dependencyName.maybeString()) {
1669
15.8k
                        dependentPropertyNames.push_back(dependencyName.getString());
1670
15.8k
                    } else {
1671
12
                        throwRuntimeError("Expected string value in dependency list of property '" +
1672
12
                            member.first + "' in 'dependencies' constraint.");
1673
12
                    }
1674
15.8k
                }
1675
1676
39.0k
                dependenciesConstraint.addPropertyDependencies(member.first,
1677
39.0k
                        dependentPropertyNames);
1678
1679
            // If the value of dependency mapping could not be processed as an
1680
            // array, we'll try to process it as an object instead. Note that
1681
            // strict type comparison is used here, since we've already
1682
            // exercised the flexibility by loosely-typed Adapter types. If the
1683
            // value of the dependency mapping is an object, then we'll try to
1684
            // process it as a dependent schema.
1685
39.0k
            } else if (member.second.isObject() || (m_version == kDraft7 && member.second.maybeBool())) {
1686
                // Parse dependent subschema
1687
28.8k
                const std::string childPath = nodePath + "/" +
1688
28.8k
                        escapeJsonPointerToken(member.first);
1689
28.8k
                const Subschema *childSubschema =
1690
28.8k
                        makeOrReuseSchema<AdapterType>(rootSchema, rootNode,
1691
28.8k
                                member.second, currentScope, childPath,
1692
28.8k
                                fetchDoc, nullptr, nullptr, docCache,
1693
28.8k
                                schemaRegistry);
1694
28.8k
                dependenciesConstraint.addSchemaDependency(member.first,
1695
28.8k
                        childSubschema);
1696
1697
            // If we're supposed to be parsing a Draft3 schema, then the value
1698
            // of the dependency mapping can also be a string containing the
1699
            // name of a single dependency.
1700
28.8k
            } else if (m_version == kDraft3 && member.second.isString()) {
1701
0
                dependenciesConstraint.addPropertyDependency(member.first,
1702
0
                        member.second.getString());
1703
1704
            // All other types result in an exception being thrown.
1705
2.44k
            } else {
1706
2.44k
                throwRuntimeError("Invalid dependencies definition.");
1707
2.44k
            }
1708
70.4k
        }
1709
1710
17.0k
        return dependenciesConstraint;
1711
17.0k
    }
1712
1713
    /**
1714
     * @brief   Make a new EnumConstraint object.
1715
     *
1716
     * @param   node  JSON node containing an array of values permitted by the
1717
     *                constraint.
1718
     *
1719
     * @return  pointer to a new EnumConstraint that belongs to the caller
1720
     */
1721
    template<typename AdapterType>
1722
    constraints::EnumConstraint makeEnumConstraint(
1723
        const AdapterType &node)
1724
8.82k
    {
1725
        // Make a copy of each value in the enum array
1726
8.82k
        constraints::EnumConstraint constraint;
1727
105k
        for (const AdapterType value : node.getArray()) {
1728
105k
            constraint.addValue(value);
1729
105k
        }
1730
1731
        /// @todo This will make another copy of the values while constructing
1732
        /// the EnumConstraint. Move semantics in C++11 should make it possible
1733
        /// to avoid these copies without complicating the implementation of the
1734
        /// EnumConstraint class.
1735
8.82k
        return constraint;
1736
8.82k
    }
1737
1738
    /**
1739
     * @brief   Make a new FormatConstraint object
1740
     *
1741
     * @param   node  JSON node containing the configuration for this constraint
1742
     *
1743
     * @return  pointer to a new FormatConstraint that belongs to the caller
1744
     */
1745
    template<typename AdapterType>
1746
    constraints::FormatConstraint makeFormatConstraint(
1747
        const AdapterType &node)
1748
6.04k
    {
1749
6.04k
        if (node.isString()) {
1750
5.98k
            const std::string value = node.asString();
1751
5.98k
            if (!value.empty()) {
1752
5.98k
                constraints::FormatConstraint constraint;
1753
5.98k
                constraint.setFormat(value);
1754
5.98k
                return constraint;
1755
5.98k
            }
1756
5.98k
        }
1757
1758
57
        throwRuntimeError("Expected a string value for 'format' constraint.");
1759
57
    }
1760
1761
    /**
1762
     * @brief   Make a new ItemsConstraint object.
1763
     *
1764
     * @param   rootSchema           The Schema instance, and root subschema,
1765
     *                               through which other subschemas can be
1766
     *                               created and modified
1767
     * @param   rootNode             Reference to the node from which JSON
1768
     *                               References will be resolved when they refer
1769
     *                               to the current document; used for recursive
1770
     *                               parsing of schemas
1771
     * @param   items                Optional pointer to a JSON node containing
1772
     *                               an object mapping property names to
1773
     *                               schemas.
1774
     * @param   additionalItems      Optional pointer to a JSON node containing
1775
     *                               an additional properties schema or a
1776
     *                               boolean value.
1777
     * @param   currentScope         URI for current resolution scope
1778
     * @param   itemsPath            JSON Pointer representing the path to
1779
     *                               the 'items' node
1780
     * @param   additionalItemsPath  JSON Pointer representing the path to
1781
     *                               the 'additionalItems' node
1782
     * @param   fetchDoc             Function to fetch remote JSON documents
1783
     *                               (optional)
1784
     * @param   docCache             Cache of resolved and fetched remote
1785
     *                               documents
1786
     * @param   schemaRegistry       Registry of populated schemas
1787
     *
1788
     * @return  pointer to a new ItemsConstraint that belongs to the caller
1789
     */
1790
    template<typename AdapterType>
1791
    constraints::LinearItemsConstraint makeLinearItemsConstraint(
1792
        Schema &rootSchema,
1793
        const AdapterType &rootNode,
1794
        const AdapterType *items,
1795
        const AdapterType *additionalItems,
1796
        const std::optional<std::string> currentScope,
1797
        const std::string &itemsPath,
1798
        const std::string &additionalItemsPath,
1799
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1800
        typename DocumentCache<AdapterType>::Type &docCache,
1801
        SchemaRegistry &schemaRegistry)
1802
27.6k
    {
1803
27.6k
        constraints::LinearItemsConstraint constraint;
1804
1805
        // Construct a Schema object for the additionalItems constraint,
1806
        // if the additionalItems property is present
1807
27.6k
        if (additionalItems) {
1808
1.95k
            if (additionalItems->maybeBool()) {
1809
                // If the value of the additionalItems property is a boolean
1810
                // and is set to true, then additional array items do not need
1811
                // to satisfy any constraints.
1812
41
                if (additionalItems->asBool()) {
1813
5
                    constraint.setAdditionalItemsSubschema(rootSchema.emptySubschema());
1814
5
                }
1815
1.91k
            } else if (additionalItems->maybeObject()) {
1816
                // If the value of the additionalItems property is an object,
1817
                // then it should be parsed into a Schema object, which will be
1818
                // used to validate additional array items.
1819
1.90k
                const Subschema *subschema = makeOrReuseSchema<AdapterType>(
1820
1.90k
                        rootSchema, rootNode, *additionalItems, currentScope,
1821
1.90k
                        additionalItemsPath, fetchDoc, nullptr, nullptr, docCache,
1822
1.90k
                        schemaRegistry);
1823
1.90k
                constraint.setAdditionalItemsSubschema(subschema);
1824
1.90k
            } else {
1825
                // Any other format for the additionalItems property will result
1826
                // in an exception being thrown.
1827
10
                throwRuntimeError("Expected bool or object value for 'additionalItems'");
1828
10
            }
1829
25.6k
        } else {
1830
            // The default value for the additionalItems property is an empty
1831
            // object, which means that additional array items do not need to
1832
            // satisfy any constraints.
1833
25.6k
            constraint.setAdditionalItemsSubschema(rootSchema.emptySubschema());
1834
25.6k
        }
1835
1836
        // Construct a Schema object for each item in the items array.
1837
        // If the items constraint is not provided, then array items
1838
        // will be validated against the additionalItems schema.
1839
27.6k
        if (items) {
1840
27.2k
            if (items->isArray()) {
1841
                // If the items constraint contains an array, then it should
1842
                // contain a list of child schemas which will be used to
1843
                // validate the values at the corresponding indexes in a target
1844
                // array.
1845
27.2k
                int index = 0;
1846
250k
                for (const AdapterType v : items->getArray()) {
1847
250k
                    const std::string childPath = itemsPath + "/" +
1848
250k
                            std::to_string(index);
1849
250k
                    const Subschema *subschema = makeOrReuseSchema<AdapterType>(
1850
250k
                            rootSchema, rootNode, v, currentScope, childPath,
1851
250k
                            fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
1852
250k
                    constraint.addItemSubschema(subschema);
1853
250k
                    index++;
1854
250k
                }
1855
27.2k
            } else {
1856
0
                throwRuntimeError("Expected array value for non-singular 'items' constraint.");
1857
0
            }
1858
27.2k
        }
1859
1860
27.6k
        return constraint;
1861
27.6k
    }
1862
1863
    /**
1864
     * @brief   Make a new ItemsConstraint object.
1865
     *
1866
     * @param   rootSchema           The Schema instance, and root subschema,
1867
     *                               through which other subschemas can be
1868
     *                               created and modified
1869
     * @param   rootNode             Reference to the node from which JSON
1870
     *                               References will be resolved when they refer
1871
     *                               to the current document; used for recursive
1872
     *                               parsing of schemas
1873
     * @param   items                Optional pointer to a JSON node containing
1874
     *                               an object mapping property names to
1875
     *                               schemas.
1876
     * @param   currentScope         URI for current resolution scope
1877
     * @param   itemsPath            JSON Pointer representing the path to
1878
     *                               the 'items' node
1879
     * @param   fetchDoc             Function to fetch remote JSON documents
1880
     *                               (optional)
1881
     * @param   docCache             Cache of resolved and fetched remote
1882
     *                               documents
1883
     * @param   schemaRegistry       Registry of populated schemas
1884
     *
1885
     * @return  pointer to a new ItemsConstraint that belongs to the caller
1886
     */
1887
    template<typename AdapterType>
1888
    constraints::SingularItemsConstraint makeSingularItemsConstraint(
1889
        Schema &rootSchema,
1890
        const AdapterType &rootNode,
1891
        const AdapterType &items,
1892
        const std::optional<std::string> currentScope,
1893
        const std::string &itemsPath,
1894
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
1895
        typename DocumentCache<AdapterType>::Type &docCache,
1896
        SchemaRegistry &schemaRegistry)
1897
15.9k
    {
1898
15.9k
        constraints::SingularItemsConstraint constraint;
1899
1900
        // Construct a Schema object for each item in the items array, if an
1901
        // array is provided, or a single Schema object, in an object value is
1902
        // provided. If the items constraint is not provided, then array items
1903
        // will be validated against the additionalItems schema.
1904
15.9k
        if (items.isObject() || (m_version == kDraft7 && items.maybeBool())) {
1905
            // If the items constraint contains an object value, then it
1906
            // should contain a Schema that will be used to validate all
1907
            // items in a target array. Any schema defined by the
1908
            // additionalItems constraint will be ignored.
1909
15.1k
            const Subschema *subschema = makeOrReuseSchema<AdapterType>(
1910
15.1k
                    rootSchema, rootNode, items, currentScope, itemsPath,
1911
15.1k
                    fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
1912
15.1k
            constraint.setItemsSubschema(subschema);
1913
1914
15.1k
        } else if (items.maybeObject()) {
1915
            // If a loosely-typed Adapter type is being used, then we'll
1916
            // assume that an empty schema has been provided.
1917
0
            constraint.setItemsSubschema(rootSchema.emptySubschema());
1918
1919
823
        } else {
1920
            // All other formats will result in an exception being thrown.
1921
823
            throwRuntimeError("Expected valid schema for singular 'items' constraint.");
1922
823
        }
1923
1924
15.9k
        return constraint;
1925
15.9k
    }
1926
1927
    /**
1928
     * @brief   Make a new MaximumConstraint object (draft 3 and 4).
1929
     *
1930
     * @param   node              JSON node containing the maximum value.
1931
     * @param   exclusiveMaximum  Optional pointer to a JSON boolean value that
1932
     *                            indicates whether maximum value is excluded
1933
     *                            from the range of permitted values.
1934
     *
1935
     * @return  pointer to a new MaximumConstraint that belongs to the caller
1936
     */
1937
    template<typename AdapterType>
1938
    constraints::MaximumConstraint makeMaximumConstraint(
1939
        const AdapterType &node,
1940
        const AdapterType *exclusiveMaximum)
1941
2.84k
    {
1942
2.84k
        if (!node.maybeDouble()) {
1943
456
            throwRuntimeError("Expected numeric value for maximum constraint.");
1944
456
        }
1945
1946
2.84k
        constraints::MaximumConstraint constraint;
1947
2.84k
        constraint.setMaximum(node.asDouble());
1948
1949
2.84k
        if (exclusiveMaximum) {
1950
154
            if (!exclusiveMaximum->maybeBool()) {
1951
71
                throwRuntimeError("Expected boolean value for exclusiveMaximum constraint.");
1952
71
            }
1953
1954
154
            constraint.setExclusiveMaximum(exclusiveMaximum->asBool());
1955
154
        }
1956
1957
2.84k
        return constraint;
1958
2.84k
    }
1959
1960
    /**
1961
     * @brief   Make a new MaximumConstraint object that is always exclusive (draft 7).
1962
     *
1963
     * @param   node       JSON node containing an integer, representing the maximum value.
1964
     *
1965
     * @return  pointer to a new Maximum that belongs to the caller
1966
     */
1967
    template<typename AdapterType>
1968
    constraints::MaximumConstraint makeMaximumConstraintExclusive(const AdapterType &node)
1969
776
    {
1970
776
        if (!node.maybeDouble()) {
1971
198
            throwRuntimeError("Expected numeric value for exclusiveMaximum constraint.");
1972
198
        }
1973
1974
776
        constraints::MaximumConstraint constraint;
1975
776
        constraint.setMaximum(node.asDouble());
1976
776
        constraint.setExclusiveMaximum(true);
1977
776
        return constraint;
1978
776
    }
1979
1980
    /**
1981
     * @brief   Make a new MaxItemsConstraint object.
1982
     *
1983
     * @param   node  JSON node containing an integer value representing the
1984
     *                maximum number of items that may be contained by an array.
1985
     *
1986
     * @return  pointer to a new MaxItemsConstraint that belongs to the caller.
1987
     */
1988
    template<typename AdapterType>
1989
    constraints::MaxItemsConstraint makeMaxItemsConstraint(
1990
        const AdapterType &node)
1991
3.00k
    {
1992
3.00k
        if (node.maybeInteger()) {
1993
2.90k
            const int64_t value = node.asInteger();
1994
2.90k
            if (value >= 0) {
1995
2.76k
                constraints::MaxItemsConstraint constraint;
1996
2.76k
                constraint.setMaxItems(value);
1997
2.76k
                return constraint;
1998
2.76k
            }
1999
2.90k
        }
2000
2001
231
        throwRuntimeError("Expected non-negative integer value for 'maxItems' constraint.");
2002
231
    }
2003
2004
    /**
2005
     * @brief   Make a new MaxLengthConstraint object.
2006
     *
2007
     * @param   node  JSON node containing an integer value representing the
2008
     *                maximum length of a string.
2009
     *
2010
     * @return  pointer to a new MaxLengthConstraint that belongs to the caller
2011
     */
2012
    template<typename AdapterType>
2013
    constraints::MaxLengthConstraint makeMaxLengthConstraint(
2014
        const AdapterType &node)
2015
3.66k
    {
2016
3.66k
        if (node.maybeInteger()) {
2017
3.24k
            const int64_t value = node.asInteger();
2018
3.24k
            if (value >= 0) {
2019
3.11k
                constraints::MaxLengthConstraint constraint;
2020
3.11k
                constraint.setMaxLength(value);
2021
3.11k
                return constraint;
2022
3.11k
            }
2023
3.24k
        }
2024
2025
555
        throwRuntimeError("Expected a non-negative integer value for 'maxLength' constraint.");
2026
555
    }
2027
2028
    /**
2029
     * @brief   Make a new MaxPropertiesConstraint object.
2030
     *
2031
     * @param   node  JSON node containing an integer value representing the
2032
     *                maximum number of properties that may be contained by an
2033
     *                object.
2034
     *
2035
     * @return  pointer to a new MaxPropertiesConstraint that belongs to the
2036
     *          caller
2037
     */
2038
    template<typename AdapterType>
2039
    constraints::MaxPropertiesConstraint makeMaxPropertiesConstraint(
2040
        const AdapterType &node)
2041
2.32k
    {
2042
2.32k
        if (node.maybeInteger()) {
2043
2.18k
            int64_t value = node.asInteger();
2044
2.18k
            if (value >= 0) {
2045
2.07k
                constraints::MaxPropertiesConstraint constraint;
2046
2.07k
                constraint.setMaxProperties(value);
2047
2.07k
                return constraint;
2048
2.07k
            }
2049
2.18k
        }
2050
2051
248
        throwRuntimeError("Expected a non-negative integer for 'maxProperties' constraint.");
2052
248
    }
2053
2054
    /**
2055
     * @brief  Make a new MinimumConstraint object (draft 3 and 4).
2056
     *
2057
     * @param  node              JSON node containing an integer, representing
2058
     *                           the minimum value.
2059
     *
2060
     * @param  exclusiveMinimum  Optional pointer to a JSON boolean value that
2061
     *                           indicates whether the minimum value is
2062
     *                           excluded from the range of permitted values.
2063
     *
2064
     * @return  pointer to a new MinimumConstraint that belongs to the caller
2065
     */
2066
    template<typename AdapterType>
2067
    constraints::MinimumConstraint makeMinimumConstraint(
2068
        const AdapterType &node,
2069
        const AdapterType *exclusiveMinimum)
2070
6.52k
    {
2071
6.52k
        if (!node.maybeDouble()) {
2072
136
            throwRuntimeError("Expected numeric value for minimum constraint.");
2073
136
        }
2074
2075
6.52k
        constraints::MinimumConstraint constraint;
2076
6.52k
        constraint.setMinimum(node.asDouble());
2077
2078
6.52k
        if (exclusiveMinimum) {
2079
12
            if (!exclusiveMinimum->maybeBool()) {
2080
1
                throwRuntimeError("Expected boolean value for 'exclusiveMinimum' constraint.");
2081
1
            }
2082
2083
12
            constraint.setExclusiveMinimum(exclusiveMinimum->asBool());
2084
12
        }
2085
2086
6.52k
        return constraint;
2087
6.52k
    }
2088
2089
    /**
2090
     * @brief   Make a new MinimumConstraint object that is always exclusive (draft 7).
2091
     *
2092
     * @param   node       JSON node containing an integer, representing the minimum value.
2093
     *
2094
     * @return  pointer to a new MinimumConstraint that belongs to the caller
2095
     */
2096
    template<typename AdapterType>
2097
    constraints::MinimumConstraint makeMinimumConstraintExclusive(const AdapterType &node)
2098
1.02k
    {
2099
1.02k
        if (!node.maybeDouble()) {
2100
72
            throwRuntimeError("Expected numeric value for exclusiveMinimum constraint.");
2101
72
        }
2102
2103
1.02k
        constraints::MinimumConstraint constraint;
2104
1.02k
        constraint.setMinimum(node.asDouble());
2105
1.02k
        constraint.setExclusiveMinimum(true);
2106
1.02k
        return constraint;
2107
1.02k
    }
2108
2109
    /**
2110
     * @brief  Make a new MinItemsConstraint object.
2111
     *
2112
     * @param  node  JSON node containing an integer value representing the
2113
     *               minimum number of items that may be contained by an array.
2114
     *
2115
     * @return  pointer to a new MinItemsConstraint that belongs to the caller
2116
     */
2117
    template<typename AdapterType>
2118
    constraints::MinItemsConstraint makeMinItemsConstraint(const AdapterType &node)
2119
3.46k
    {
2120
3.46k
        if (node.maybeInteger()) {
2121
3.24k
            const int64_t value = node.asInteger();
2122
3.24k
            if (value >= 0) {
2123
3.15k
                constraints::MinItemsConstraint constraint;
2124
3.15k
                constraint.setMinItems(value);
2125
3.15k
                return constraint;
2126
3.15k
            }
2127
3.24k
        }
2128
2129
310
        throwRuntimeError("Expected a non-negative integer value for 'minItems' constraint.");
2130
310
    }
2131
2132
    /**
2133
     * @brief  Make a new MinLengthConstraint object.
2134
     *
2135
     * @param  node  JSON node containing an integer value representing the
2136
     *               minimum length of a string.
2137
     *
2138
     * @return  pointer to a new MinLengthConstraint that belongs to the caller
2139
     */
2140
    template<typename AdapterType>
2141
    constraints::MinLengthConstraint makeMinLengthConstraint(const AdapterType &node)
2142
4.80k
    {
2143
4.80k
        if (node.maybeInteger()) {
2144
4.44k
            const int64_t value = node.asInteger();
2145
4.44k
            if (value >= 0) {
2146
4.24k
                constraints::MinLengthConstraint constraint;
2147
4.24k
                constraint.setMinLength(value);
2148
4.24k
                return constraint;
2149
4.24k
            }
2150
4.44k
        }
2151
2152
560
        throwRuntimeError("Expected a non-negative integer value for 'minLength' constraint.");
2153
560
    }
2154
2155
2156
    /**
2157
     * @brief   Make a new MaxPropertiesConstraint object.
2158
     *
2159
     * @param   node  JSON node containing an integer value representing the
2160
     *                minimum number of properties that may be contained by an
2161
     *                object.
2162
     *
2163
     * @return  pointer to a new MinPropertiesConstraint that belongs to the
2164
     *          caller
2165
     */
2166
    template<typename AdapterType>
2167
    constraints::MinPropertiesConstraint makeMinPropertiesConstraint(const AdapterType &node)
2168
2.75k
    {
2169
2.75k
        if (node.maybeInteger()) {
2170
2.68k
            int64_t value = node.asInteger();
2171
2.68k
            if (value >= 0) {
2172
2.58k
                constraints::MinPropertiesConstraint constraint;
2173
2.58k
                constraint.setMinProperties(value);
2174
2.58k
                return constraint;
2175
2.58k
            }
2176
2.68k
        }
2177
2178
167
        throwRuntimeError("Expected a non-negative integer for 'minProperties' constraint.");
2179
167
    }
2180
2181
    /**
2182
     * @brief   Make a new MultipleOfDoubleConstraint object
2183
     *
2184
     * @param   node  JSON node containing an numeric value that a target value
2185
     *                must divide by in order to satisfy this constraint
2186
     *
2187
     * @return  a MultipleOfConstraint
2188
     */
2189
    template<typename AdapterType>
2190
    constraints::MultipleOfDoubleConstraint makeMultipleOfDoubleConstraint(const AdapterType &node)
2191
1.92k
    {
2192
1.92k
        constraints::MultipleOfDoubleConstraint constraint;
2193
1.92k
        constraint.setDivisor(node.asDouble());
2194
1.92k
        return constraint;
2195
1.92k
    }
2196
2197
    /**
2198
     * @brief   Make a new MultipleOfIntConstraint object
2199
     *
2200
     * @param   node  JSON node containing a numeric value that a target value
2201
     *                must divide by in order to satisfy this constraint
2202
     *
2203
     * @return  a MultipleOfIntConstraint
2204
     */
2205
    template<typename AdapterType>
2206
    constraints::MultipleOfIntConstraint makeMultipleOfIntConstraint(const AdapterType &node)
2207
6.16k
    {
2208
6.16k
        constraints::MultipleOfIntConstraint constraint;
2209
6.16k
        constraint.setDivisor(node.asInteger());
2210
6.16k
        return constraint;
2211
6.16k
    }
2212
2213
    /**
2214
     * @brief   Make a new NotConstraint object
2215
     *
2216
     * @param   rootSchema     The Schema instance, and root subschema, through
2217
     *                         which other subschemas can be created and modified
2218
     * @param   rootNode       Reference to the node from which JSON References
2219
     *                         will be resolved when they refer to the current
2220
     *                         document; used for recursive parsing of schemas
2221
     * @param   node           JSON node containing a schema
2222
     * @param   currentScope   URI for current resolution scope
2223
     * @param   nodePath       JSON Pointer representing path to current node
2224
     * @param   fetchDoc       Function to fetch remote JSON documents (optional)
2225
     * @param   docCache       Cache of resolved and fetched remote documents
2226
     * @param   schemaRegistry Registry of populated schemas
2227
     *
2228
     * @return  pointer to a new NotConstraint object that belongs to the caller
2229
     */
2230
    template<typename AdapterType>
2231
    constraints::NotConstraint makeNotConstraint(
2232
        Schema &rootSchema,
2233
        const AdapterType &rootNode,
2234
        const AdapterType &node,
2235
        const std::optional<std::string> currentScope,
2236
        const std::string &nodePath,
2237
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
2238
        typename DocumentCache<AdapterType>::Type &docCache,
2239
        SchemaRegistry &schemaRegistry)
2240
13.3k
    {
2241
13.3k
        if (node.maybeObject() || (m_version == kDraft7 && node.maybeBool())) {
2242
12.6k
            const Subschema *subschema = makeOrReuseSchema<AdapterType>(
2243
12.6k
                    rootSchema, rootNode, node, currentScope, nodePath,
2244
12.6k
                    fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
2245
12.6k
            constraints::NotConstraint constraint;
2246
12.6k
            constraint.setSubschema(subschema);
2247
12.6k
            return constraint;
2248
12.6k
        }
2249
2250
723
        throwRuntimeError("Expected object value for 'not' constraint.");
2251
723
    }
2252
2253
    /**
2254
     * @brief   Make a new OneOfConstraint object
2255
     *
2256
     * @param   rootSchema     The Schema instance, and root subschema, through
2257
     *                         which other subschemas can be created and modified
2258
     * @param   rootNode       Reference to the node from which JSON References
2259
     *                         will be resolved when they refer to the current
2260
     *                         document; used for recursive parsing of schemas
2261
     * @param   node           JSON node containing an array of child schemas
2262
     * @param   currentScope   URI for current resolution scope
2263
     * @param   nodePath       JSON Pointer representing path to current node
2264
     * @param   fetchDoc       Function to fetch remote JSON documents (optional)
2265
     * @param   docCache       Cache of resolved and fetched remote documents
2266
     * @param   schemaRegistry Registry of populated schemas
2267
     *
2268
     * @return  pointer to a new OneOfConstraint that belongs to the caller
2269
     */
2270
    template<typename AdapterType>
2271
    constraints::OneOfConstraint makeOneOfConstraint(
2272
        Schema &rootSchema,
2273
        const AdapterType &rootNode,
2274
        const AdapterType &node,
2275
        const std::optional<std::string> currentScope,
2276
        const std::string &nodePath,
2277
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
2278
        typename DocumentCache<AdapterType>::Type &docCache,
2279
        SchemaRegistry &schemaRegistry)
2280
14.3k
    {
2281
14.3k
        constraints::OneOfConstraint constraint;
2282
2283
14.3k
        int index = 0;
2284
64.1k
        for (const AdapterType schemaNode : node.getArray()) {
2285
64.1k
            const std::string childPath = nodePath + "/" + std::to_string(index);
2286
64.1k
            const Subschema *subschema = makeOrReuseSchema<AdapterType>(
2287
64.1k
                rootSchema, rootNode, schemaNode, currentScope, childPath,
2288
64.1k
                fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
2289
64.1k
            constraint.addSubschema(subschema);
2290
64.1k
            index++;
2291
64.1k
        }
2292
2293
14.3k
        return constraint;
2294
14.3k
    }
2295
2296
    /**
2297
     * @brief   Make a new PatternConstraint object.
2298
     *
2299
     * @param   node      JSON node containing a pattern string
2300
     *
2301
     * @return  pointer to a new PatternConstraint object that belongs to the
2302
     *          caller
2303
     */
2304
    template<typename AdapterType>
2305
    constraints::PatternConstraint makePatternConstraint(
2306
        const AdapterType &node)
2307
18.6k
    {
2308
18.6k
        constraints::PatternConstraint constraint;
2309
18.6k
        constraint.setPattern(node.getString());
2310
18.6k
        return constraint;
2311
18.6k
    }
2312
2313
    /**
2314
     * @brief   Make a new Properties object.
2315
     *
2316
     * @param   rootSchema                The Schema instance, and root
2317
     *                                    subschema, through which other
2318
     *                                    subschemas can be created and modified
2319
     * @param   rootNode                  Reference to the node from which JSON
2320
     *                                    References will be resolved when they
2321
     *                                    refer to the current document; used
2322
     *                                    for recursive parsing of schemas
2323
     * @param   properties                Optional pointer to a JSON node
2324
     *                                    containing an object mapping property
2325
     *                                    names to schemas.
2326
     * @param   patternProperties         Optional pointer to a JSON node
2327
     *                                    containing an object mapping pattern
2328
     *                                    property names to schemas.
2329
     * @param   additionalProperties      Optional pointer to a JSON node
2330
     *                                    containing an additional properties
2331
     *                                    schema or a boolean value.
2332
     * @param   currentScope              URI for current resolution scope
2333
     * @param   propertiesPath            JSON Pointer representing the path to
2334
     *                                    the 'properties' node
2335
     * @param   patternPropertiesPath     JSON Pointer representing the path to
2336
     *                                    the 'patternProperties' node
2337
     * @param   additionalPropertiesPath  JSON Pointer representing the path to
2338
     *                                    the 'additionalProperties' node
2339
     * @param   fetchDoc                  Function to fetch remote JSON
2340
     *                                    documents (optional)
2341
     * @param   parentSubschema           Optional pointer to the Schema of the
2342
     *                                    parent object, needed to support the
2343
     *                                    'required' keyword in Draft 3
2344
     * @param   docCache                  Cache of resolved and fetched remote
2345
     *                                    documents
2346
     * @param   schemaRegistry            Registry of populated schemas
2347
     *
2348
     * @return  pointer to a new Properties that belongs to the caller
2349
     */
2350
    template<typename AdapterType>
2351
    constraints::PropertiesConstraint makePropertiesConstraint(
2352
        Schema &rootSchema,
2353
        const AdapterType &rootNode,
2354
        const AdapterType *properties,
2355
        const AdapterType *patternProperties,
2356
        const AdapterType *additionalProperties,
2357
        const std::optional<std::string> currentScope,
2358
        const std::string &propertiesPath,
2359
        const std::string &patternPropertiesPath,
2360
        const std::string &additionalPropertiesPath,
2361
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
2362
        const Subschema *parentSubschema,
2363
        typename DocumentCache<AdapterType>::Type &docCache,
2364
        SchemaRegistry &schemaRegistry)
2365
33.7k
    {
2366
33.7k
        typedef typename AdapterType::ObjectMember Member;
2367
2368
33.7k
        constraints::PropertiesConstraint constraint;
2369
2370
        // Create subschemas for 'properties' constraint
2371
33.7k
        if (properties) {
2372
41.6k
            for (const Member m : properties->getObject()) {
2373
41.6k
                const std::string &property = m.first;
2374
41.6k
                const std::string childPath = propertiesPath + "/" + property;
2375
41.6k
                const Subschema *subschema = makeOrReuseSchema<AdapterType>(
2376
41.6k
                        rootSchema, rootNode, m.second, currentScope, childPath,
2377
41.6k
                        fetchDoc, parentSubschema, &property, docCache,
2378
41.6k
                        schemaRegistry);
2379
41.6k
                constraint.addPropertySubschema(property, subschema);
2380
41.6k
            }
2381
10.9k
        }
2382
2383
        // Create subschemas for 'patternProperties' constraint
2384
33.7k
        if (patternProperties) {
2385
176k
            for (const Member m : patternProperties->getObject()) {
2386
176k
                const std::string &pattern = m.first;
2387
176k
                const std::string childPath = patternPropertiesPath + "/" + pattern;
2388
176k
                const Subschema *subschema = makeOrReuseSchema<AdapterType>(
2389
176k
                        rootSchema, rootNode, m.second, currentScope, childPath,
2390
176k
                        fetchDoc, parentSubschema, &pattern, docCache,
2391
176k
                        schemaRegistry);
2392
176k
                constraint.addPatternPropertySubschema(pattern, subschema);
2393
176k
            }
2394
18.9k
        }
2395
2396
        // Create an additionalItems subschema if required
2397
33.7k
        if (additionalProperties) {
2398
            // If additionalProperties has been set, check for a boolean value.
2399
            // Setting 'additionalProperties' to true allows the values of
2400
            // additional properties to take any form. Setting it false
2401
            // prohibits the use of additional properties.
2402
            // If additionalProperties is instead an object, it should be
2403
            // parsed as a schema. If additionalProperties has any other type,
2404
            // then the schema is not valid.
2405
4.22k
            if (additionalProperties->isBool() ||
2406
3.70k
                additionalProperties->maybeBool()) {
2407
                // If it has a boolean value that is 'true', then an empty
2408
                // schema should be used.
2409
830
                if (additionalProperties->asBool()) {
2410
681
                    constraint.setAdditionalPropertiesSubschema(rootSchema.emptySubschema());
2411
681
                }
2412
3.39k
            } else if (additionalProperties->isObject()) {
2413
                // If additionalProperties is an object, it should be used as
2414
                // a child schema.
2415
3.32k
                const Subschema *subschema = makeOrReuseSchema<AdapterType>(
2416
3.32k
                        rootSchema, rootNode, *additionalProperties,
2417
3.32k
                        currentScope, additionalPropertiesPath, fetchDoc, nullptr,
2418
3.32k
                        nullptr, docCache, schemaRegistry);
2419
3.32k
                constraint.setAdditionalPropertiesSubschema(subschema);
2420
3.32k
            } else {
2421
                // All other types are invalid
2422
74
                throwRuntimeError("Invalid type for 'additionalProperties' constraint.");
2423
74
            }
2424
29.5k
        } else {
2425
            // If an additionalProperties constraint is not provided, then the
2426
            // default value is an empty schema.
2427
29.5k
            constraint.setAdditionalPropertiesSubschema(rootSchema.emptySubschema());
2428
29.5k
        }
2429
2430
33.7k
        return constraint;
2431
33.7k
    }
2432
2433
    template<typename AdapterType>
2434
    constraints::PropertyNamesConstraint makePropertyNamesConstraint(
2435
        Schema &rootSchema,
2436
        const AdapterType &rootNode,
2437
        const AdapterType &currentNode,
2438
        const std::optional<std::string> currentScope,
2439
        const std::string &nodePath,
2440
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
2441
        typename DocumentCache<AdapterType>::Type &docCache,
2442
        SchemaRegistry &schemaRegistry)
2443
3.70k
    {
2444
3.70k
        const Subschema *subschema = makeOrReuseSchema<AdapterType>(rootSchema, rootNode, currentNode, currentScope,
2445
3.70k
                nodePath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
2446
3.70k
        constraints::PropertyNamesConstraint constraint;
2447
3.70k
        constraint.setSubschema(subschema);
2448
3.70k
        return constraint;
2449
3.70k
    }
2450
2451
    /**
2452
     * @brief   Make a new RequiredConstraint.
2453
     *
2454
     * This function is used to create new RequiredContraint objects for
2455
     * Draft 3 schemas.
2456
     *
2457
     * @param   node  Node containing a boolean value.
2458
     * @param   name  Name of the required attribute.
2459
     *
2460
     * @return  pointer to a new RequiredConstraint object that belongs to the
2461
     *          caller
2462
     */
2463
    template<typename AdapterType>
2464
    std::optional<constraints::RequiredConstraint>
2465
            makeRequiredConstraintForSelf(const AdapterType &node,
2466
                    const std::string &name)
2467
0
    {
2468
0
        if (!node.maybeBool()) {
2469
0
            throwRuntimeError("Expected boolean value for 'required' attribute.");
2470
0
        }
2471
2472
0
        if (node.asBool()) {
2473
0
            constraints::RequiredConstraint constraint;
2474
0
            constraint.addRequiredProperty(name);
2475
0
            return constraint;
2476
0
        }
2477
2478
0
        return std::optional<constraints::RequiredConstraint>();
2479
0
    }
2480
2481
    /**
2482
     * @brief   Make a new RequiredConstraint.
2483
     *
2484
     * This function is used to create new RequiredContraint objects for
2485
     * Draft 4 schemas.
2486
     *
2487
     * @param   node  Node containing an array of strings.
2488
     *
2489
     * @return  pointer to a new RequiredConstraint object that belongs to the
2490
     *          caller
2491
     */
2492
    template<typename AdapterType>
2493
    constraints::RequiredConstraint makeRequiredConstraint(
2494
        const AdapterType &node)
2495
9.24k
    {
2496
9.24k
        constraints::RequiredConstraint constraint;
2497
2498
30.9k
        for (const AdapterType v : node.getArray()) {
2499
30.9k
            if (!v.maybeString()) {
2500
68
                throwRuntimeError("Expected required property name to be a string value");
2501
68
            }
2502
2503
30.9k
            constraint.addRequiredProperty(v.getString());
2504
30.9k
        }
2505
2506
9.24k
        return constraint;
2507
9.24k
    }
2508
2509
    /**
2510
     * @brief   Make a new TypeConstraint object
2511
     *
2512
     * @param   rootSchema     The Schema instance, and root subschema, through
2513
     *                         which other subschemas can be created and modified
2514
     * @param   rootNode       Reference to the node from which JSON References
2515
     *                         will be resolved when they refer to the current
2516
     *                         document; used for recursive parsing of schemas
2517
     * @param   node           Node containing the name of a JSON type
2518
     * @param   currentScope   URI for current resolution scope
2519
     * @param   nodePath       JSON Pointer representing path to current node
2520
     * @param   fetchDoc       Function to fetch remote JSON documents (optional)
2521
     * @param   docCache       Cache of resolved and fetched remote documents
2522
     * @param   schemaRegistry Registry of populated schemas
2523
     *
2524
     * @return  pointer to a new TypeConstraint object.
2525
     */
2526
    template<typename AdapterType>
2527
    constraints::TypeConstraint makeTypeConstraint(
2528
        Schema &rootSchema,
2529
        const AdapterType &rootNode,
2530
        const AdapterType &node,
2531
        const std::optional<std::string> currentScope,
2532
        const std::string &nodePath,
2533
        const typename FunctionPtrs<AdapterType>::FetchDoc fetchDoc,
2534
        typename DocumentCache<AdapterType>::Type &docCache,
2535
        SchemaRegistry &schemaRegistry)
2536
27.3k
    {
2537
27.3k
        typedef constraints::TypeConstraint TypeConstraint;
2538
2539
27.3k
        TypeConstraint constraint;
2540
2541
27.3k
        if (node.maybeString()) {
2542
25.2k
            const TypeConstraint::JsonType type = TypeConstraint::jsonTypeFromString(node.getString());
2543
25.2k
            if (type == TypeConstraint::kAny && m_version == kDraft4) {
2544
877
                throwRuntimeError("'any' type is not supported in version 4 schemas.");
2545
877
            }
2546
2547
25.2k
            constraint.addNamedType(type);
2548
2549
25.2k
        } else if (node.maybeArray()) {
2550
2.03k
            int index = 0;
2551
2.69k
            for (const AdapterType v : node.getArray()) {
2552
2.69k
                if (v.maybeString()) {
2553
2.53k
                    const TypeConstraint::JsonType type = TypeConstraint::jsonTypeFromString(v.getString());
2554
2.53k
                    if (type == TypeConstraint::kAny && m_version == kDraft4) {
2555
262
                        throwRuntimeError("'any' type is not supported in version 4 schemas.");
2556
262
                    }
2557
2558
2.53k
                    constraint.addNamedType(type);
2559
2560
2.53k
                } else if (v.maybeObject() && m_version == kDraft3) {
2561
0
                    const std::string childPath = nodePath + "/" + std::to_string(index);
2562
0
                    const Subschema *subschema = makeOrReuseSchema<AdapterType>(rootSchema, rootNode, v, currentScope,
2563
0
                            childPath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
2564
0
                    constraint.addSchemaType(subschema);
2565
2566
154
                } else {
2567
154
                    throwRuntimeError("Type name should be a string.");
2568
154
                }
2569
2570
2.69k
                index++;
2571
2.69k
            }
2572
2573
2.03k
        } else if (node.maybeObject() && m_version == kDraft3) {
2574
0
            const Subschema *subschema = makeOrReuseSchema<AdapterType>(rootSchema, rootNode, node, currentScope,
2575
0
                    nodePath, fetchDoc, nullptr, nullptr, docCache, schemaRegistry);
2576
0
            constraint.addSchemaType(subschema);
2577
2578
86
        } else {
2579
86
            throwRuntimeError("Type name should be a string.");
2580
86
        }
2581
2582
27.3k
        return constraint;
2583
27.3k
    }
2584
2585
    /**
2586
     * @brief   Make a new UniqueItemsConstraint object.
2587
     *
2588
     * @param   node  Node containing a boolean value.
2589
     *
2590
     * @return  pointer to a new UniqueItemsConstraint object that belongs to
2591
     *          the caller, or nullptr if the boolean value is false.
2592
     */
2593
    template<typename AdapterType>
2594
    std::optional<constraints::UniqueItemsConstraint> makeUniqueItemsConstraint(const AdapterType &node)
2595
1.69k
    {
2596
1.69k
        if (node.isBool() || node.maybeBool()) {
2597
            // If the boolean value is true, this function will return a pointer
2598
            // to a new UniqueItemsConstraint object. If it is value, then the
2599
            // constraint is redundant, so nullptr is returned instead.
2600
1.56k
            if (node.asBool()) {
2601
1.54k
                return constraints::UniqueItemsConstraint();
2602
1.54k
            } else {
2603
21
                return std::optional<constraints::UniqueItemsConstraint>();
2604
21
            }
2605
1.56k
        }
2606
2607
122
        throwRuntimeError("Expected boolean value for 'uniqueItems' constraint.");
2608
122
    }
2609
2610
private:
2611
2612
    /// Version of JSON Schema that should be expected when parsing
2613
    Version m_version;
2614
};
2615
2616
}  // namespace valijson