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;