From 28b96752b90d4a9e58875cdf44bdb7bfcf624ce6 Mon Sep 17 00:00:00 2001
From: RTLcoil <oleg@rtl.co.il>
Date: Thu, 25 Apr 2019 19:46:08 +0300
Subject: [PATCH] Add support for remote/local function invocation (fn:remote
 and fn:wasm)

---
 lib-es5/utils/index.js  | 19 +++++++++++
 lib/utils/index.js      | 19 +++++++++++
 test/cloudinary_spec.js | 72 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 110 insertions(+)

diff --git a/lib-es5/utils/index.js b/lib-es5/utils/index.js
index e9498c1d..7ffa24e4 100644
--- a/lib-es5/utils/index.js
+++ b/lib-es5/utils/index.js
@@ -210,6 +210,23 @@ function normalize_expression(expression) {
   return expression.replace(/[ _]+/g, '_');
 }
 
+/**
+ * Parse custom_function options
+ * @private
+ * @param {object|*} customFunction a custom function object containing function_type and source values
+ * @return {string|*} custom_function transformation string
+ */
+function process_custom_function(customFunction) {
+  if (!isObject(customFunction)) {
+    return customFunction;
+  }
+  if (customFunction.function_type === "remote") {
+    return [customFunction.function_type, base64EncodeURL(customFunction.source)].join(":");
+  } else {
+    return [customFunction.function_type, customFunction.source].join(":");
+  }
+}
+
 /**
  * Parse "if" parameter
  * Translates the condition if provided.
@@ -546,6 +563,7 @@ exports.generate_transformation_string = function generate_transformation_string
   var underlay = process_layer(utils.option_consume(options, "underlay"));
   var ifValue = process_if(utils.option_consume(options, "if"));
   var fps = utils.option_consume(options, 'fps');
+  var custom_function = process_custom_function(utils.option_consume(options, "custom_function"));
   if (isArray(fps)) {
     fps = fps.join('-');
   }
@@ -559,6 +577,7 @@ exports.generate_transformation_string = function generate_transformation_string
     dpr: normalize_expression(dpr),
     e: normalize_expression(effect),
     fl: flags,
+    fn: custom_function,
     fps: fps,
     h: normalize_expression(height),
     ki: normalize_expression(utils.option_consume(options, "keyframe_interval")),
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 4c09f498..19ce7e8b 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -206,6 +206,23 @@ function normalize_expression(expression) {
   return expression.replace(/[ _]+/g, '_');
 }
 
+/**
+ * Parse custom_function options
+ * @private
+ * @param {object|*} customFunction a custom function object containing function_type and source values
+ * @return {string|*} custom_function transformation string
+ */
+function process_custom_function(customFunction) {
+  if (!isObject(customFunction)) {
+    return customFunction;
+  }
+  if (customFunction.function_type === "remote") {
+    return [customFunction.function_type, base64EncodeURL(customFunction.source)].join(":");
+  } else {
+    return [customFunction.function_type, customFunction.source].join(":");
+  }
+}
+
 /**
  * Parse "if" parameter
  * Translates the condition if provided.
@@ -553,6 +570,7 @@ exports.generate_transformation_string = function generate_transformation_string
   let underlay = process_layer(utils.option_consume(options, "underlay"));
   let ifValue = process_if(utils.option_consume(options, "if"));
   let fps = utils.option_consume(options, 'fps');
+  let custom_function = process_custom_function(utils.option_consume(options, "custom_function"));
   if(isArray(fps)){
     fps = fps.join('-');
   }
@@ -566,6 +584,7 @@ exports.generate_transformation_string = function generate_transformation_string
     dpr: normalize_expression(dpr),
     e: normalize_expression(effect),
     fl: flags,
+    fn: custom_function,
     fps: fps,
     h: normalize_expression(height),
     ki: normalize_expression(utils.option_consume(options, "keyframe_interval")),
diff --git a/test/cloudinary_spec.js b/test/cloudinary_spec.js
index 8120be8d..22f75599 100644
--- a/test/cloudinary_spec.js
+++ b/test/cloudinary_spec.js
@@ -423,6 +423,78 @@ describe("cloudinary", function() {
       signature: "123515adfa151"
     })).to.eql("image/upload/v1251251251/abcd.jpg#123515adfa151");
   });
+  it('should support custom function of type wasm with a source', function () {
+    var options, result;
+    options = {
+      custom_function: {function_type: 'wasm', source: 'blur.wasm'}
+    };
+    result = cloudinary.utils.url("test", options);
+    expect(options).to.eql({});
+    expect(result).to.eql("http://res.cloudinary.com/test123/image/upload/fn_wasm:blur.wasm/test");
+  });
+  it('should support arbitrary custom function types', function () {
+    var options, result;
+    options = {
+      custom_function: {function_type: 'amazing', source: 'awesome'}
+    };
+    result = cloudinary.utils.url("test", options);
+    expect(options).to.eql({});
+    expect(result).to.eql("http://res.cloudinary.com/test123/image/upload/fn_amazing:awesome/test");
+  });
+  it('should support custom function with no source', function () {
+    var options, result;
+    options = {
+      custom_function: {function_type: 'wasm'}
+    };
+    result = cloudinary.utils.url("test", options);
+    expect(options).to.eql({});
+    expect(result).to.eql("http://res.cloudinary.com/test123/image/upload/fn_wasm:/test");
+  });
+  it('should support custom function with no function_type', function () {
+    var options, result;
+    options = {
+      custom_function:  {source: 'blur.wasm'}
+    };
+    result = cloudinary.utils.url("test", options);
+    expect(options).to.eql({});
+    expect(result).to.eql("http://res.cloudinary.com/test123/image/upload/fn_:blur.wasm/test");
+  });
+  it('should support custom function that is not an object', function () {
+    var options, result;
+    options = {
+      custom_function: []
+    };
+    result = cloudinary.utils.url("test", options);
+    expect(options).to.eql({});
+    expect(result).to.eql("http://res.cloudinary.com/test123/image/upload/fn_:/test");
+  });
+  it('should support custom function with no function_type or source', function () {
+    var options, result;
+    options = {
+      custom_function: {}
+    };
+    result = cloudinary.utils.url("test", options);
+    expect(options).to.eql({});
+    expect(result).to.eql("http://res.cloudinary.com/test123/image/upload/fn_:/test");
+  });
+  it('should support custom function of type remote', function() {
+    var options, result;
+    options = {
+      custom_function: {function_type: 'remote', source: 'https://df34ra4a.execute-api.us-west-2.amazonaws.com/default/cloudinaryFunction'}
+    };
+    result = cloudinary.utils.url("test", options);
+    expect(options).to.eql({});
+    expect(result).to.eql("http://res.cloudinary.com/test123/image/upload/fn_remote:aHR0cHM6Ly9kZjM0cmE0YS5leGVjdXRlLWFwaS51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9kZWZhdWx0L2Nsb3VkaW5hcnlGdW5jdGlvbg==/test");
+  });
+  it('should should not include custom function with undefined value', function() {
+    var options, result;
+    options = {
+      custom_function: undefined
+    };
+    result = cloudinary.utils.url("test", options);
+    expect(options).to.eql({});
+    expect(result).to.eql("http://res.cloudinary.com/test123/image/upload/test");
+  });
   it("should support density", function() {
     var options, result;
     options = {