Skip to content

Commit 71d69cd

Browse files
committed
Merge branch 'feature/custom-formats' into develop
2 parents cb0bdd1 + 8fb6d89 commit 71d69cd

File tree

15 files changed

+229
-100
lines changed

15 files changed

+229
-100
lines changed

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ A [JSON Schema](http://json-schema.org/documentation.html) validator for Node.js
55
* Complete validation coverage of JSON Schema Draft v4.
66
* Optional dynamic loader for referenced schemas (load schemas from a database or the web)
77
* Useful error messages.
8+
* **NEW:** Supports custom validators for the `format` keword.
89

910
## Install
1011

@@ -53,6 +54,27 @@ js.validate(instance, schema, function(errs) {
5354
});
5455
```
5556

57+
### Custom format validators
58+
59+
Create a custom validator for the JSON Schema `format` keyword:
60+
61+
```js
62+
var JaySchema = require('jayschema');
63+
var js = new JaySchema();
64+
65+
js.addFormat('phone-us', function(value) {
66+
var PHONE_US_REGEXP = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/;
67+
if (PHONE_US_REGEXP.test(value)) { return null; }
68+
return 'must be a US phone number';
69+
});
70+
71+
var instance = '212-555-';
72+
var schema = { "type": "string", "format": "phone-us" };
73+
74+
console.log(js.validate(instance, schema));
75+
// fails with error description: must be a US phone number
76+
````
77+
5678
## Why JSON Schema?
5779

5880
* Validate JSON server-side:
@@ -79,7 +101,7 @@ function loader(ref, callback) {
79101
// [ load your schema! ]
80102
if (errorOccurred) {
81103
callback(err);
82-
} else {
104+
} else {
83105
callback(null, schema);
84106
}
85107
}
@@ -116,6 +138,12 @@ Return boolean indicating whether the specified schema id has previously been re
116138
117139
See [Schema loading](#schema-loading).
118140
141+
### JaySchema.prototype.addFormat(formatName, handler)
142+
143+
Add a custom handler for the `format` keyword. Whenever a schema uses the `format` keyword, with the given by `formatName`, your handler will be called.
144+
145+
The handler receives the value to be validated, and returns `null` if the value is valid, or a `string` (description of the error) if the value is not valid.
146+
119147
### Loaders
120148
121149
A loader can be passed to the constructor, or you can set the `loader` property at any time. You can define your own loader. **JaySchema** also includes one built-in loader for your convenience:
@@ -153,7 +181,7 @@ Pass a `loader` callback to the `JaySchema` constructor. When an external schema
153181
This works with synchronous or async code.
154182
155183
1. First, `register()` the main schemas you plan to use.
156-
2. Next, call `getMissingSchemas`, which returns an array of externally-referenced schemas.
184+
2. Next, call `getMissingSchemas`, which returns an array of externally-referenced schemas.
157185
3. Retrieve and `register()` each missing schema.
158186
4. Repeat from step 2 until there are no more missing schemas.
159187
@@ -169,7 +197,7 @@ If, instead, you want the list of *all* missing schemas referenced by all regist
169197
170198
## Format specifiers
171199
172-
**JaySchema** supports the following values for the optional `format` keyword:
200+
For the `format` keyword, **JaySchema** allows you to add custom validation functions (see [`addFormat`](#jayschemaprototypeaddformatformatname-handler)). In addition, the following built-in formats are supported:
173201
174202
* `date-time`: Must match the `date-time` specification given in [RFC 3339, Section 5.6](https://tools.ietf.org/html/rfc3339#section-5.6). This expects *both* a date and a time. For date-only validation or time-only validation, JaySchema supports the older draft v3 `date` and `time` formats.
175203
* `hostname`: Must match the “Preferred name syntax” given in [RFC 1034, Section 3.5](https://tools.ietf.org/html/rfc1034#section-3.5), with the exception that hostnames are permitted to begin with a digit, as per [RFC 1123 Section 2.1](http://tools.ietf.org/html/rfc1123#section-2.1).

Release Notes.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
#0.3.0
2+
3+
* **FEATURE:** [Custom `format` validators](https://github.com/natesilva/jayschema#jayschemaprototypeaddformatformatname-handler) are now supported. The syntax is compatible with that of [tv4](https://github.com/geraintluff/tv4#addformatformat-validationfunction). Thanks to [alexkwolfe](https://github.com/alexkwolfe) for the suggestion.
4+
15
# 0.2.8
26

3-
* **BUGFIX:** `enum` properties with `null` values now work. Thanks to [alexkwolfe](https://github.com/alexkwolfe).
7+
* **BUGFIX**: `enum` properties with `null` values now work. Thanks to [alexkwolfe](https://github.com/alexkwolfe).
48

59
# 0.2.7
610

7-
* **BUGFIX:** Handle falsy values correctly in `ValidationError`. Thanks to [larose](https://github.com/larose).
11+
* **BUGFIX**: Handle falsy values correctly in `ValidationError`. Thanks to [larose](https://github.com/larose).
812
* Other code cleanups. Thanks again to [larose](https://github.com/larose).
913

1014
# 0.2.6

lib/jayschema.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var JaySchema = function(loader) {
3030

3131
// internal
3232
this._schemaRegistry = new SchemaRegistry();
33+
this._customFormatHandlers = {};
3334

3435
// _refsRequested is an object where the key is the normalized ID
3536
// of the schema ref that was requested, and the value is a
@@ -73,6 +74,33 @@ JaySchema.prototype.register = function() {
7374
return this._schemaRegistry.register.apply(this._schemaRegistry, arguments);
7475
};
7576

77+
// ******************************************************************
78+
// Register a handler for the format keyword. Handlers have the
79+
// following signature:
80+
//
81+
// function handler(value) -> returns null (if value is valid) or
82+
// error description string (if value is
83+
// not valid)
84+
// ******************************************************************
85+
JaySchema.prototype.addFormat = function(attributeName, handler) {
86+
// Wrap the format handler to make its signature match that of the
87+
// internal handlers.
88+
var handlerWrapper = function(config) {
89+
var errors = [];
90+
var result = handler(config.inst, config.schema);
91+
if (typeof result === 'string') {
92+
errors.push(new Errors.FormatValidationError(config.resolutionScope,
93+
config.instanceContext, 'format', attributeName, config.inst, result));
94+
} else if (result !== null) {
95+
var desc = 'failed custom format validation';
96+
errors.push(new Errors.FormatValidationError(config.resolutionScope,
97+
config.instanceContext, 'format', attributeName, config.inst, desc));
98+
}
99+
return errors;
100+
};
101+
this._customFormatHandlers[attributeName] = handlerWrapper;
102+
};
103+
76104
// ******************************************************************
77105
// [static] Return a hash for an object. We rely on JSON.stringify
78106
// to always return the same value for a given object. (If it
@@ -169,11 +197,20 @@ JaySchema.prototype._validateImpl = function(instance, schema, resolutionScope,
169197

170198
// run the tests
171199
var config = {
200+
clone: function() {
201+
var result = {};
202+
var keys = Object.keys(this);
203+
for (var index = 0, len = keys.length; index !== len; ++index) {
204+
result[keys[index]] = this[keys[index]];
205+
}
206+
return result;
207+
},
172208
inst: instance,
173209
schema: schema,
174210
resolutionScope: resolutionScope,
175211
instanceContext: instanceContext || '#',
176-
schemaRegistry: this._schemaRegistry
212+
schemaRegistry: this._schemaRegistry,
213+
customFormatHandlers: this._customFormatHandlers
177214
};
178215

179216
var testRunner = testRunners[schema.$schema || DEFAULT_SCHEMA_VERSION];

lib/suites/draft-04/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ function getApplicableTests(config) {
5151
}
5252
}
5353

54-
// for objects, the properties, patternProperties, and
55-
// additionalProperties validations are inseperable
54+
// for objects: the properties, patternProperties, and
55+
// additionalProperties validations are inseparable
5656
if (result.indexOf('properties') !== -1 ||
5757
result.indexOf('patternProperties') !== -1 ||
5858
result.indexOf('additionalProperties') !== -1)
@@ -73,6 +73,7 @@ function getApplicableTests(config) {
7373
// resolutionScope: resolutionScope,
7474
// instanceContext: current position within the overall instance
7575
// schemaRegistry: a SchemaRegistry
76+
// customFormatHandlers: custom handlers for the "format" keyword
7677
//
7778
// ******************************************************************
7879
function run(config)

lib/suites/draft-04/keywords/_propertiesImpl.js

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,25 @@ module.exports = function(config) {
3535
var applyAdditionalProperties = true;
3636

3737
if (Object.prototype.hasOwnProperty.call(p, m)) {
38-
subTestConfig = {
39-
inst: config.inst[m],
40-
schema: p[m],
41-
resolutionScope: config.resolutionScope + '/properties/' + m,
42-
instanceContext: context,
43-
schemaRegistry: config.schemaRegistry
44-
};
38+
subTestConfig = config.clone();
39+
subTestConfig.inst = config.inst[m];
40+
subTestConfig.schema = p[m];
41+
subTestConfig.resolutionScope =
42+
config.resolutionScope + '/properties/' + m;
43+
subTestConfig.instanceContext = context;
4544
errors = errors.concat(testRunner(subTestConfig));
4645
applyAdditionalProperties = false;
4746
}
4847

4948
for (var y = 0; y < pp.length; ++y) {
5049
var rx = pp[y][0];
5150
if (m.match(rx)) {
52-
subTestConfig = {
53-
inst: config.inst[m],
54-
schema: pp[y][1],
55-
resolutionScope: config.resolutionScope + '/patternProperties/' + m,
56-
instanceContext: context,
57-
schemaRegistry: config.schemaRegistry
58-
};
51+
subTestConfig = config.clone();
52+
subTestConfig.inst = config.inst[m];
53+
subTestConfig.schema = pp[y][1];
54+
subTestConfig.resolutionScope =
55+
config.resolutionScope + '/patternProperties/' + m;
56+
subTestConfig.instanceContext = context;
5957
errors = errors.concat(testRunner(subTestConfig));
6058
applyAdditionalProperties = false;
6159
}
@@ -68,13 +66,12 @@ module.exports = function(config) {
6866
errors.push(new Errors.ObjectValidationError(config.resolutionScope,
6967
config.instanceContext, 'additionalProperties', undefined, m, desc));
7068
} else if (additionalProperties !== true) {
71-
subTestConfig = {
72-
inst: config.inst[m],
73-
schema: additionalProperties,
74-
resolutionScope: config.resolutionScope + '/additionalProperties',
75-
instanceContext: context,
76-
schemaRegistry: config.schemaRegistry
77-
};
69+
subTestConfig = config.clone();
70+
subTestConfig.inst = config.inst[m];
71+
subTestConfig.schema = additionalProperties;
72+
subTestConfig.resolutionScope =
73+
config.resolutionScope + '/additionalProperties';
74+
subTestConfig.instanceContext = context;
7875
errors = errors.concat(testRunner(subTestConfig));
7976
}
8077
}

lib/suites/draft-04/keywords/allOf.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,9 @@ module.exports = function(config) {
1010
for (var index = 0; index < config.schema.allOf.length; ++index) {
1111
var schema = config.schema.allOf[index];
1212

13-
var subTestConfig = {
14-
inst: config.inst,
15-
schema: schema,
16-
resolutionScope: config.resolutionScope + '/allOf/' + index,
17-
instanceContext: config.instanceContext,
18-
schemaRegistry: config.schemaRegistry
19-
};
13+
var subTestConfig = config.clone();
14+
subTestConfig.schema = schema;
15+
subTestConfig.resolutionScope = config.resolutionScope + '/allOf/' + index;
2016

2117
errors = errors.concat(testRunner(subTestConfig));
2218
}

lib/suites/draft-04/keywords/anyOf.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,9 @@ module.exports = function(config) {
1313

1414
for (var index = 0, len = config.schema.anyOf.length; index !== len; ++index)
1515
{
16-
var subTestConfig = {
17-
inst: config.inst,
18-
schema: config.schema.anyOf[index],
19-
resolutionScope: config.resolutionScope + '/anyOf/' + index,
20-
instanceContext: config.instanceContext,
21-
schemaRegistry: config.schemaRegistry
22-
};
16+
var subTestConfig = config.clone();
17+
subTestConfig.schema = config.schema.anyOf[index];
18+
subTestConfig.resolutionScope = config.resolutionScope + '/anyOf/' + index;
2319

2420
var nestedErrors = testRunner(subTestConfig);
2521

lib/suites/draft-04/keywords/dependencies.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,11 @@ module.exports = function(config) {
4242
} else {
4343
// schema dependency: validates the *instance*, not the value
4444
// associated with the property name.
45-
var subTestConfig = {
46-
inst: config.inst,
47-
schema: dep,
48-
resolutionScope: config.resolutionScope + '/dependencies/' + index,
49-
instanceContext: config.instanceContext + '/' + key,
50-
schemaRegistry: config.schemaRegistry
51-
};
45+
var subTestConfig = config.clone();
46+
subTestConfig.schema = dep;
47+
subTestConfig.resolutionScope =
48+
config.resolutionScope + '/dependencies/' + index;
49+
subTestConfig.instanceContext = config.instanceContext + '/' + key;
5250
errors = errors.concat(testRunner(subTestConfig));
5351
}
5452
}

lib/suites/draft-04/keywords/format.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ var SUPPORTED_FORMATS = {
1616
module.exports = function(config) {
1717
var errors = [];
1818

19-
if (SUPPORTED_FORMATS.hasOwnProperty(config.schema.format)) {
19+
if (config.customFormatHandlers.hasOwnProperty(config.schema.format)) {
20+
errors =
21+
errors.concat(config.customFormatHandlers[config.schema.format](config));
22+
} else if (SUPPORTED_FORMATS.hasOwnProperty(config.schema.format)) {
2023
errors = errors.concat(SUPPORTED_FORMATS[config.schema.format](config));
2124
}
2225

lib/suites/draft-04/keywords/items.js

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ module.exports = function(config) {
1616
var item = config.inst[index];
1717
var itemSchema = config.schema.items[index];
1818

19-
subTestConfig = {
20-
inst: item,
21-
schema: itemSchema,
22-
resolutionScope: config.resolutionScope + '/items/' + index,
23-
instanceContext: config.instanceContext + '/' + index,
24-
schemaRegistry: config.schemaRegistry
25-
};
19+
subTestConfig = config.clone();
20+
subTestConfig.inst = item;
21+
subTestConfig.schema = itemSchema;
22+
subTestConfig.resolutionScope =
23+
config.resolutionScope + '/items/' + index;
24+
subTestConfig.instanceContext = config.instanceContext + '/' + index;
2625

2726
errors = errors.concat(testRunner(subTestConfig));
2827
}
@@ -39,13 +38,12 @@ module.exports = function(config) {
3938
index < config.inst.length;
4039
++index)
4140
{
42-
subTestConfig = {
43-
inst: config.inst[index],
44-
schema: config.schema.additionalItems,
45-
resolutionScope: config.resolutionScope + '/items/' + index,
46-
instanceContext: config.instanceContext + '/' + index,
47-
schemaRegistry: config.schemaRegistry
48-
};
41+
subTestConfig = config.clone();
42+
subTestConfig.inst = config.inst[index];
43+
subTestConfig.schema = config.schema.additionalItems;
44+
subTestConfig.resolutionScope =
45+
config.resolutionScope + '/items/' + index;
46+
subTestConfig.instanceContext = config.instanceContext + '/' + index;
4947

5048
errors = errors.concat(testRunner(subTestConfig));
5149
}
@@ -55,13 +53,11 @@ module.exports = function(config) {
5553
} else {
5654
// one schema for all items in the array
5755
for (index = 0; index < config.inst.length; ++index) {
58-
subTestConfig = {
59-
inst: config.inst[index],
60-
schema: config.schema.items,
61-
resolutionScope: config.resolutionScope + '/items',
62-
instanceContext: config.instanceContext + '/' + index,
63-
schemaRegistry: config.schemaRegistry
64-
};
56+
subTestConfig = config.clone();
57+
subTestConfig.inst = config.inst[index];
58+
subTestConfig.schema = config.schema.items;
59+
subTestConfig.resolutionScope = config.resolutionScope + '/items';
60+
subTestConfig.instanceContext = config.instanceContext + '/' + index;
6561

6662
errors = errors.concat(testRunner(subTestConfig));
6763
}

0 commit comments

Comments
 (0)