validator.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. 'use strict';
  2. var attribute = require('./attribute');
  3. var helpers = require('./helpers');
  4. var scanSchema = require('./scan').scan;
  5. var ValidatorResult = helpers.ValidatorResult;
  6. var ValidatorResultError = helpers.ValidatorResultError;
  7. var SchemaError = helpers.SchemaError;
  8. var SchemaContext = helpers.SchemaContext;
  9. //var anonymousBase = 'vnd.jsonschema:///';
  10. var anonymousBase = '/';
  11. /**
  12. * Creates a new Validator object
  13. * @name Validator
  14. * @constructor
  15. */
  16. var Validator = function Validator () {
  17. // Allow a validator instance to override global custom formats or to have their
  18. // own custom formats.
  19. this.customFormats = Object.create(Validator.prototype.customFormats);
  20. this.schemas = {};
  21. this.unresolvedRefs = [];
  22. // Use Object.create to make this extensible without Validator instances stepping on each other's toes.
  23. this.types = Object.create(types);
  24. this.attributes = Object.create(attribute.validators);
  25. };
  26. // Allow formats to be registered globally.
  27. Validator.prototype.customFormats = {};
  28. // Hint at the presence of a property
  29. Validator.prototype.schemas = null;
  30. Validator.prototype.types = null;
  31. Validator.prototype.attributes = null;
  32. Validator.prototype.unresolvedRefs = null;
  33. /**
  34. * Adds a schema with a certain urn to the Validator instance.
  35. * @param schema
  36. * @param urn
  37. * @return {Object}
  38. */
  39. Validator.prototype.addSchema = function addSchema (schema, base) {
  40. var self = this;
  41. if (!schema) {
  42. return null;
  43. }
  44. var scan = scanSchema(base||anonymousBase, schema);
  45. var ourUri = base || schema.$id || schema.id;
  46. for(var uri in scan.id){
  47. this.schemas[uri] = scan.id[uri];
  48. }
  49. for(var uri in scan.ref){
  50. // If this schema is already defined, it will be filtered out by the next step
  51. this.unresolvedRefs.push(uri);
  52. }
  53. // Remove newly defined schemas from unresolvedRefs
  54. this.unresolvedRefs = this.unresolvedRefs.filter(function(uri){
  55. return typeof self.schemas[uri]==='undefined';
  56. });
  57. return this.schemas[ourUri];
  58. };
  59. Validator.prototype.addSubSchemaArray = function addSubSchemaArray(baseuri, schemas) {
  60. if(!Array.isArray(schemas)) return;
  61. for(var i=0; i<schemas.length; i++){
  62. this.addSubSchema(baseuri, schemas[i]);
  63. }
  64. };
  65. Validator.prototype.addSubSchemaObject = function addSubSchemaArray(baseuri, schemas) {
  66. if(!schemas || typeof schemas!='object') return;
  67. for(var p in schemas){
  68. this.addSubSchema(baseuri, schemas[p]);
  69. }
  70. };
  71. /**
  72. * Sets all the schemas of the Validator instance.
  73. * @param schemas
  74. */
  75. Validator.prototype.setSchemas = function setSchemas (schemas) {
  76. this.schemas = schemas;
  77. };
  78. /**
  79. * Returns the schema of a certain urn
  80. * @param urn
  81. */
  82. Validator.prototype.getSchema = function getSchema (urn) {
  83. return this.schemas[urn];
  84. };
  85. /**
  86. * Validates instance against the provided schema
  87. * @param instance
  88. * @param schema
  89. * @param [options]
  90. * @param [ctx]
  91. * @return {Array}
  92. */
  93. Validator.prototype.validate = function validate (instance, schema, options, ctx) {
  94. if((typeof schema !== 'boolean' && typeof schema !== 'object') || schema === null){
  95. throw new SchemaError('Expected `schema` to be an object or boolean');
  96. }
  97. if (!options) {
  98. options = {};
  99. }
  100. // This section indexes subschemas in the provided schema, so they don't need to be added with Validator#addSchema
  101. // This will work so long as the function at uri.resolve() will resolve a relative URI to a relative URI
  102. var id = schema.$id || schema.id;
  103. let base = helpers.resolveUrl(options.base,id||'');
  104. if(!ctx){
  105. ctx = new SchemaContext(schema, options, [], base, Object.create(this.schemas));
  106. if (!ctx.schemas[base]) {
  107. ctx.schemas[base] = schema;
  108. }
  109. var found = scanSchema(base, schema);
  110. for(var n in found.id){
  111. var sch = found.id[n];
  112. ctx.schemas[n] = sch;
  113. }
  114. }
  115. if(options.required && instance===undefined){
  116. var result = new ValidatorResult(instance, schema, options, ctx);
  117. result.addError('is required, but is undefined');
  118. return result;
  119. }
  120. var result = this.validateSchema(instance, schema, options, ctx);
  121. if (!result) {
  122. throw new Error('Result undefined');
  123. }else if(options.throwAll && result.errors.length){
  124. throw new ValidatorResultError(result);
  125. }
  126. return result;
  127. };
  128. /**
  129. * @param Object schema
  130. * @return mixed schema uri or false
  131. */
  132. function shouldResolve(schema) {
  133. var ref = (typeof schema === 'string') ? schema : schema.$ref;
  134. if (typeof ref=='string') return ref;
  135. return false;
  136. }
  137. /**
  138. * Validates an instance against the schema (the actual work horse)
  139. * @param instance
  140. * @param schema
  141. * @param options
  142. * @param ctx
  143. * @private
  144. * @return {ValidatorResult}
  145. */
  146. Validator.prototype.validateSchema = function validateSchema (instance, schema, options, ctx) {
  147. var result = new ValidatorResult(instance, schema, options, ctx);
  148. // Support for the true/false schemas
  149. if(typeof schema==='boolean') {
  150. if(schema===true){
  151. // `true` is always valid
  152. schema = {};
  153. }else if(schema===false){
  154. // `false` is always invalid
  155. schema = {type: []};
  156. }
  157. }else if(!schema){
  158. // This might be a string
  159. throw new Error("schema is undefined");
  160. }
  161. if (schema['extends']) {
  162. if (Array.isArray(schema['extends'])) {
  163. var schemaobj = {schema: schema, ctx: ctx};
  164. schema['extends'].forEach(this.schemaTraverser.bind(this, schemaobj));
  165. schema = schemaobj.schema;
  166. schemaobj.schema = null;
  167. schemaobj.ctx = null;
  168. schemaobj = null;
  169. } else {
  170. schema = helpers.deepMerge(schema, this.superResolve(schema['extends'], ctx));
  171. }
  172. }
  173. // If passed a string argument, load that schema URI
  174. var switchSchema = shouldResolve(schema);
  175. if (switchSchema) {
  176. var resolved = this.resolve(schema, switchSchema, ctx);
  177. var subctx = new SchemaContext(resolved.subschema, options, ctx.path, resolved.switchSchema, ctx.schemas);
  178. return this.validateSchema(instance, resolved.subschema, options, subctx);
  179. }
  180. var skipAttributes = options && options.skipAttributes || [];
  181. // Validate each schema attribute against the instance
  182. for (var key in schema) {
  183. if (!attribute.ignoreProperties[key] && skipAttributes.indexOf(key) < 0) {
  184. var validatorErr = null;
  185. var validator = this.attributes[key];
  186. if (validator) {
  187. validatorErr = validator.call(this, instance, schema, options, ctx);
  188. } else if (options.allowUnknownAttributes === false) {
  189. // This represents an error with the schema itself, not an invalid instance
  190. throw new SchemaError("Unsupported attribute: " + key, schema);
  191. }
  192. if (validatorErr) {
  193. result.importErrors(validatorErr);
  194. }
  195. }
  196. }
  197. if (typeof options.rewrite == 'function') {
  198. var value = options.rewrite.call(this, instance, schema, options, ctx);
  199. result.instance = value;
  200. }
  201. return result;
  202. };
  203. /**
  204. * @private
  205. * @param Object schema
  206. * @param SchemaContext ctx
  207. * @returns Object schema or resolved schema
  208. */
  209. Validator.prototype.schemaTraverser = function schemaTraverser (schemaobj, s) {
  210. schemaobj.schema = helpers.deepMerge(schemaobj.schema, this.superResolve(s, schemaobj.ctx));
  211. };
  212. /**
  213. * @private
  214. * @param Object schema
  215. * @param SchemaContext ctx
  216. * @returns Object schema or resolved schema
  217. */
  218. Validator.prototype.superResolve = function superResolve (schema, ctx) {
  219. var ref = shouldResolve(schema);
  220. if(ref) {
  221. return this.resolve(schema, ref, ctx).subschema;
  222. }
  223. return schema;
  224. };
  225. /**
  226. * @private
  227. * @param Object schema
  228. * @param Object switchSchema
  229. * @param SchemaContext ctx
  230. * @return Object resolved schemas {subschema:String, switchSchema: String}
  231. * @throws SchemaError
  232. */
  233. Validator.prototype.resolve = function resolve (schema, switchSchema, ctx) {
  234. switchSchema = ctx.resolve(switchSchema);
  235. // First see if the schema exists under the provided URI
  236. if (ctx.schemas[switchSchema]) {
  237. return {subschema: ctx.schemas[switchSchema], switchSchema: switchSchema};
  238. }
  239. // Else try walking the property pointer
  240. let parsed = new URL(switchSchema,'thismessage::/');
  241. let fragment = parsed.hash;
  242. var document = fragment && fragment.length && switchSchema.substr(0, switchSchema.length - fragment.length);
  243. if (!document || !ctx.schemas[document]) {
  244. throw new SchemaError("no such schema <" + switchSchema + ">", schema);
  245. }
  246. var subschema = helpers.objectGetPath(ctx.schemas[document], fragment.substr(1));
  247. if(subschema===undefined){
  248. throw new SchemaError("no such schema " + fragment + " located in <" + document + ">", schema);
  249. }
  250. return {subschema: subschema, switchSchema: switchSchema};
  251. };
  252. /**
  253. * Tests whether the instance if of a certain type.
  254. * @private
  255. * @param instance
  256. * @param schema
  257. * @param options
  258. * @param ctx
  259. * @param type
  260. * @return {boolean}
  261. */
  262. Validator.prototype.testType = function validateType (instance, schema, options, ctx, type) {
  263. if(type===undefined){
  264. return;
  265. }else if(type===null){
  266. throw new SchemaError('Unexpected null in "type" keyword');
  267. }
  268. if (typeof this.types[type] == 'function') {
  269. return this.types[type].call(this, instance);
  270. }
  271. if (type && typeof type == 'object') {
  272. var res = this.validateSchema(instance, type, options, ctx);
  273. return res === undefined || !(res && res.errors.length);
  274. }
  275. // Undefined or properties not on the list are acceptable, same as not being defined
  276. return true;
  277. };
  278. var types = Validator.prototype.types = {};
  279. types.string = function testString (instance) {
  280. return typeof instance == 'string';
  281. };
  282. types.number = function testNumber (instance) {
  283. // isFinite returns false for NaN, Infinity, and -Infinity
  284. return typeof instance == 'number' && isFinite(instance);
  285. };
  286. types.integer = function testInteger (instance) {
  287. return (typeof instance == 'number') && instance % 1 === 0;
  288. };
  289. types.boolean = function testBoolean (instance) {
  290. return typeof instance == 'boolean';
  291. };
  292. types.array = function testArray (instance) {
  293. return Array.isArray(instance);
  294. };
  295. types['null'] = function testNull (instance) {
  296. return instance === null;
  297. };
  298. types.date = function testDate (instance) {
  299. return instance instanceof Date;
  300. };
  301. types.any = function testAny (instance) {
  302. return true;
  303. };
  304. types.object = function testObject (instance) {
  305. // TODO: fix this - see #15
  306. return instance && (typeof instance === 'object') && !(Array.isArray(instance)) && !(instance instanceof Date);
  307. };
  308. module.exports = Validator;