ZSchema.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. "use strict";
  2. require("./Polyfills");
  3. var get = require("lodash.get");
  4. var Report = require("./Report");
  5. var FormatValidators = require("./FormatValidators");
  6. var JsonValidation = require("./JsonValidation");
  7. var SchemaCache = require("./SchemaCache");
  8. var SchemaCompilation = require("./SchemaCompilation");
  9. var SchemaValidation = require("./SchemaValidation");
  10. var Utils = require("./Utils");
  11. var Draft4Schema = require("./schemas/schema.json");
  12. var Draft4HyperSchema = require("./schemas/hyper-schema.json");
  13. /**
  14. * default options
  15. */
  16. var defaultOptions = {
  17. // default timeout for all async tasks
  18. asyncTimeout: 2000,
  19. // force additionalProperties and additionalItems to be defined on "object" and "array" types
  20. forceAdditional: false,
  21. // assume additionalProperties and additionalItems are defined as "false" where appropriate
  22. assumeAdditional: false,
  23. // do case insensitive comparison for enums
  24. enumCaseInsensitiveComparison: false,
  25. // force items to be defined on "array" types
  26. forceItems: false,
  27. // force minItems to be defined on "array" types
  28. forceMinItems: false,
  29. // force maxItems to be defined on "array" types
  30. forceMaxItems: false,
  31. // force minLength to be defined on "string" types
  32. forceMinLength: false,
  33. // force maxLength to be defined on "string" types
  34. forceMaxLength: false,
  35. // force properties or patternProperties to be defined on "object" types
  36. forceProperties: false,
  37. // ignore references that cannot be resolved (remote schemas) // TODO: make sure this is only for remote schemas, not local ones
  38. ignoreUnresolvableReferences: false,
  39. // disallow usage of keywords that this validator can't handle
  40. noExtraKeywords: false,
  41. // disallow usage of schema's without "type" defined
  42. noTypeless: false,
  43. // disallow zero length strings in validated objects
  44. noEmptyStrings: false,
  45. // disallow zero length arrays in validated objects
  46. noEmptyArrays: false,
  47. // forces "uri" format to be in fully rfc3986 compliant
  48. strictUris: false,
  49. // turn on some of the above
  50. strictMode: false,
  51. // report error paths as an array of path segments to get to the offending node
  52. reportPathAsArray: false,
  53. // stop validation as soon as an error is found
  54. breakOnFirstError: false,
  55. // check if schema follows best practices and common sense
  56. pedanticCheck: false,
  57. // ignore unknown formats (do not report them as an error)
  58. ignoreUnknownFormats: false,
  59. // function to be called on every schema
  60. customValidator: null
  61. };
  62. function normalizeOptions(options) {
  63. var normalized;
  64. // options
  65. if (typeof options === "object") {
  66. var keys = Object.keys(options),
  67. idx = keys.length,
  68. key;
  69. // check that the options are correctly configured
  70. while (idx--) {
  71. key = keys[idx];
  72. if (defaultOptions[key] === undefined) {
  73. throw new Error("Unexpected option passed to constructor: " + key);
  74. }
  75. }
  76. // copy the default options into passed options
  77. keys = Object.keys(defaultOptions);
  78. idx = keys.length;
  79. while (idx--) {
  80. key = keys[idx];
  81. if (options[key] === undefined) {
  82. options[key] = Utils.clone(defaultOptions[key]);
  83. }
  84. }
  85. normalized = options;
  86. } else {
  87. normalized = Utils.clone(defaultOptions);
  88. }
  89. if (normalized.strictMode === true) {
  90. normalized.forceAdditional = true;
  91. normalized.forceItems = true;
  92. normalized.forceMaxLength = true;
  93. normalized.forceProperties = true;
  94. normalized.noExtraKeywords = true;
  95. normalized.noTypeless = true;
  96. normalized.noEmptyStrings = true;
  97. normalized.noEmptyArrays = true;
  98. }
  99. return normalized;
  100. }
  101. /**
  102. * @class
  103. *
  104. * @param {*} [options]
  105. */
  106. function ZSchema(options) {
  107. this.cache = {};
  108. this.referenceCache = [];
  109. this.validateOptions = {};
  110. this.options = normalizeOptions(options);
  111. // Disable strict validation for the built-in schemas
  112. var metaschemaOptions = normalizeOptions({ });
  113. this.setRemoteReference("http://json-schema.org/draft-04/schema", Draft4Schema, metaschemaOptions);
  114. this.setRemoteReference("http://json-schema.org/draft-04/hyper-schema", Draft4HyperSchema, metaschemaOptions);
  115. }
  116. /**
  117. * instance methods
  118. *
  119. * @param {*} schema
  120. *
  121. * @returns {boolean}
  122. */
  123. ZSchema.prototype.compileSchema = function (schema) {
  124. var report = new Report(this.options);
  125. schema = SchemaCache.getSchema.call(this, report, schema);
  126. SchemaCompilation.compileSchema.call(this, report, schema);
  127. this.lastReport = report;
  128. return report.isValid();
  129. };
  130. /**
  131. *
  132. * @param {*} schema
  133. *
  134. * @returns {boolean}
  135. */
  136. ZSchema.prototype.validateSchema = function (schema) {
  137. if (Array.isArray(schema) && schema.length === 0) {
  138. throw new Error(".validateSchema was called with an empty array");
  139. }
  140. var report = new Report(this.options);
  141. schema = SchemaCache.getSchema.call(this, report, schema);
  142. var compiled = SchemaCompilation.compileSchema.call(this, report, schema);
  143. if (compiled) { SchemaValidation.validateSchema.call(this, report, schema); }
  144. this.lastReport = report;
  145. return report.isValid();
  146. };
  147. /**
  148. *
  149. * @param {*} json
  150. * @param {*} schema
  151. * @param {*} [options]
  152. * @param {function(*, *)} [callback]
  153. *
  154. * @returns {boolean}
  155. */
  156. ZSchema.prototype.validate = function (json, schema, options, callback) {
  157. if (Utils.whatIs(options) === "function") {
  158. callback = options;
  159. options = {};
  160. }
  161. if (!options) { options = {}; }
  162. this.validateOptions = options;
  163. var whatIs = Utils.whatIs(schema);
  164. if (whatIs !== "string" && whatIs !== "object") {
  165. var e = new Error("Invalid .validate call - schema must be a string or object but " + whatIs + " was passed!");
  166. if (callback) {
  167. process.nextTick(function () {
  168. callback(e, false);
  169. });
  170. return;
  171. }
  172. throw e;
  173. }
  174. var foundError = false;
  175. var report = new Report(this.options);
  176. report.json = json;
  177. if (typeof schema === "string") {
  178. var schemaName = schema;
  179. schema = SchemaCache.getSchema.call(this, report, schemaName);
  180. if (!schema) {
  181. throw new Error("Schema with id '" + schemaName + "' wasn't found in the validator cache!");
  182. }
  183. } else {
  184. schema = SchemaCache.getSchema.call(this, report, schema);
  185. }
  186. var compiled = false;
  187. if (!foundError) {
  188. compiled = SchemaCompilation.compileSchema.call(this, report, schema);
  189. }
  190. if (!compiled) {
  191. this.lastReport = report;
  192. foundError = true;
  193. }
  194. var validated = false;
  195. if (!foundError) {
  196. validated = SchemaValidation.validateSchema.call(this, report, schema);
  197. }
  198. if (!validated) {
  199. this.lastReport = report;
  200. foundError = true;
  201. }
  202. if (options.schemaPath) {
  203. report.rootSchema = schema;
  204. schema = get(schema, options.schemaPath);
  205. if (!schema) {
  206. throw new Error("Schema path '" + options.schemaPath + "' wasn't found in the schema!");
  207. }
  208. }
  209. if (!foundError) {
  210. JsonValidation.validate.call(this, report, schema, json);
  211. }
  212. if (callback) {
  213. report.processAsyncTasks(this.options.asyncTimeout, callback);
  214. return;
  215. } else if (report.asyncTasks.length > 0) {
  216. throw new Error("This validation has async tasks and cannot be done in sync mode, please provide callback argument.");
  217. }
  218. // assign lastReport so errors are retrievable in sync mode
  219. this.lastReport = report;
  220. return report.isValid();
  221. };
  222. ZSchema.prototype.getLastError = function () {
  223. if (this.lastReport.errors.length === 0) {
  224. return null;
  225. }
  226. var e = new Error();
  227. e.name = "z-schema validation error";
  228. e.message = this.lastReport.commonErrorMessage;
  229. e.details = this.lastReport.errors;
  230. return e;
  231. };
  232. ZSchema.prototype.getLastErrors = function () {
  233. return this.lastReport && this.lastReport.errors.length > 0 ? this.lastReport.errors : null;
  234. };
  235. ZSchema.prototype.getMissingReferences = function (arr) {
  236. arr = arr || this.lastReport.errors;
  237. var res = [],
  238. idx = arr.length;
  239. while (idx--) {
  240. var error = arr[idx];
  241. if (error.code === "UNRESOLVABLE_REFERENCE") {
  242. var reference = error.params[0];
  243. if (res.indexOf(reference) === -1) {
  244. res.push(reference);
  245. }
  246. }
  247. if (error.inner) {
  248. res = res.concat(this.getMissingReferences(error.inner));
  249. }
  250. }
  251. return res;
  252. };
  253. ZSchema.prototype.getMissingRemoteReferences = function () {
  254. var missingReferences = this.getMissingReferences(),
  255. missingRemoteReferences = [],
  256. idx = missingReferences.length;
  257. while (idx--) {
  258. var remoteReference = SchemaCache.getRemotePath(missingReferences[idx]);
  259. if (remoteReference && missingRemoteReferences.indexOf(remoteReference) === -1) {
  260. missingRemoteReferences.push(remoteReference);
  261. }
  262. }
  263. return missingRemoteReferences;
  264. };
  265. ZSchema.prototype.setRemoteReference = function (uri, schema, validationOptions) {
  266. if (typeof schema === "string") {
  267. schema = JSON.parse(schema);
  268. } else {
  269. schema = Utils.cloneDeep(schema);
  270. }
  271. if (validationOptions) {
  272. schema.__$validationOptions = normalizeOptions(validationOptions);
  273. }
  274. SchemaCache.cacheSchemaByUri.call(this, uri, schema);
  275. };
  276. ZSchema.prototype.getResolvedSchema = function (schema) {
  277. var report = new Report(this.options);
  278. schema = SchemaCache.getSchema.call(this, report, schema);
  279. // clone before making any modifications
  280. schema = Utils.cloneDeep(schema);
  281. var visited = [];
  282. // clean-up the schema and resolve references
  283. var cleanup = function (schema) {
  284. var key,
  285. typeOf = Utils.whatIs(schema);
  286. if (typeOf !== "object" && typeOf !== "array") {
  287. return;
  288. }
  289. if (schema.___$visited) {
  290. return;
  291. }
  292. schema.___$visited = true;
  293. visited.push(schema);
  294. if (schema.$ref && schema.__$refResolved) {
  295. var from = schema.__$refResolved;
  296. var to = schema;
  297. delete schema.$ref;
  298. delete schema.__$refResolved;
  299. for (key in from) {
  300. if (from.hasOwnProperty(key)) {
  301. to[key] = from[key];
  302. }
  303. }
  304. }
  305. for (key in schema) {
  306. if (schema.hasOwnProperty(key)) {
  307. if (key.indexOf("__$") === 0) {
  308. delete schema[key];
  309. } else {
  310. cleanup(schema[key]);
  311. }
  312. }
  313. }
  314. };
  315. cleanup(schema);
  316. visited.forEach(function (s) {
  317. delete s.___$visited;
  318. });
  319. this.lastReport = report;
  320. if (report.isValid()) {
  321. return schema;
  322. } else {
  323. throw this.getLastError();
  324. }
  325. };
  326. /**
  327. *
  328. * @param {*} schemaReader
  329. *
  330. * @returns {void}
  331. */
  332. ZSchema.prototype.setSchemaReader = function (schemaReader) {
  333. return ZSchema.setSchemaReader(schemaReader);
  334. };
  335. ZSchema.prototype.getSchemaReader = function () {
  336. return ZSchema.schemaReader;
  337. };
  338. ZSchema.schemaReader = undefined;
  339. /*
  340. static methods
  341. */
  342. ZSchema.setSchemaReader = function (schemaReader) {
  343. ZSchema.schemaReader = schemaReader;
  344. };
  345. ZSchema.registerFormat = function (formatName, validatorFunction) {
  346. FormatValidators[formatName] = validatorFunction;
  347. };
  348. ZSchema.unregisterFormat = function (formatName) {
  349. delete FormatValidators[formatName];
  350. };
  351. ZSchema.getRegisteredFormats = function () {
  352. return Object.keys(FormatValidators);
  353. };
  354. ZSchema.getDefaultOptions = function () {
  355. return Utils.cloneDeep(defaultOptions);
  356. };
  357. ZSchema.schemaSymbol = Utils.schemaSymbol;
  358. ZSchema.jsonSymbol = Utils.jsonSymbol;
  359. module.exports = ZSchema;