Skip to content

Commit 250db0c

Browse files
Merge pull request patefacio#9 from michaelcarter-wf/nested_ref_fetch
EPL-7782: Allow Async Fetch of Refs Within Keywords + Resolve Fragments
2 parents 220b314 + 62e4e03 commit 250db0c

File tree

9 files changed

+240
-49
lines changed

9 files changed

+240
-49
lines changed

lib/src/json_schema/browser/platform_functions.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@ import 'dart:async';
4040

4141
import 'package:w_transport/w_transport.dart';
4242
import 'package:json_schema/src/json_schema/json_schema.dart';
43+
import 'package:json_schema/src/json_schema/utils.dart';
4344

4445
Future<JsonSchema> createSchemaFromUrlBrowser(String schemaUrl) async {
4546
final uri = Uri.parse(schemaUrl);
4647
if (uri.scheme != 'file') {
4748
// _logger.info('Getting url $uri'); TODO: re-add logger.
4849
final response = await (new JsonRequest()..uri = uri).get();
49-
return JsonSchema.createSchema(response.body.asJson());
50+
// HTTP servers ignore fragments, so resolve a sub-map if a fragment was specified.
51+
final Map schemaMap = JsonSchemaUtils.getSubMapFromFragment(response.body.asJson(), uri);
52+
return JsonSchema.createSchema(schemaMap);
5053
} else {
5154
throw new FormatException('Url schema must be http: $schemaUrl. To use a local file, use dart:io');
5255
}

lib/src/json_schema/json_schema.dart

Lines changed: 130 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ class JsonSchema {
6767
/// Create a schema from a [Map].
6868
///
6969
/// This method is asyncronous to support automatic fetching of sub-[JsonSchema]s for items,
70-
/// properties, and sub-properties of the root schema.
71-
///
70+
/// properties, and sub-properties of the root schema.
71+
///
7272
/// TODO: If you want to create a [JsonSchema],
7373
/// first ensure you have fetched all sub-schemas out of band, and use [createSchemaWithProvidedRefs]
7474
/// instead.
@@ -77,9 +77,9 @@ class JsonSchema {
7777
static Future<JsonSchema> createSchema(Map data) => new JsonSchema._fromRootMap(data)._thisCompleter.future;
7878

7979
/// Create a schema from a URL.
80-
///
80+
///
8181
/// This method is asyncronous to support automatic fetching of sub-[JsonSchema]s for items,
82-
/// properties, and sub-properties of the root schema.
82+
/// properties, and sub-properties of the root schema.
8383
static Future<JsonSchema> createSchemaFromUrl(String schemaUrl) {
8484
if (globalCreateJsonSchemaFromUrl == null) {
8585
throw new StateError('no globalCreateJsonSchemaFromUrl defined!');
@@ -139,7 +139,6 @@ class JsonSchema {
139139
}
140140

141141
Future<JsonSchema> _validateAllPathsAsync() {
142-
// Check all _schemaAssignments for
143142
if (_root == this) {
144143
_schemaAssignments.forEach((assignment) => assignment());
145144
if (_retrievalRequests.isNotEmpty) {
@@ -156,7 +155,9 @@ class JsonSchema {
156155
Future<JsonSchema> _validateSchemaAsync() {
157156
// _logger.info('Validating schema $_path'); TODO: re-add logger
158157

159-
if (_registerSchemaRef(_path, _schemaMap)) {
158+
_registerSchemaRef(_path, _schemaMap);
159+
160+
if (_isRemoteRef(_path, _schemaMap)) {
160161
// _logger.info('Top level schema is ref: $_schemaRefs'); TODO: re-add logger
161162
}
162163

@@ -184,55 +185,136 @@ class JsonSchema {
184185
return new JsonSchema._fromMap(_root, schemaDefinition, path);
185186
}
186187

187-
// Root Schema Properties
188+
// --------------------------------------------------------------------------
189+
// Root Schema Fields
190+
// --------------------------------------------------------------------------
191+
192+
/// The root [JsonSchema] for this [JsonSchema].
188193
JsonSchema _root;
194+
195+
/// JSON of the [JsonSchema] as a [Map].
189196
Map<String, dynamic> _schemaMap = {};
197+
198+
/// A [List<JsonSchema>] which the value must conform to all of.
190199
List<JsonSchema> _allOf = [];
200+
201+
/// A [List<JsonSchema>] which the value must conform to at least one of.
191202
List<JsonSchema> _anyOf = [];
203+
204+
/// Default value of the [JsonSchema].
192205
dynamic _defaultValue;
206+
207+
/// Included [JsonSchema] definitions.
193208
Map<String, JsonSchema> _definitions = {};
209+
210+
/// Description of the [JsonSchema].
194211
String _description;
212+
213+
/// Possible values of the [JsonSchema].
195214
List _enumValues = [];
215+
216+
/// Whether the maximum of the [JsonSchema] is exclusive.
196217
bool _exclusiveMaximum;
218+
219+
/// Whether the minumum of the [JsonSchema] is exclusive.
197220
bool _exclusiveMinimum;
198221

199-
/// Support for optional formats (date-time, uri, email, ipv6, hostname)
222+
/// Pre-defined format (i.e. date-time, email, etc) of the [JsonSchema] value.
200223
String _format;
224+
225+
/// ID of the [JsonSchema].
201226
Uri _id;
227+
228+
/// Maximum value of the [JsonSchema] value.
202229
num _maximum;
230+
231+
/// Minimum value of the [JsonSchema] value.
203232
num _minimum;
233+
234+
/// Maximum value of the [JsonSchema] value.
204235
int _maxLength;
236+
237+
/// Minimum length of the [JsonSchema] value.
205238
int _minLength;
239+
240+
/// The number which the value of the [JsonSchema] must be a multiple of.
206241
num _multipleOf;
242+
243+
/// A [JsonSchema] which the value must NOT be.
207244
JsonSchema _notSchema;
245+
246+
/// A [List<JsonSchema>] which the value must conform to at least one of.
208247
List<JsonSchema> _oneOf = [];
248+
249+
/// The regular expression the [JsonSchema] value must conform to.
209250
RegExp _pattern;
251+
252+
/// Ref to the URI of the [JsonSchema].
210253
String _ref;
254+
255+
/// The path of the [JsonSchema] within the root [JsonSchema].
211256
String _path;
257+
258+
/// Title of the [JsonSchema].
212259
String _title;
260+
261+
/// List of allowable types for the [JsonSchema].
213262
List<SchemaType> _schemaTypeList;
214263

264+
// --------------------------------------------------------------------------
215265
// Schema List Item Related Fields
266+
// --------------------------------------------------------------------------
267+
268+
/// [JsonSchema] definition used to validate items of this schema.
216269
JsonSchema _items;
270+
271+
/// List of [JsonSchema] used to validate items of this schema.
217272
List<JsonSchema> _itemsList;
218-
dynamic _additionalItems;
273+
274+
/// Whether additional items are allowed or the [JsonSchema] they should conform to.
275+
/* union bool | Map */ dynamic _additionalItems;
276+
277+
/// Maimum number of items allowed.
219278
int _maxItems;
279+
280+
/// Maimum number of items allowed.
220281
int _minItems;
282+
283+
/// Whether the items in the list must be unique.
221284
bool _uniqueItems = false;
222285

286+
// --------------------------------------------------------------------------
223287
// Schema Sub-Property Related Fields
288+
// --------------------------------------------------------------------------
289+
290+
/// Map of [JsonSchema]s by property key.
224291
Map<String, JsonSchema> _properties = {};
292+
293+
/// Whether additional properties, other than those specified, are allowed.
225294
bool _additionalProperties;
295+
296+
/// [JsonSchema] that additional properties must conform to.
226297
JsonSchema _additionalPropertiesSchema;
298+
227299
Map<String, List<String>> _propertyDependencies = {};
300+
228301
Map<String, JsonSchema> _schemaDependencies = {};
302+
303+
/// The maximum number of properties allowed.
229304
int _maxProperties;
305+
306+
/// The minimum number of properties allowed.
230307
int _minProperties = 0;
308+
309+
/// Map of [JsonSchema]s for properties, based on [RegExp]s keys.
231310
Map<RegExp, JsonSchema> _patternProperties = {};
311+
232312
Map<String, JsonSchema> _refMap = {};
233313
List<String> _requiredProperties;
234314

235-
/// Implementation-specific properties:
315+
// --------------------------------------------------------------------------
316+
// Implementation Specific Feilds
317+
// --------------------------------------------------------------------------
236318

237319
/// Maps any unsupported top level property to its original value
238320
Map<String, dynamic> _freeFormMap = {};
@@ -246,7 +328,7 @@ class JsonSchema {
246328
/// Assignments to call for resolution upon end of parse.
247329
List _schemaAssignments = [];
248330

249-
/// For schemas with $ref maps path of schema to $ref path
331+
/// For schemas with $ref maps, path of schema to $ref path
250332
Map<String, String> _schemaRefs = {};
251333

252334
/// Completer that fires when [this] [JsonSchema] has finished building.
@@ -294,12 +376,7 @@ class JsonSchema {
294376
};
295377

296378
/// Get a nested [JsonSchema] from a path.
297-
JsonSchema resolvePath(String path) {
298-
while (_schemaRefs.containsKey(path)) {
299-
path = _schemaRefs[path];
300-
}
301-
return _refMap[path];
302-
}
379+
JsonSchema resolvePath(String path) => _resolvePath(path);
303380

304381
@override
305382
String toString() => '${_schemaMap}';
@@ -377,7 +454,7 @@ class JsonSchema {
377454
/// Title of the [JsonSchema].
378455
String get title => _title;
379456

380-
/// TODO
457+
/// List of allowable types for the [JsonSchema].
381458
List<SchemaType> get schemaTypeList => _schemaTypeList;
382459

383460
// --------------------------------------------------------------------------
@@ -390,7 +467,7 @@ class JsonSchema {
390467
/// Ordered list of [JsonSchema] which the value of the same index must conform to.
391468
List<JsonSchema> get itemsList => _itemsList;
392469

393-
/// Whether additional items are allowed.
470+
/// Whether additional items are allowed or the [JsonSchema] they should conform to.
394471
/* union bool | Map */ dynamic get additionalItems => _additionalItems;
395472

396473
/// The maximum number of items allowed.
@@ -475,28 +552,46 @@ class JsonSchema {
475552
// JSON Schema Internal Operations
476553
// --------------------------------------------------------------------------
477554

478-
bool _registerSchemaRef(String path, dynamic schemaDefinition) {
479-
if (schemaDefinition is Map) {
480-
final dynamic ref = schemaDefinition[r'$ref'];
481-
if (ref != null) {
482-
if (ref is String) {
483-
// _logger.info('Linking $path to $ref'); TODO: re-add logger
484-
_schemaRefs[path] = ref;
485-
return true;
486-
} else {
487-
throw FormatExceptions.string('\$ref', ref);
488-
}
489-
}
555+
/// Function to determine whether a given [schemaDefinition] is a remote $ref.
556+
bool _isRemoteRef(String path, dynamic schemaDefinition) {
557+
final Map schemaDefinitionMap = TypeValidators.object(path, schemaDefinition);
558+
final dynamic ref = schemaDefinitionMap[r'$ref'];
559+
if (ref != null) {
560+
TypeValidators.nonEmptyString(r'$ref', ref);
561+
// If the ref begins with "#" it is a local ref, so we return false.
562+
if (ref[0] != '#') return false;
563+
return true;
490564
}
491565
return false;
492566
}
493567

568+
/// Checks if a [schemaDefinition] has a $ref.
569+
/// If it does, it adds the $ref to [_shemaRefs] at the path key and returns true.
570+
void _registerSchemaRef(String path, dynamic schemaDefinition) {
571+
final Map schemaDefinitionMap = TypeValidators.object(path, schemaDefinition);
572+
final dynamic ref = schemaDefinitionMap[r'$ref'];
573+
if (_isRemoteRef(path, schemaDefinition)) {
574+
// _logger.info('Linking $path to $ref'); TODO: re-add logger
575+
_schemaRefs[path] = ref;
576+
}
577+
}
578+
494579
/// Add a ref'd JsonSchema to the map of available Schemas.
495580
_addSchema(String path, JsonSchema schema) => _refMap[path] = schema;
496581

582+
// Create a [JsonSchema] from a sub-schema of the root.
497583
_makeSchema(String path, dynamic schema, SchemaAssigner assigner) {
498584
if (schema is! Map) throw FormatExceptions.schema(path, schema);
499-
if (_registerSchemaRef(path, schema)) {
585+
586+
_registerSchemaRef(path, schema);
587+
588+
final isRemoteReference = _isRemoteRef(path, schema);
589+
final isPathLocal = path[0] == '#';
590+
591+
/// If this sub-schema is a ref within the root schema,
592+
/// add it to the map of local schema assignments.
593+
/// Otherwise, call the assigner function and create a new [JsonSchema].
594+
if (isRemoteReference && isPathLocal) {
500595
_schemaAssignments.add(() => assigner(_resolvePath(path)));
501596
} else {
502597
assigner(_createSubSchema(schema, path));
@@ -578,7 +673,9 @@ class JsonSchema {
578673
_ref = TypeValidators.nonEmptyString(r'$ref', value);
579674
if (_ref[0] != '#') {
580675
final refSchemaFuture = createSchemaFromUrl(_ref).then((schema) => _addSchema(_ref, schema));
581-
_retrievalRequests.add(refSchemaFuture);
676+
677+
/// Always add sub-schema retrieval requests to the [_root], as this is where the promise resolves.
678+
_root._retrievalRequests.add(refSchemaFuture);
582679
}
583680
}
584681

lib/src/json_schema/utils.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ class JsonSchemaUtils {
6363
}
6464

6565
static String normalizePath(String path) => path.replaceAll('~', '~0').replaceAll('/', '~1').replaceAll('%', '%25');
66+
67+
static Map getSubMapFromFragment(Map schemaMap, Uri uri) {
68+
if (uri.fragment?.isNotEmpty == true) {
69+
final List<String> pathSegments = uri.fragment.split('/');
70+
for (final segment in pathSegments) {
71+
if (segment.isNotEmpty) {
72+
if (schemaMap[segment] is Map) {
73+
schemaMap = schemaMap[segment];
74+
} else {
75+
throw new FormatException('Invalid fragment: ${uri.fragment} at ${segment}');
76+
}
77+
}
78+
}
79+
}
80+
return schemaMap;
81+
}
6682
}
6783

6884
class DefaultValidators {

lib/src/json_schema/validator.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,12 @@ class Validator {
384384
}
385385

386386
void _validate(JsonSchema schema, dynamic instance) {
387+
/// If the [JsonSchema] being validated is a ref, pull the ref
388+
/// from the [refMap] instead.
389+
if (schema.ref != null) {
390+
final String path = schema.root.endPath(schema.ref);
391+
schema = schema.root.refMap[path];
392+
}
387393
_typeValidation(schema, instance);
388394
_enumValidation(schema, instance);
389395
if (instance is List) _itemsValidation(schema, instance);

lib/src/json_schema/vm/platform_functions.dart

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,27 @@ import 'dart:async';
4141
import 'dart:convert' as convert;
4242

4343
import 'package:json_schema/src/json_schema/json_schema.dart';
44+
import 'package:json_schema/src/json_schema/utils.dart';
4445

45-
Future<JsonSchema> createSchemaFromUrlVm(String schemaUrl) {
46+
Future<JsonSchema> createSchemaFromUrlVm(String schemaUrl) async {
4647
final uri = Uri.parse(schemaUrl);
48+
Map schemaMap;
4749
if (uri.scheme == 'http') {
48-
return new HttpClient().getUrl(uri).then((HttpClientRequest request) {
49-
request.followRedirects = true;
50-
return request.close();
51-
}).then((HttpClientResponse response) {
52-
return response.transform(new convert.Utf8Decoder()).join().then((schemaText) {
53-
final map = convert.JSON.decode(schemaText);
54-
return JsonSchema.createSchema(map);
55-
});
56-
});
50+
// Setup the HTTP request.
51+
final httpRequest = await new HttpClient().getUrl(uri);
52+
httpRequest.followRedirects = true;
53+
// Fetch the response
54+
final response = await httpRequest.close();
55+
// Convert the response into a string
56+
final schemaText = await response.transform(new convert.Utf8Decoder()).join();
57+
schemaMap = convert.JSON.decode(schemaText);
5758
} else if (uri.scheme == 'file' || uri.scheme == '') {
58-
return new File(uri.scheme == 'file' ? uri.toFilePath() : schemaUrl)
59-
.readAsString()
60-
.then((text) => JsonSchema.createSchema(convert.JSON.decode(text)));
59+
final fileString = await new File(uri.scheme == 'file' ? uri.toFilePath() : schemaUrl).readAsString();
60+
schemaMap = convert.JSON.decode(fileString);
6161
} else {
62-
throw new FormatException('Url schemd must be http, file, or empty: $schemaUrl');
62+
throw new FormatException('Url schema must be http, file, or empty: $schemaUrl');
6363
}
64+
// HTTP servers / file systems ignore fragments, so resolve a sub-map if a fragment was specified.
65+
schemaMap = JsonSchemaUtils.getSubMapFromFragment(schemaMap, uri);
66+
return await JsonSchema.createSchema(schemaMap);
6467
}

test/additional_remotes/bar.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"baz": { "$ref": "http://localhost:4321/string.json#" }
3+
}

0 commit comments

Comments
 (0)