/** * @fileoverview A base class for EE Functions. */ goog.provide('ee.Function'); goog.require('ee.ComputedObject'); goog.require('ee.Encodable'); goog.require('ee.Serializer'); goog.require('ee.Types'); goog.require('ee.api'); goog.require('goog.array'); goog.require('goog.functions'); goog.require('goog.object'); goog.requireType('ee.data'); /** * An abstract base class for functions callable by the EE API. Subclasses * must implement encode() and getSignature(). * * TODO(user): Implement Function.bind() while supporting both positional * and named args and avoiding dependendency on CustomFunction. * * @constructor * @extends {ee.Encodable} */ ee.Function = function() { if (!(this instanceof ee.Function)) { return new ee.Function(); } }; goog.inherits(ee.Function, ee.Encodable); // Exporting manually to avoid marking the class public in the docs. goog.exportSymbol('ee.Function', ee.Function); /** * A function used to type-coerce arguments and return values. * @type {function(*, string): *} * @private */ ee.Function.promoter_ = goog.functions.identity; /** * Register a function used to type-coerce arguments and return values. * @param {function(*, string): *} promoter A function used to type-coerce * arguments and return values. Passed a value as the first parameter * and a type name as the second. Can be used, for example, promote * numbers or strings to Images. Should return the input promoted if * the type is recognized, otherwise the original input. */ ee.Function.registerPromoter = function(promoter) { ee.Function.promoter_ = promoter; }; /** * Returns a description of the interface provided by this function. * * @return {ee.Function.Signature} */ ee.Function.prototype.getSignature = goog.abstractMethod; /** * Call the function with the given positional arguments. * * @param {...*} var_args Positional arguments to pass to the function. * @return {!ee.ComputedObject} An object representing the called function. * If the signature specifies a recognized return type, the returned * value will be cast to that type. * @export */ ee.Function.prototype.call = function(var_args) { return this.apply(this.nameArgs(Array.prototype.slice.call(arguments, 0))); }; /** * Call the function with a dictionary of named arguments. * * @param {Object} namedArgs A dictionary of arguments to the function. * @return {!ee.ComputedObject} An object representing the lazy result of * the called function. If the signature specifies a recognized return * type, the returned value will be cast to that type. * @export */ ee.Function.prototype.apply = function(namedArgs) { const result = new ee.ComputedObject(this, this.promoteArgs(namedArgs)); return /** @type {!ee.ComputedObject} */( ee.Function.promoter_(result, this.getReturnType())); }; /** * Call the function automatically deciding whether to interpret the arguments * as positional ones or as a dictionary of named ones. * * @param {*|undefined} thisValue The "this" value on which the function was * called. If defined, interpreted as the first argument. * @param {!Array<*>} args A list containing either positional args or a * keyword arg dictionary. * @return {!ee.ComputedObject} An object representing the called function. * If the signature specifies a recognized return type, the returned * value will be cast to that type. * @package */ ee.Function.prototype.callOrApply = function(thisValue, args) { const isInstance = (thisValue !== undefined); const signature = this.getSignature(); // Convert positional to named args. let namedArgs; if (ee.Types.useKeywordArgs(args, signature, isInstance)) { namedArgs = goog.object.clone(/** @type {Object} */ (args[0])); if (isInstance) { const firstArgName = signature['args'][0]['name']; if (firstArgName in namedArgs) { throw Error('Named args for ' + signature['name'] + ' can\'t contain keyword ' + firstArgName); } namedArgs[firstArgName] = thisValue; } } else { namedArgs = this.nameArgs(isInstance ? [thisValue].concat(args) : args); } return this.apply(namedArgs); }; /** * Promotes arguments to their types based on the function's signature and * verifies that all required arguments are provided and no unknown arguments * are present. * * @param {Object} args Keyword arguments to the function. * @return {!Object} The promoted arguments. * @protected */ ee.Function.prototype.promoteArgs = function(args) { const specs = this.getSignature()['args']; // Promote all recognized args. const promotedArgs = {}; const known = {}; for (let i = 0; i < specs.length; i++) { const name = specs[i]['name']; if (name in args && args[name] !== undefined) { promotedArgs[name] = ee.Function.promoter_(args[name], specs[i]['type']); } else if (!specs[i]['optional']) { throw Error('Required argument (' + name + ') missing to function: ' + this); } known[name] = true; } // Check for unknown arguments. const unknown = []; for (const argName in args) { if (!known[argName]) { unknown.push(argName); } } if (unknown.length > 0) { throw Error('Unrecognized arguments (' + unknown + ') to function: ' + this); } return promotedArgs; }; /** * Converts a list of positional arguments to a map of keyword arguments * using the function's signature. Note that this does not check whether * the list contains enough arguments to satisfy the call. * * @param {Array} args Positional arguments to the function. * @return {!Object} Keyword arguments to the function. * @protected */ ee.Function.prototype.nameArgs = function(args) { const specs = this.getSignature()['args']; if (specs.length < args.length) { throw Error('Too many (' + args.length + ') arguments to function: ' + this); } const namedArgs = {}; for (let i = 0; i < args.length; i++) { namedArgs[specs[i]['name']] = args[i]; } return namedArgs; }; /** * @return {string} The name of the return type of the function. * @protected */ ee.Function.prototype.getReturnType = function() { return this.getSignature()['returns']; }; /** * @param {string=} opt_name An optional override of the function name. * @param {boolean=} opt_isInstance If true, the first argument is documented * to be "this". * @return {string} A user-friendly formatted description of the function based * on its signature. * @override */ ee.Function.prototype.toString = function(opt_name, opt_isInstance) { const signature = this.getSignature(); const buffer = []; buffer.push(opt_name || signature['name']); buffer.push('('); buffer.push(goog.array.map(signature['args'].slice(opt_isInstance ? 1 : 0), function(elem) { return elem['name']; }).join(', ')); buffer.push(')\n'); buffer.push('\n'); if (signature['description']) { buffer.push(signature['description']); } else { buffer.push('Undocumented.'); } buffer.push('\n'); if (signature['args'].length) { buffer.push('\nArgs:\n'); for (let i = 0; i < signature['args'].length; i++) { if (opt_isInstance && i == 0) { buffer.push(' this:'); } else { buffer.push('\n '); } const arg = signature['args'][i]; buffer.push(arg['name']); buffer.push(' ('); buffer.push(arg['type']); if (arg['optional']) { buffer.push(', optional'); } buffer.push('): '); if (arg['description']) { buffer.push(arg['description']); } else { buffer.push('Undocumented.'); } } } return buffer.join(''); }; /** * @param {boolean=} legacy Enables legacy format. * @return {string} The serialized representation of this object. */ ee.Function.prototype.serialize = function(legacy = false) { return legacy ? ee.Serializer.toJSON(this) : ee.Serializer.toCloudApiJSON(this); }; /** * The signature of a function. * * @typedef {{ * name: string, * args: !Array., * returns: string, * description: (string|undefined), * deprecated: (string|undefined), * preview: (boolean|undefined), * sourceCodeUri: (string|undefined), * }} */ ee.Function.Signature; /** * Encodes the function in a format compatible with ee.Serializer. * @param {!ee.Encodable.Serializer} * serializer An object that can be used to encode a serializable object. * @param {!Object} args * @return {!ee.api.ValueNode} The encoded object. */ ee.Function.prototype.encodeCloudInvocation = goog.abstractMethod;