infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
697 lines (629 loc) • 31.1 kB
JavaScript
/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/main/AUTHORS.md.
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt
*/
;
fluid.registerNamespace("fluid.model.transform");
/** Grade definitions for standard transformation function hierarchy **/
fluid.defaults("fluid.transformFunction", {
gradeNames: "fluid.function"
});
// uses standard layout and workflow involving inputPath - an undefined input value
// will short-circuit the evaluation
fluid.defaults("fluid.standardInputTransformFunction", {
gradeNames: "fluid.transformFunction"
});
fluid.defaults("fluid.standardOutputTransformFunction", {
gradeNames: "fluid.transformFunction"
});
// defines a set of options "inputVariables" referring to its inputs, which are converted
// to functions that the transform may explicitly use to demand the input value
fluid.defaults("fluid.multiInputTransformFunction", {
gradeNames: "fluid.transformFunction"
});
// uses the standard layout and workflow involving inputPath and outputPath
fluid.defaults("fluid.standardTransformFunction", {
gradeNames: ["fluid.standardInputTransformFunction", "fluid.standardOutputTransformFunction"]
});
fluid.defaults("fluid.lens", {
gradeNames: "fluid.transformFunction",
invertConfiguration: null
// this function method returns "inverted configuration" rather than actually performing inversion
// TODO: harmonise with strategy used in VideoPlayer_framework.js
});
/***********************************
* Base utilities for transformers *
***********************************/
// unsupported, NON-API function
fluid.model.transform.pathToRule = function (inputPath) {
return {
transform: {
type: "fluid.transforms.value",
inputPath: inputPath
}
};
};
// unsupported, NON-API function
fluid.model.transform.literalValueToRule = function (input) {
return {
transform: {
type: "fluid.transforms.literalValue",
input: input
}
};
};
/* Accepts two fully escaped paths, either of which may be empty or null */
fluid.model.composePaths = function (prefix, suffix) {
prefix = prefix === 0 ? "0" : prefix || "";
suffix = suffix === 0 ? "0" : suffix || "";
return !prefix ? suffix : (!suffix ? prefix : prefix + "." + suffix);
};
fluid.model.transform.accumulateInputPath = function (inputPath, transformer, paths) {
if (inputPath !== undefined) {
paths.push(fluid.model.composePaths(transformer.inputPrefix, inputPath));
}
};
fluid.model.transform.accumulateStandardInputPath = function (input, transformSpec, transformer, paths) {
fluid.model.transform.getValue(undefined, transformSpec[input], transformer);
fluid.model.transform.accumulateInputPath(transformSpec[input + "Path"], transformer, paths);
};
fluid.model.transform.accumulateMultiInputPaths = function (inputVariables, transformSpec, transformer, paths) {
fluid.each(inputVariables, function (v, k) {
fluid.model.transform.accumulateStandardInputPath(k, transformSpec, transformer, paths);
});
};
fluid.model.transform.getValue = function (inputPath, value, transformer) {
var togo;
if (inputPath !== undefined) { // NB: We may one day want to reverse the crazy jQuery-like convention that "no path means root path"
togo = fluid.get(transformer.source, fluid.model.composePaths(transformer.inputPrefix, inputPath), transformer.resolverGetConfig);
}
if (togo === undefined) {
// FLUID-5867 - actually helpful behaviour here rather than the insane original default of expecting a short-form value document
togo = fluid.isPrimitive(value) ? value :
("literalValue" in value ? value.literalValue :
(value.transform === undefined ? value : transformer.expand(value)));
}
return togo;
};
// distinguished value which indicates that a transformation rule supplied a
// non-default output path, and so the user should be prevented from making use of it
// in a compound transform definition
fluid.model.transform.NONDEFAULT_OUTPUT_PATH_RETURN = {};
fluid.model.transform.setValue = function (userOutputPath, value, transformer) {
// avoid crosslinking to input object - this might be controlled by a "nocopy" option in future
var toset = fluid.copy(value);
var outputPath = fluid.model.composePaths(transformer.outputPrefix, userOutputPath);
// TODO: custom resolver config here to create non-hash output model structure
if (toset !== undefined) {
transformer.applier.change(outputPath, toset);
}
return userOutputPath ? fluid.model.transform.NONDEFAULT_OUTPUT_PATH_RETURN : toset;
};
/* Resolves the <key> given as parameter by looking up the path <key>Path in the object
* to be transformed. If not present, it resolves the <key> by using the literal value if primitive,
* or expanding otherwise. <def> defines the default value if unableto resolve the key. If no
* default value is given undefined is returned
*/
fluid.model.transform.resolveParam = function (transformSpec, transformer, key, def) {
var val = fluid.model.transform.getValue(transformSpec[key + "Path"], transformSpec[key], transformer);
return (val !== undefined) ? val : def;
};
// Compute a "match score" between two pieces of model material, with 0 indicating a complete mismatch, and
// higher values indicating increasingly good matches
fluid.model.transform.matchValue = function (expected, actual, partialMatches) {
var stats = {changes: 0, unchanged: 0, changeMap: {}};
fluid.model.diff(expected, actual, stats);
// i) a pair with 0 matches counts for 0 in all cases
// ii) without "partial match mode" (the default), we simply count matches, with any mismatch giving 0
// iii) with "partial match mode", a "perfect score" in the top 24 bits is
// penalised for each mismatch, with a positive score of matches store in the bottom 24 bits
return stats.unchanged === 0 ? 0 :
(partialMatches ? 0xffffff000000 - 0x1000000 * stats.changes + stats.unchanged :
(stats.changes ? 0 : 0xffffff000000 + stats.unchanged));
};
fluid.model.transform.invertPaths = function (transformSpec, transformer) {
// TODO: this will not behave correctly in the face of compound "input" which contains
// further transforms
var oldOutput = fluid.model.composePaths(transformer.outputPrefix, transformSpec.outputPath);
transformSpec.outputPath = fluid.model.composePaths(transformer.inputPrefix, transformSpec.inputPath);
transformSpec.inputPath = oldOutput;
return transformSpec;
};
// TODO: prefixApplier is a transform which is currently unused and untested
fluid.model.transform.prefixApplier = function (transformSpec, transformer) {
if (transformSpec.inputPrefix) {
transformer.inputPrefixOp.push(transformSpec.inputPrefix);
}
if (transformSpec.outputPrefix) {
transformer.outputPrefixOp.push(transformSpec.outputPrefix);
}
transformer.expand(transformSpec.input);
if (transformSpec.inputPrefix) {
transformer.inputPrefixOp.pop();
}
if (transformSpec.outputPrefix) {
transformer.outputPrefixOp.pop();
}
};
fluid.defaults("fluid.model.transform.prefixApplier", {
gradeNames: ["fluid.transformFunction"]
});
// unsupported, NON-API function
fluid.model.makePathStack = function (transform, prefixName) {
var stack = transform[prefixName + "Stack"] = [];
transform[prefixName] = "";
return {
push: function (prefix) {
var newPath = fluid.model.composePaths(transform[prefixName], prefix);
stack.push(transform[prefixName]);
transform[prefixName] = newPath;
},
pop: function () {
transform[prefixName] = stack.pop();
}
};
};
// unsupported, NON-API function
fluid.model.transform.doTransform = function (transformSpec, transformer, transformOpts) {
var expdef = transformOpts.defaults;
var transformFn = fluid.getGlobalValue(transformOpts.typeName);
if (typeof(transformFn) !== "function") {
fluid.fail("Transformation record specifies transformation function with name " +
transformSpec.type + " which is not a function - ", transformFn);
}
if (!fluid.hasGrade(expdef, "fluid.transformFunction")) {
// If no suitable grade is set up, assume that it is intended to be used as a standardTransformFunction
expdef = fluid.defaults("fluid.standardTransformFunction");
}
var transformArgs = [transformSpec, transformer];
if (fluid.hasGrade(expdef, "fluid.multiInputTransformFunction")) {
var inputs = {};
fluid.each(expdef.inputVariables, function (v, k) {
inputs[k] = function () {
var input = fluid.model.transform.getValue(transformSpec[k + "Path"], transformSpec[k], transformer);
// TODO: This is a mess, null might perfectly well be a possible default
// if no match, assign default if one exists (v != null)
input = (input === undefined && v !== null) ? v : input;
return input;
};
});
transformArgs.unshift(inputs);
}
if (fluid.hasGrade(expdef, "fluid.standardInputTransformFunction")) {
if (!("input" in transformSpec) && !("inputPath" in transformSpec)) {
fluid.fail("Error in transform specification. Either \"input\" or \"inputPath\" must be specified for a standardInputTransformFunction: received ", transformSpec);
}
var expanded = fluid.model.transform.getValue(transformSpec.inputPath, transformSpec.input, transformer);
transformArgs.unshift(expanded);
// if the function has no input, the result is considered undefined, and this is returned
if (expanded === undefined) {
return undefined;
}
}
var transformed = transformFn.apply(null, transformArgs);
if (fluid.hasGrade(expdef, "fluid.standardOutputTransformFunction")) {
// "doOutput" flag is currently set nowhere, but could be used in future
var outputPath = transformSpec.outputPath !== undefined ? transformSpec.outputPath : (transformOpts.doOutput ? "" : undefined);
if (outputPath !== undefined && transformed !== undefined) {
//If outputPath is given in the expander we want to:
// (1) output to the document
// (2) return undefined, to ensure that expanders higher up in the hierarchy doesn't attempt to output it again
fluid.model.transform.setValue(transformSpec.outputPath, transformed, transformer);
transformed = undefined;
}
}
return transformed;
};
// OLD PATHUTIL utilities: Rescued from old DataBinding implementation to support obsolete "schema" scheme for transforms - all of this needs to be rethought
var globalAccept = [];
fluid.registerNamespace("fluid.pathUtil");
/* Parses a path segment, following escaping rules, starting from character index i in the supplied path */
fluid.pathUtil.getPathSegment = function (path, i) {
fluid.pathUtil.getPathSegmentImpl(globalAccept, path, i);
return globalAccept[0];
};
/* Returns just the head segment of an EL path */
fluid.pathUtil.getHeadPath = function (path) {
return fluid.pathUtil.getPathSegment(path, 0);
};
/* Returns all of an EL path minus its first segment - if the path consists of just one segment, returns "" */
fluid.pathUtil.getFromHeadPath = function (path) {
var firstdot = fluid.pathUtil.getPathSegmentImpl(null, path, 0);
return firstdot === path.length ? "" : path.substring(firstdot + 1);
};
/** Determines whether a particular EL path matches a given path specification.
* The specification consists of a path with optional wildcard segments represented by "*".
* @param {String} spec - The specification to be matched
* @param {String} path - The path to be tested
* @param {Boolean} exact - Whether the path must exactly match the length of the specification in
* terms of path segments in order to count as match. If exact is falsy, short specifications will
* match all longer paths as if they were padded out with "*" segments
* @return {Array|null} - An array of {String} path segments which matched the specification, or <code>null</code> if there was no match.
*/
fluid.pathUtil.matchPath = function (spec, path, exact) {
var togo = [];
while (true) {
if (((path === "") ^ (spec === "")) && exact) {
return null;
}
// FLUID-4625 - symmetry on spec and path is actually undesirable, but this
// quickly avoids at least missed notifications - improved (but slower)
// implementation should explode composite changes
if (!spec || !path) {
break;
}
var spechead = fluid.pathUtil.getHeadPath(spec);
var pathhead = fluid.pathUtil.getHeadPath(path);
// if we fail to match on a specific component, fail.
if (spechead !== "*" && spechead !== pathhead) {
return null;
}
togo.push(pathhead);
spec = fluid.pathUtil.getFromHeadPath(spec);
path = fluid.pathUtil.getFromHeadPath(path);
}
return togo;
};
// unsupported, NON-API function
fluid.model.transform.expandWildcards = function (transformer, source) {
fluid.each(source, function (value, key) {
var q = transformer.queuedTransforms;
transformer.pathOp.push(fluid.pathUtil.escapeSegment(key.toString()));
for (var i = 0; i < q.length; ++i) {
if (fluid.pathUtil.matchPath(q[i].matchPath, transformer.path, true)) {
var esCopy = fluid.copy(q[i].transformSpec);
if (esCopy.inputPath === undefined || fluid.model.transform.hasWildcard(esCopy.inputPath)) {
esCopy.inputPath = "";
}
// TODO: allow some kind of interpolation for output path
// TODO: Also, we now require outputPath to be specified in these cases for output to be produced as well.. Is that something we want to continue with?
transformer.inputPrefixOp.push(transformer.path);
transformer.outputPrefixOp.push(transformer.path);
var transformOpts = fluid.model.transform.lookupType(esCopy.type);
var result = fluid.model.transform.doTransform(esCopy, transformer, transformOpts);
if (result !== undefined) {
fluid.model.transform.setValue(null, result, transformer);
}
transformer.outputPrefixOp.pop();
transformer.inputPrefixOp.pop();
}
}
if (!fluid.isPrimitive(value)) {
fluid.model.transform.expandWildcards(transformer, value);
}
transformer.pathOp.pop();
});
};
// unsupported, NON-API function
fluid.model.transform.hasWildcard = function (path) {
return typeof(path) === "string" && path.indexOf("*") !== -1;
};
// unsupported, NON-API function
fluid.model.transform.maybePushWildcard = function (transformSpec, transformer) {
var hw = fluid.model.transform.hasWildcard;
var matchPath;
if (hw(transformSpec.inputPath)) {
matchPath = fluid.model.composePaths(transformer.inputPrefix, transformSpec.inputPath);
}
else if (hw(transformer.outputPrefix) || hw(transformSpec.outputPath)) {
matchPath = fluid.model.composePaths(transformer.outputPrefix, transformSpec.outputPath);
}
if (matchPath) {
transformer.queuedTransforms.push({transformSpec: transformSpec, outputPrefix: transformer.outputPrefix, inputPrefix: transformer.inputPrefix, matchPath: matchPath});
return true;
}
return false;
};
fluid.model.sortByKeyLength = function (inObject) {
var keys = fluid.keys(inObject);
return keys.sort(fluid.compareStringLength(true));
};
// Three handler functions operating the (currently) three different processing modes
// unsupported, NON-API function
fluid.model.transform.handleTransformStrategy = function (transformSpec, transformer, transformOpts) {
if (fluid.model.transform.maybePushWildcard(transformSpec, transformer)) {
return;
}
else {
return fluid.model.transform.doTransform(transformSpec, transformer, transformOpts);
}
};
// unsupported, NON-API function
fluid.model.transform.handleInvertStrategy = function (transformSpec, transformer, transformOpts) {
transformSpec = fluid.copy(transformSpec);
// if we have a standardTransformFunction we can switch input and output arguments:
if (fluid.hasGrade(transformOpts.defaults, "fluid.standardTransformFunction")) {
transformSpec = fluid.model.transform.invertPaths(transformSpec, transformer);
}
var invertor = transformOpts.defaults && transformOpts.defaults.invertConfiguration;
if (invertor) {
var inverted = fluid.invokeGlobalFunction(invertor, [transformSpec, transformer]);
transformer.inverted.push(inverted);
} else {
transformer.inverted.push(fluid.model.transform.uninvertibleTransform);
}
};
// unsupported, NON-API function
fluid.model.transform.handleCollectStrategy = function (transformSpec, transformer, transformOpts) {
var defaults = transformOpts.defaults;
var standardInput = fluid.hasGrade(defaults, "fluid.standardInputTransformFunction");
var multiInput = fluid.hasGrade(defaults, "fluid.multiInputTransformFunction");
if (standardInput) {
fluid.model.transform.accumulateStandardInputPath("input", transformSpec, transformer, transformer.inputPaths);
}
if (multiInput) {
fluid.model.transform.accumulateMultiInputPaths(defaults.inputVariables, transformSpec, transformer, transformer.inputPaths);
}
var collector = defaults.collectInputPaths;
if (collector) {
var collected = fluid.makeArray(fluid.invokeGlobalFunction(collector, [transformSpec, transformer]));
Array.prototype.push.apply(transformer.inputPaths, collected); // push all elements of collected onto inputPaths
}
};
fluid.model.transform.lookupType = function (typeName, transformSpec) {
if (!typeName) {
fluid.fail("Transformation record is missing a type name: ", transformSpec);
}
if (typeName.indexOf(".") === -1) {
typeName = "fluid.transforms." + typeName;
}
var defaults = fluid.defaults(typeName);
return { defaults: defaults, typeName: typeName};
};
// unsupported, NON-API function
fluid.model.transform.processRule = function (rule, transformer) {
if (typeof(rule) === "string") {
rule = fluid.model.transform.pathToRule(rule);
}
// special dispensation to allow "literalValue" to escape any value
else if (rule.literalValue !== undefined) {
rule = fluid.model.transform.literalValueToRule(rule.literalValue);
}
var togo;
if (rule.transform) {
var transformSpec, transformOpts;
if (fluid.isArrayable(rule.transform)) {
// if the transform holds an array, each transformer within that is responsible for its own output
var transforms = rule.transform;
togo = undefined;
for (var i = 0; i < transforms.length; ++i) {
transformSpec = transforms[i];
transformOpts = fluid.model.transform.lookupType(transformSpec.type);
transformer.transformHandler(transformSpec, transformer, transformOpts);
}
} else {
// else we just have a normal single transform which will return 'undefined' as a flag to defeat cascading output
transformSpec = rule.transform;
transformOpts = fluid.model.transform.lookupType(transformSpec.type);
togo = transformer.transformHandler(transformSpec, transformer, transformOpts);
}
}
// if rule is an array, save path for later use in schema strategy on final applier (so output will be interpreted as array)
if (fluid.isArrayable(rule)) {
transformer.collectedFlatSchemaOpts = transformer.collectedFlatSchemaOpts || {};
transformer.collectedFlatSchemaOpts[transformer.outputPrefix] = "array";
}
fluid.each(rule, function (value, key) {
if (key !== "transform") {
transformer.outputPrefixOp.push(key);
var togo = transformer.expand(value, transformer);
// Value expanders and arrays as rules implicitly output, unless they have nothing (undefined) to output
if (togo !== undefined) {
fluid.model.transform.setValue(null, togo, transformer);
// ensure that expanders further up does not try to output this value as well.
togo = undefined;
}
transformer.outputPrefixOp.pop();
}
});
return togo;
};
// unsupported, NON-API function
// 3rd arg is disused by the framework and always defaults to fluid.model.transform.processRule
fluid.model.transform.makeStrategy = function (transformer, handleFn, transformFn) {
transformFn = transformFn || fluid.model.transform.processRule;
transformer.expand = function (rules) {
return transformFn(rules, transformer);
};
transformer.outputPrefixOp = fluid.model.makePathStack(transformer, "outputPrefix");
transformer.inputPrefixOp = fluid.model.makePathStack(transformer, "inputPrefix");
transformer.transformHandler = handleFn;
};
/* A special, empty, transform document representing the inversion of a transformation which does not not have an inverse
*/
fluid.model.transform.uninvertibleTransform = Object.freeze({});
/** Accepts a transformation document, and returns its inverse if all of its constituent transforms have inverses
* defined via their individual invertConfiguration functions, or else `fluid.model.transform.uninvertibleTransform`
* if any of them do not.
* Note that this algorithm will give faulty results in many cases of compound transformation documents.
* @param {Transform} rules - The model transformation document to be inverted
* @return {Transform} The inverse transformation document if it can be computed easily, or
* `fluid.model.transform.uninvertibleTransform` if it is clear that it cannot.
*/
fluid.model.transform.invertConfiguration = function (rules) {
var transformer = {
inverted: []
};
fluid.model.transform.makeStrategy(transformer, fluid.model.transform.handleInvertStrategy);
transformer.expand(rules);
var invertible = transformer.inverted.indexOf(fluid.model.transform.uninvertibleTransform) === -1;
return invertible ? {
transform: transformer.inverted
} : fluid.model.transform.uninvertibleTransform;
};
/** Compute the paths which will be read from the input document of the supplied transformation if it were operated.
*
* @param {Transform} rules - The transformation for which the input paths are to be computed.
* @return {Array} - An array of paths which will be read by the document.
*/
fluid.model.transform.collectInputPaths = function (rules) {
var transformer = {
inputPaths: []
};
fluid.model.transform.makeStrategy(transformer, fluid.model.transform.handleCollectStrategy);
transformer.expand(rules);
// Deduplicate input paths
var inputPathHash = fluid.arrayToHash(transformer.inputPaths);
return Object.keys(inputPathHash);
};
// unsupported, NON-API function
fluid.model.transform.flatSchemaStrategy = function (flatSchema, getConfig) {
var keys = fluid.model.sortByKeyLength(flatSchema);
return function (root, segment, index, segs) {
var path = getConfig.parser.compose.apply(null, segs.slice(0, index));
// TODO: clearly this implementation could be much more efficient
for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
if (fluid.pathUtil.matchPath(key, path, true) !== null) {
return flatSchema[key];
}
}
};
};
// unsupported, NON-API function
fluid.model.transform.defaultSchemaValue = function (schemaValue) {
var type = fluid.isPrimitive(schemaValue) ? schemaValue : schemaValue.type;
return type === "array" ? [] : {};
};
// unsupported, NON-API function
fluid.model.transform.isomorphicSchemaStrategy = function (source, getConfig) {
return function (root, segment, index, segs) {
var existing = fluid.get(source, segs.slice(0, index), getConfig);
return fluid.isArrayable(existing) ? "array" : "object";
};
};
// unsupported, NON-API function
fluid.model.transform.decodeStrategy = function (source, options, getConfig) {
if (options.isomorphic) {
return fluid.model.transform.isomorphicSchemaStrategy(source, getConfig);
}
else if (options.flatSchema) {
return fluid.model.transform.flatSchemaStrategy(options.flatSchema, getConfig);
}
};
// unsupported, NON-API function
fluid.model.transform.schemaToCreatorStrategy = function (strategy) {
return function (root, segment, index, segs) {
if (root[segment] === undefined) {
var schemaValue = strategy(root, segment, index, segs);
root[segment] = fluid.model.transform.defaultSchemaValue(schemaValue);
return root[segment];
}
};
};
/* Transforms a model by a sequence of rules. Parameters as for fluid.model.transform,
* only with an array accepted for "rules"
*/
fluid.model.transform.sequence = function (source, rules, options) {
for (var i = 0; i < rules.length; ++i) {
source = fluid.model.transform(source, rules[i], options);
}
return source;
};
fluid.model.compareByPathLength = function (changea, changeb) {
var pdiff = changea.path.length - changeb.path.length;
return pdiff === 0 ? changea.sequence - changeb.sequence : pdiff;
};
/* Fires an accumulated set of change requests in increasing order of target pathlength */
fluid.model.fireSortedChanges = function (changes, applier) {
changes.sort(fluid.model.compareByPathLength);
fluid.fireChanges(applier, changes);
};
/**
* Transforms a model based on a specified expansion rules objects.
* Rules objects take the form of:
* {
* "target.path": "value.el.path" || {
* transform: {
* type: "transform.function.path",
* ...
* }
* }
* }
*
* @param {Object} source - the model to transform
* @param {Object} rules - a rules object containing instructions on how to transform the model
* @param {Object} options - a set of rules governing the transformations. At present this may contain
* the values:
* @param {Boolean} options.isomorphic - `true` indicating that the output model is to be governed by the
* same schema found in the input model
* @param {Object} options.flatSchema - holding a flat schema object which
* consists of a hash of EL path specifications with wildcards, to the values "array"/"object" defining
* the schema to be used to construct missing trunk values.
* @param {ChangeApplier} options.finalApplier - A changeApplier to receive changes caused by output values
* @param {Object} options.oldTarget - A target model resulting from a previous round of updates
* @param {Object} options.oldSource - The source model which led output to `options.oldTarget`. Currently only read
* by specialised transforms such as fluid.transforms.toggle
* @return {Any} The transformed model.
*/
fluid.model.transformWithRules = function (source, rules, options) {
options = options || {};
var getConfig = fluid.model.escapedGetConfig;
var setConfig = fluid.model.escapedSetConfig;
var schemaStrategy = fluid.model.transform.decodeStrategy(source, options, getConfig);
var transformer = {
source: source,
target: {
// TODO: This should default to undefined to allow return of primitives, etc.
model: schemaStrategy ? fluid.model.transform.defaultSchemaValue(schemaStrategy(null, "", 0, [""])) : {}
},
oldSource: options.oldSource,
oldTarget: options.oldTarget,
resolverGetConfig: getConfig,
resolverSetConfig: setConfig,
collectedFlatSchemaOpts: undefined, // to hold options for flat schema collected during transforms
queuedChanges: [],
queuedTransforms: [] // TODO: This is used only by wildcard applier - explain its operation
};
fluid.model.transform.makeStrategy(transformer, fluid.model.transform.handleTransformStrategy);
transformer.applier = {
fireChangeRequest: function (changeRequest) {
changeRequest.sequence = transformer.queuedChanges.length;
transformer.queuedChanges.push(changeRequest);
}
};
fluid.bindRequestChange(transformer.applier);
transformer.expand(rules);
var rootSetConfig = fluid.copy(setConfig);
// Modify schemaStrategy if we collected flat schema options for the setConfig of finalApplier
if (transformer.collectedFlatSchemaOpts !== undefined) {
$.extend(transformer.collectedFlatSchemaOpts, options.flatSchema);
schemaStrategy = fluid.model.transform.flatSchemaStrategy(transformer.collectedFlatSchemaOpts, getConfig);
}
rootSetConfig.strategies = [fluid.model.defaultFetchStrategy, schemaStrategy ? fluid.model.transform.schemaToCreatorStrategy(schemaStrategy) :
fluid.model.defaultCreatorStrategy];
transformer.finalApplier = options.finalApplier || fluid.makeHolderChangeApplier(transformer.target, {resolverSetConfig: rootSetConfig});
if (transformer.queuedTransforms.length > 0) {
transformer.typeStack = [];
transformer.pathOp = fluid.model.makePathStack(transformer, "path");
fluid.model.transform.expandWildcards(transformer, source);
}
fluid.model.fireSortedChanges(transformer.queuedChanges, transformer.finalApplier);
return transformer.target.model;
};
$.extend(fluid.model.transformWithRules, fluid.model.transform);
fluid.model.transform = fluid.model.transformWithRules;
/* Utility function to produce a standard options transformation record for a single set of rules */
fluid.transformOne = function (rules) {
return {
transformOptions: {
transformer: "fluid.model.transformWithRules",
config: rules
}
};
};
/* Utility function to produce a standard options transformation record for multiple rules to be applied in sequence */
fluid.transformMany = function (rules) {
return {
transformOptions: {
transformer: "fluid.model.transform.sequence",
config: rules
}
};
};