diff --git a/lib/utils/index.js b/lib/utils/index.js index ab60de5..d296889 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -142,7 +142,7 @@ function normalize(uri) { return decodeURIComponent(uri.replace(/~1/g, '/').replace(/~0/g, '~')); } -function descent(uri, parentSchema) { +function descend(uri, parentSchema) { let uriFragment = fragment(uri); if (!uriFragment && isFullUri(uri)) { return parentSchema; @@ -211,5 +211,5 @@ module.exports = { isFullUri, head, fragment, - descent, + descend, }; diff --git a/lib/utils/state.js b/lib/utils/state.js index 5c9528d..4587fd5 100644 --- a/lib/utils/state.js +++ b/lib/utils/state.js @@ -1,7 +1,7 @@ const { list: validators } = require('../validators'); const { body, restore, template } = require('./template'); const { - descent, + descend, makePath, head, isFullUri, @@ -13,15 +13,25 @@ const { function State(schema = {}, env) { Object.assign(this, { - resolution: [0], context: [], entries: new Map(), env, }); - - this.push(schema); } +/** + * @name generate + * @type {function} + * @description + * The main schema process function. + * Available and used both in external and internal generation. + * Saves the state for internal recursive calls. + * @param {object} env - djv environment + * @param {object} schema - to process + * @param {State} state - saved state + * @param {Environment} options + * @returns {function} restoredFunction + */ function generate(env, schema, state = new State(schema, env), options) { const tpl = template(state, options); tpl.visit(schema); @@ -31,7 +41,6 @@ function generate(env, schema, state = new State(schema, env), options) { } State.prototype = Object.assign(Object.create(Array.prototype), { - // template related methods /** * @name addEntry * @type {function} @@ -39,48 +48,43 @@ State.prototype = Object.assign(Object.create(Array.prototype), { * Generates an internal function. * Usually necessary for `allOf` types of validators. * Caches generated functions by schema object key. + * Checks for an existing schema in a context stack to avoid double parsing and generation. * @param {string} url * @param {object} schema * @returns {number/boolean} index */ addEntry(url, schema) { - if (!isSchema(schema)) { - return schema; + let entry = this.entries.get(schema); + if (entry === false) { + // has already been added to process queue + // will be revealed as an entry + return this.context.push(schema); } - const options = { inner: true }; - const entry = this.entries.get(schema) || generate(this.env, schema, this, options); - this.entries.set(schema, entry); - this.revealReference(url); + if (typeof entry === 'undefined') { + // start to process schema + this.entries.set(schema, false); + entry = generate(this.env, schema, this, { inner: true }); + this.entries.set(schema, entry); + this.revealReference(schema); + } return this.context.push(entry); }, /** - * @name addReference + * @name revealReference * @type {function} * @description - * Checks for an existing schema in a context stack to avoid double parsing and generation. - * @param {string} url + * If a schema was added during the add entry phase + * Then it should be revealed in this step * @param {object} schema - * @returns {number/boolean} index + * @returns {void} */ - addReference(url, schema) { - const schemaIndex = this.indexOf(schema); - // reference to itself - if (schemaIndex === 0) { - return this.context.push(0); - } - if (schemaIndex !== -1 && schemaIndex !== this.length - 1) { - // has already been added to process queue - return this.context.push(url); - } - return false; - }, - revealReference(url) { + revealReference(schema) { for ( - let doubled = this.context.indexOf(url); + let doubled = this.context.indexOf(schema); doubled !== -1; - doubled = this.context.indexOf(url) + doubled = this.context.indexOf(schema) ) { this.context[doubled] = this.context.length; } @@ -89,53 +93,15 @@ State.prototype = Object.assign(Object.create(Array.prototype), { * @name link * @type {function} * @description - * Resolves schema by given url and current registered context stack. - * Returns an entry's index of in a context stack. + * Returns an entry's index in a context stack. * @param {string} url * @returns {string} entry */ link(url) { const schema = this.resolve(url); - const entry = this.addReference(url, schema) || this.addEntry(url, schema); - + const entry = this.addEntry(url, schema); return entry; }, - // pure scope resolution methods - push(schema) { - // if schema id is partial - // it should be resolved against the closest parent(s) - // so only valid URIs added to resolution - if (schema && schema.id && this.length && isFullUri(schema.id)) { - this.resolution.push(this.length); - } - - Array.prototype.push.call(this, schema); - }, - getResolvedSchema(path) { - if (hasProperty(this.env.resolved, path)) { - return this.env.resolved[path].schema; - } - - const cleanPath = cleanId(path); - if (hasProperty(this.env.resolved, cleanPath)) { - return this.env.resolved[cleanPath].schema; - } - - if (cleanPath) { - const foundResolvedIndex = this.resolution - .reverse() - .find(index => this[index].id && ( - this[index].id === cleanPath || - this[index].id === path - )); - - if (typeof foundResolvedIndex !== 'undefined') { - return this[foundResolvedIndex]; - } - } - - return null; - }, /** * @name ascend * @type {function} @@ -146,47 +112,40 @@ State.prototype = Object.assign(Object.create(Array.prototype), { * @returns {object} parentSchema */ ascend(reference) { - const indexOfParent = Math.max(this.resolution[this.resolution.length - 1], 1); - - let parentSchemaPath = head(reference); - let parentSchema = this[indexOfParent]; - - if (parentSchemaPath && this.length > 1 && !isFullUri(reference)) { - parentSchemaPath = makePath( - this.slice(indexOfParent) - .map(({ id }) => id) - .concat(parentSchemaPath) - ); + const path = head(reference); + // already saved by resolved id + if (hasProperty(this.env.resolved, path)) { + const resolvedWithEnvironment = this.env.resolved[path]; + return resolvedWithEnvironment.schema; } - const resolvedSchema = this.getResolvedSchema(parentSchemaPath); - if (!parentSchema || resolvedSchema) { - parentSchema = resolvedSchema; - } + // search in a backward order for the last "resolution" schema + // TODO refactor reverse, slice on every call + const parentSchema = this + .slice() + .reverse() + .find(({ id }) => id) + || this[0]; - // try to resolve it against environment - if (hasProperty(parentSchema, '$ref')) { - const ref = head(parentSchema.$ref); - if (ref !== '#' && this.getResolvedSchema(ref)) { - parentSchema = this.getResolvedSchema(ref); - } - } - - this.push(parentSchema); return parentSchema; }, + /** + * @name resolve + * @type {function} + * @description + * Resolves schema by given reference and current registered context stack. + * @param {string} url + * @returns {object} schema + */ resolve(reference) { - if (reference === '#') { - return 0; - } if (typeof reference !== 'string') { return reference; } - const parent = this.ascend(reference); - const resolved = descent(reference, parent); + const parentSchema = this.ascend(reference); + const subSchema = descend(reference, parentSchema); - return resolved; + return subSchema; }, /** * @name visit @@ -199,18 +158,12 @@ State.prototype = Object.assign(Object.create(Array.prototype), { * @returns {void} */ visit(pseudoSchema, tpl) { - const initialStateLength = this.length; - const initialResolutionLength = this.resolution.length; - const schema = transformSchema(pseudoSchema); this.push(schema); validators.forEach((validator) => { validator(schema, tpl); }); - - this.length = initialStateLength; - this.resolution.length = initialResolutionLength; }, }); diff --git a/lib/utils/template.js b/lib/utils/template.js index abf4237..0bada4a 100644 --- a/lib/utils/template.js +++ b/lib/utils/template.js @@ -158,7 +158,7 @@ function body(tpl, state, { inner, errorHandler } = {}) { state.context .forEach((value, i) => { if (typeof value === 'number') { - references.push(`${i + 1}=f${value}`); + references.push(`${i + 1}=f${value + 1}`); return; } functions.push(`${i + 1}=${value}`);