diff --git a/site/docs/api/references.md b/site/docs/api/references.md index 0a429af79..88ca1649f 100644 --- a/site/docs/api/references.md +++ b/site/docs/api/references.md @@ -16,7 +16,7 @@ to identify a URI to a remote resource. It also allows you to point to JSON str current document, enabling re-use. Alpaca extends `$ref` by allowing for additional loaders such as a dictionary definition loader for Cloud CMS. -Alapca lets you register Connectors to handle loading of your custom +Alpaca lets you register Connectors to handle loading of your custom `$ref` values. Alpaca also lets you use `$ref` structures within your options blocks. This lets you load options from remote sources @@ -32,7 +32,11 @@ https://www.cloudcms.com/documentation/forms/references.html https://www.cloudcms.com/documentation/content-models/references.html -## Example #1: Nested Tree (using Array) + + +## Examples + +### Example #1: Nested Tree (using Array) This example demonstrates the use of JSON Schema referencing to include or pull in schema definitions from other parts of the document. Alpaca supports referencing within @@ -68,7 +72,7 @@ $("#field1").alpaca({ {% endraw %} -## Example #2: Deeply Nested Tree (loaded from data) +### Example #2: Deeply Nested Tree (loaded from data) This example shows a deeply nested tree loaded with data. Note that unlike the previous example, this example uses a non-'create' view which allows data to bind. @@ -119,7 +123,7 @@ $("#field2").alpaca({ {% endraw %} -## Example #3: Internal Definitions +### Example #3: Internal Definitions Here is an example of an object that derives one of its sub-object properties from a referenced definition. The definition is stored in a special @@ -170,7 +174,7 @@ $("#field3").alpaca({ {% endraw %} -## Example #4: Using `$ref` within options +### Example #4: Using `$ref` within options Suppose that you have a remote Author schema and options and that their URIs are: diff --git a/src/js/Alpaca.js b/src/js/Alpaca.js index 156d561a0..ffe203784 100644 --- a/src/js/Alpaca.js +++ b/src/js/Alpaca.js @@ -2682,193 +2682,176 @@ return $(el).attr(name); }; - Alpaca.loadRefSchemaOptions = function(topField, schemaReferenceId, optionsReferenceId, callback) + /** + * Loads the schema and options for a form given their $ref strings. + * + * Each $ref string could look like: + * + * "id" (an ID for a schema element) + * "#id" (an ID for a schema element) + * "#a/b" (a path to a schema element) + * "#/a/b" (a path to a schema elemnet) + * "https://server.com/resource#/a/b (a path to a schema element inside of a remote resource) + * "https://server.com/resource#a" (an ID for a schema element inside of a remote resource + * "qname://a/b#c/d (a path to a schema element inside of a branch schema) + * + * @param schema + * @param options + * @param schemaReferenceString + * @param optionsReferenceString + * @param connector (required for any remote loading) + * @param callback + */ + Alpaca.loadRefSchemaOptions = function(schema, options, schemaReferenceString, optionsReferenceString, connector, callback) { var fns = []; // holds resolution information var resolution = {}; - // schema loading function - var fn1 = function(schema, schemaReferenceId, resolution) + var splitReferenceString = function(referenceString) { - return function(done) + // split "qname://a/b#/c/d" -> id = "qname://a/b" and hash = "#/c/d" + var referenceResource = null; + var referenceHash = null; + var x = referenceString.indexOf("#"); + if (x > -1) { - if (!schemaReferenceId) - { - done(); - } - else if (schemaReferenceId === "#") - { - resolution.schema = schema; + referenceResource = referenceString.substring(0, x); + referenceHash = referenceString.substring(x); + } + else + { + referenceResource = referenceString; + referenceHash = null; + } - done(); - } - else if (schemaReferenceId.indexOf("#/") === 0) - { - // this is a property path relative to the root of the current schema - schemaReferenceId = schemaReferenceId.substring(2); + if (referenceResource === "") { + referenceResource = null; + } - // split into tokens - var tokens = schemaReferenceId.split("/"); + var result = {}; + if (referenceResource) { + result.resource = referenceResource; + }; + if (referenceHash) { + result.hash = referenceHash; + } - var defSchema = schema; - for (var i = 0; i < tokens.length; i++) - { - var token = tokens[i]; + return result; + }; - // schema - if (defSchema[token]) - { - defSchema = defSchema[token]; - } - else if (defSchema.properties && defSchema.properties[token]) - { - defSchema = defSchema.properties[token]; - } - else if (defSchema.definitions && defSchema.definitions[token]) - { - defSchema = defSchema.definitions[token]; - } - else - { - defSchema = null; - break; - } + if (schemaReferenceString) + { + // schema loading function + var fn1 = function(schema, referenceString, resolution, connector) + { + return function(done) + { + if (!referenceString) + { + return done(); } - resolution.schema = defSchema; + var reference = splitReferenceString(referenceString); - done(); - } - else if (schemaReferenceId.indexOf("#") === 0) - { - // this is the ID of a node in the current schema document - - // walk the current document schema until we find the referenced node (using id property) - var resolvedSchema = Alpaca.resolveSchemaReference(schema, schemaReferenceId); - if (resolvedSchema) + if (reference.resource) { - resolution.schema = resolvedSchema; - } + // a reference by ID or by path (since we support "a" and "/a/b/c") + var resolvedSchema = Alpaca.resolveSchemaReference(schema, reference.resource); + if (resolvedSchema) + { + resolution.schema = resolvedSchema; + return done(); + } - done(); - } - else - { - // the reference is considered to be a URI with or without a "#" in it to point to a specific location in - // the target schema + if (!connector) + { + return done(); + } - var referenceParts = Alpaca.pathParts(schemaReferenceId); + // it's a remote reference? + return connector.loadReferenceSchema(reference.resource, function (schema) { - topField.connector.loadReferenceSchema(referenceParts.path, function (schema) { + if (!reference.hash) + { + resolution.schema = schema; + return done(); + } - if (referenceParts.id) - { - var resolvedSchema = Alpaca.resolveSchemaReference(schema, referenceParts.id); + // offset path into remote resource (after hash) + var resolvedSchema = Alpaca.resolveSchemaReference(schema, reference.hash); if (resolvedSchema) { resolution.schema = resolvedSchema; + return done(); } - } - else + + done(); + }, function(err) { + done(); + }); + } + + if (reference.hash) + { + // a reference by ID or by path + var resolvedSchema = Alpaca.resolveSchemaReference(schema, reference.hash); + if (resolvedSchema) { - resolution.schema = schema; + resolution.schema = resolvedSchema; } - done(); - }, function(err) { - done(); - }); - } + return done(); + } + + done(); + }; }; - }; - fns.push(fn1(topField.schema, schemaReferenceId, resolution)); + fns.push(fn1(schema, schemaReferenceString, resolution, connector)); + } - var fn2 = function(options, optionsReferenceId, resolution) + var fn2 = function(options, referenceString, resolution, connector) { return function(done) { - if (!optionsReferenceId) + if (!referenceString) { - done(); + return done(); } - else if (optionsReferenceId === "#") - { - resolution.options = options; - done(); - } - else if (optionsReferenceId.indexOf("#/") === 0) - { - // this is a property path relative to the root of the current schema - optionsReferenceId = optionsReferenceId.substring(2); - - // split into tokens - var tokens = optionsReferenceId.split("/"); - - var defOptions = options; - for (var i = 0; i < tokens.length; i++) - { - var token = tokens[i]; - - // options - if (defOptions[token]) - { - defOptions = defOptions[token]; - } - else if (defOptions.fields && defOptions.fields[token]) - { - defOptions = defOptions.fields[token]; - } - else if (defOptions.definitions && defOptions.definitions[token]) - { - defOptions = defOptions.definitions[token]; - } - else - { - defOptions = null; - break; - } - } + var reference = splitReferenceString(referenceString); - resolution.options = defOptions; - - done(); - } - else if (optionsReferenceId.indexOf("#") === 0) + if (reference.resource) { - // this is the ID of a node in the current schema document - - // walk the current document schema until we find the referenced node (using id property) - var resolvedOptions = Alpaca.resolveOptionsReference(options, optionsReferenceId); + // a reference by ID or by path (since we support "a" and "/a/b/c") + var resolvedOptions = Alpaca.resolveOptionsReference(options, reference.resource); if (resolvedOptions) { resolution.options = resolvedOptions; + return done(); } - done(); - } - else - { - // the reference is considered to be a URI with or without a "#" in it to point to a specific location in - // the target schema - - var optionReferenceParts = Alpaca.pathParts(optionsReferenceId); + if (!connector) + { + return done(); + } - topField.connector.loadReferenceOptions(optionReferenceParts.path, function (options) { + // it's a remote reference? + return connector.loadReferenceOptions(reference.resource, function (options) { - if (optionReferenceParts.id) + if (!reference.hash) { - var resolvedOptions = Alpaca.resolveOptionsReference(options, optionReferenceParts.id); - if (resolvedOptions) - { - resolution.options = resolvedOptions; - } + resolution.options = options; + return done(); } - else + + // offset path into remote resource (after hash) + var resolvedOptions = Alpaca.resolveOptionsReference(options, reference.hash); + if (resolvedOptions) { - resolution.options = options; + resolution.options = resolvedOptions; + return done(); } done(); @@ -2876,13 +2859,27 @@ done(); }); } + + if (reference.hash) + { + // a reference by ID or by path + var resolvedOptions = Alpaca.resolveOptionsReference(options, reference.hash); + if (resolvedOptions) + { + resolution.options = resolvedOptions; + } + + return done(); + } + + done(); }; }; - fns.push(fn2(topField.options, optionsReferenceId, resolution)); + fns.push(fn2(options, optionsReferenceString, resolution, connector)); // run loads in parallel - Alpaca.parallel(fns, function() { - callback(resolution.schema, resolution.options); + Alpaca.parallel(fns, function(err) { + callback(null, resolution.schema, resolution.options); }); }; @@ -2957,38 +2954,100 @@ /** * Resolves a schema path reference to the given sub-schema. * + * The schema referenceId can either be an explicit ID (such as "123" or "#123") + * or it can be a relative path within the current document (i.e. "#/a/b/c"). + * * @param schema * @param referenceId * @returns {*} */ Alpaca.resolveSchemaReference = function(schema, referenceId) { - if ((schema.id === referenceId) || (("#" + schema.id) === referenceId)) // jshint ignore:line + if (!referenceId) + { + return null; + } + + if ((schema.id === referenceId) || (("#" + schema.id) === referenceId) || referenceId === "/") // jshint ignore:line { return schema; } - if (schema.properties) + if (Alpaca.startsWith(referenceId, "#")) + { + referenceId = referenceId.substring(1); + } + + if (Alpaca.endsWith(referenceId, "/")) { - for (var propertyId in schema.properties) + referenceId = referenceId.substring(0, referenceId.length - 1); + } + + if (Alpaca.startsWith(referenceId, "/")) + { + // path based lookup + referenceId = referenceId.substring(1); + + // split into tokens + var tokens = referenceId.split("/"); + + var defSchema = schema; + for (var i = 0; i < tokens.length; i++) { - var subSchema = schema.properties[propertyId]; + var token = tokens[i]; - var x = Alpaca.resolveSchemaReference(subSchema, referenceId); - if (x) + // schema + if (defSchema[token]) { - return x; + defSchema = defSchema[token]; + } + else if (defSchema.properties && defSchema.properties[token]) + { + defSchema = defSchema.properties[token]; + } + else if (defSchema.definitions && defSchema.definitions[token]) + { + defSchema = defSchema.definitions[token]; + } + else if (defSchema.items) + { + defSchema = defSchema.items[token]; + } + else + { + defSchema = null; + break; } } + + return defSchema; } - else if (schema.items) + else { - var subSchema = schema.items; + // id based lookup - var x = Alpaca.resolveSchemaReference(subSchema, referenceId); - if (x) + if (schema.properties) { - return x; + for (var propertyId in schema.properties) + { + var subSchema = schema.properties[propertyId]; + + var x = Alpaca.resolveSchemaReference(subSchema, referenceId); + if (x) + { + return x; + } + } + } + else if (schema.items) + { + var subSchema = schema.items; + + var x = Alpaca.resolveSchemaReference(subSchema, referenceId); + if (x) + { + return x; + } } } @@ -2997,32 +3056,91 @@ Alpaca.resolveOptionsReference = function(options, referenceId) { - if ((options.id === referenceId) || (("#" + options.id) === referenceId)) // jshint ignore:line + if (!referenceId) + { + return null; + } + + if ((options.id === referenceId) || (("#" + options.id) === referenceId) || referenceId === "/") // jshint ignore:line { return options; } - if (options.fields) + if (Alpaca.startsWith(referenceId, "#")) { - for (var fieldId in options.fields) + referenceId = referenceId.substring(1); + } + + if (Alpaca.endsWith(referenceId, "/")) + { + referenceId = referenceId.substring(0, referenceId.length - 1); + } + + // if it's a path... + if (Alpaca.startsWith(referenceId, "/")) + { + referenceId = referenceId.substring(1); + + // split into tokens + var tokens = referenceId.split("/"); + + var defOptions = options; + for (var i = 0; i < tokens.length; i++) { - var subOptions = options.fields[fieldId]; + var token = tokens[i]; - var x = Alpaca.resolveOptionsReference(subOptions, referenceId); - if (x) + // options + if (defOptions[token]) { - return x; + defOptions = defOptions[token]; + } + else if (defOptions.fields && defOptions.fields[token]) + { + defOptions = defOptions.fields[token]; + } + else if (defOptions.definitions && defOptions.definitions[token]) + { + defOptions = defOptions.definitions[token]; + } + else if (defOptions.items && defOptions.items[token]) + { + defOptions = defOptions.items[token]; + } + else + { + defOptions = null; + break; } } + + return defOptions; } - else if (options.items) + else { - var subOptions = options.items; + // it's an ID lookup, use this legacy approach + + if (options.fields) + { + for (var fieldId in options.fields) + { + var subOptions = options.fields[fieldId]; - var x = Alpaca.resolveOptionsReference(subOptions, referenceId); - if (x) + var x = Alpaca.resolveOptionsReference(subOptions, referenceId); + if (x) + { + return x; + } + } + } + else if (options.items) { - return x; + var subOptions = options.items; + + var x = Alpaca.resolveOptionsReference(subOptions, referenceId); + if (x) + { + return x; + } } } @@ -3393,7 +3511,7 @@ { ourselvesHandler(current, entry, function() { done(); - }) + }); } }; diff --git a/src/js/fields/basic/ArrayField.js b/src/js/fields/basic/ArrayField.js index 9d9bc92f6..43258ae1f 100644 --- a/src/js/fields/basic/ArrayField.js +++ b/src/js/fields/basic/ArrayField.js @@ -644,7 +644,11 @@ var originalItemSchema = itemSchema; var originalItemOptions = itemOptions; - Alpaca.loadRefSchemaOptions(topField, schemaReferenceId, optionsReferenceId, function(itemSchema, itemOptions) { + var topConnector = topField.connector; + var topSchema = topField.schema; + var topOptions = topField.options; + + Alpaca.loadRefSchemaOptions(topSchema, topOptions, schemaReferenceId, optionsReferenceId, topConnector, function(err, itemSchema, itemOptions) { // walk the field chain to see if we have any circularity (for schema) var refCount = 0; diff --git a/src/js/fields/basic/ObjectField.js b/src/js/fields/basic/ObjectField.js index 3073d51f4..5c3bf8a3c 100644 --- a/src/js/fields/basic/ObjectField.js +++ b/src/js/fields/basic/ObjectField.js @@ -530,7 +530,11 @@ var originalPropertySchema = propertySchema; var originalPropertyOptions = propertyOptions; - Alpaca.loadRefSchemaOptions(topField, propertyReferenceId, fieldReferenceId, function(propertySchema, propertyOptions) { + var topConnector = topField.connector; + var topSchema = topField.schema; + var topOptions = topField.options; + + Alpaca.loadRefSchemaOptions(topSchema, topOptions, propertyReferenceId, fieldReferenceId, topConnector, function(err, propertySchema, propertyOptions) { // walk the field chain to see if we have any circularity (for schema) var refCount = 0;