Repository: jsonata-js/jsonata
Pull request #357 (2019-08-26 14:13:25)

Add URL/URI Helper Functions
As discussed in #103

jhorbulyk: @andrew-coleman Do you have any preference for one single jsonata error code or should I add an error code for each of the 4 functions?
andrew-coleman: One error code is fine.  You could add the function name as an insert into that error.  Thanks.
jhorbulyk: @andrew-coleman Updated.

Patch:
diff --git a/docs/string-functions.md b/docs/string-functions.md
index 50b1142..36b9690 100644
--- a/docs/string-functions.md
+++ b/docs/string-functions.md
@@ -293,3 +293,39 @@ Converts base 64 encoded bytes to a string, using a UTF-8 Unicode codepage.
 __Examples__
 
 - `$base64decode("bXl1c2VyOm15cGFzcw==")` => `"myuser:mypass"`
+
+## `$encodeUrlComponent()`
+__Signature:__ `$encodeUrlComponent(str)`
+
+Encodes a Uniform Resource Locator (URL) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.
+
+__Examples__
+
+- `$encodeUrlComponent("?x=test")` => `"%3Fx%3Dtest"`
+
+## `$encodeUrl()`
+__Signature:__ `$encodeUrl(str)`
+
+Encodes a Uniform Resource Locator (URL) by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.
+
+__Examples__
+
+- `$encodeUrl("https://mozilla.org/?x=шеллы")` => `"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"`
+
+## `$decodeUrlComponent()`
+__Signature:__ `$decodeUrlComponent(str)`
+
+Decodes a Uniform Resource Locator (URL) component previously created by encodeUrlComponent.
+
+__Examples__
+
+- `$decodeUrlComponent("%3Fx%3Dtest")` => `"?x=test"`
+
+## `$decodeUrl()`
+__Signature:__ `$decodeUrl(str)`
+
+Decodes a Uniform Resource Locator (URL) previously created by encodeUrl.
+
+__Examples__
+
+- `$decodeUrl("https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B")` => `"https://mozilla.org/?x=шеллы"`
diff --git a/src/functions.js b/src/functions.js
index c44a6d7..69cd32f 100644
--- a/src/functions.js
+++ b/src/functions.js
@@ -580,6 +580,110 @@ const functions = (() => {
         return atob(str);
     }
 
+    /**
+     * Encode a string into a component for a url
+     * @param {String} str - String to encode
+     * @returns {string} Encoded string
+     */
+    function encodeUrlComponent(str) {
+        // undefined inputs always return undefined
+        if (typeof str === 'undefined') {
+            return undefined;
+        }
+
+        // Catch URIErrors when URI sequence is malformed
+        var returnVal;
+        try {
+            returnVal = encodeURIComponent(str);
+        } catch (e) {
+            throw {
+                code: "D3140",
+                stack: (new Error()).stack,
+                value: str,
+                functionName: "encodeUrlComponent"
+            };
+        }
+        return returnVal;
+    }
+
+    /**
+     * Encode a string into a url
+     * @param {String} str - String to encode
+     * @returns {string} Encoded string
+     */
+    function encodeUrl(str) {
+        // undefined inputs always return undefined
+        if (typeof str === 'undefined') {
+            return undefined;
+        }
+
+        // Catch URIErrors when URI sequence is malformed
+        var returnVal;
+        try {
+            returnVal = encodeURI(str);
+        } catch (e) {
+            throw {
+                code: "D3140",
+                stack: (new Error()).stack,
+                value: str,
+                functionName: "encodeUrl"
+            };
+        }
+        return returnVal;
+    }
+
+    /**
+     * Decode a string from a component for a url
+     * @param {String} str - String to decode
+     * @returns {string} Decoded string
+     */
+    function decodeUrlComponent(str) {
+        // undefined inputs always return undefined
+        if (typeof str === 'undefined') {
+            return undefined;
+        }
+
+        // Catch URIErrors when URI sequence is malformed
+        var returnVal;
+        try {
+            returnVal = decodeURIComponent(str);
+        } catch (e) {
+            throw {
+                code: "D3140",
+                stack: (new Error()).stack,
+                value: str,
+                functionName: "decodeUrlComponent"
+            };
+        }
+        return returnVal;
+    }
+
+    /**
+     * Decode a string from a url
+     * @param {String} str - String to decode
+     * @returns {string} Decoded string
+     */
+    function decodeUrl(str) {
+        // undefined inputs always return undefined
+        if (typeof str === 'undefined') {
+            return undefined;
+        }
+
+        // Catch URIErrors when URI sequence is malformed
+        var returnVal;
+        try {
+            returnVal = decodeURI(str);
+        } catch (e) {
+            throw {
+                code: "D3140",
+                stack: (new Error()).stack,
+                value: str,
+                functionName: "decodeUrl"
+            };
+        }
+        return returnVal;
+    }
+
     /**
      * Split a string into an array of substrings
      * @param {String} str - string
@@ -1844,7 +1948,7 @@ const functions = (() => {
         boolean, not,
         map, zip, filter, single, foldLeft, sift,
         keys, lookup, append, exists, spread, merge, reverse, each, error, sort, shuffle,
-        base64encode, base64decode
+        base64encode, base64decode,  encodeUrlComponent, encodeUrl, decodeUrlComponent, decodeUrl
     };
 })();
 
diff --git a/src/jsonata.js b/src/jsonata.js
index 3d2e145..b82b7a7 100644
--- a/src/jsonata.js
+++ b/src/jsonata.js
@@ -1705,6 +1705,10 @@ var jsonata = (function() {
     staticFrame.bind('shuffle', defineFunction(fn.shuffle, '<a:a>'));
     staticFrame.bind('base64encode', defineFunction(fn.base64encode, '<s-:s>'));
     staticFrame.bind('base64decode', defineFunction(fn.base64decode, '<s-:s>'));
+    staticFrame.bind('encodeUrlComponent', defineFunction(fn.encodeUrlComponent, '<s-:s>'));
+    staticFrame.bind('encodeUrl', defineFunction(fn.encodeUrl, '<s-:s>'));
+    staticFrame.bind('decodeUrlComponent', defineFunction(fn.decodeUrlComponent, '<s-:s>'));
+    staticFrame.bind('decodeUrl', defineFunction(fn.decodeUrl, '<s-:s>'));
     staticFrame.bind('eval', defineFunction(functionEval, '<sx?:x>'));
     staticFrame.bind('toMillis', defineFunction(datetime.toMillis, '<s-s?:n>'));
     staticFrame.bind('fromMillis', defineFunction(datetime.fromMillis, '<n-s?s?:s>'));
@@ -1814,7 +1818,8 @@ var jsonata = (function() {
         "D3136": "The date/time picture string is missing specifiers required to parse the timestamp",
         "D3137": "{{{message}}}",
         "D3138": "The $single() function expected exactly 1 matching result.  Instead it matched more.",
-        "D3139": "The $single() function expected exactly 1 matching result.  Instead it matched 0."
+        "D3139": "The $single() function expected exactly 1 matching result.  Instead it matched 0.",
+        "D3140": "Malformed URL passed to ${{{functionName}}}(): {{value}}"
     };
 
     /**


Test patch:
diff --git a/test/test-suite/groups/function-decodeUrl/case000.json b/test/test-suite/groups/function-decodeUrl/case000.json
new file mode 100644
index 0000000..e64b673
--- /dev/null
+++ b/test/test-suite/groups/function-decodeUrl/case000.json
@@ -0,0 +1,6 @@
+{
+    "expr": "$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")",
+    "dataset": "dataset5",
+    "bindings": {},
+    "result": "https://mozilla.org/?x=шеллы"
+}
diff --git a/test/test-suite/groups/function-decodeUrl/case001.json b/test/test-suite/groups/function-decodeUrl/case001.json
new file mode 100644
index 0000000..515414c
--- /dev/null
+++ b/test/test-suite/groups/function-decodeUrl/case001.json
@@ -0,0 +1,6 @@
+{
+    "expr": "$decodeUrl(blah)",
+    "dataset": "dataset5",
+    "bindings": {},
+    "undefinedResult": true
+}
diff --git a/test/test-suite/groups/function-decodeUrl/case002.json b/test/test-suite/groups/function-decodeUrl/case002.json
new file mode 100644
index 0000000..113ef06
--- /dev/null
+++ b/test/test-suite/groups/function-decodeUrl/case002.json
@@ -0,0 +1,9 @@
+{
+    "expr": "$decodeUrl('%E0%A4%A')",
+    "dataset": "dataset5",
+    "bindings": {},
+    "error": {
+        "code": "D3140",
+        "message": "Malformed URL passed to $decodeUrl(): \"%E0%A4%A\""
+    }
+}
diff --git a/test/test-suite/groups/function-decodeUrlComponent/case000.json b/test/test-suite/groups/function-decodeUrlComponent/case000.json
new file mode 100644
index 0000000..b3b9be2
--- /dev/null
+++ b/test/test-suite/groups/function-decodeUrlComponent/case000.json
@@ -0,0 +1,6 @@
+{
+    "expr": "$decodeUrlComponent(\"%3Fx%3Dtest\")",
+    "dataset": "dataset5",
+    "bindings": {},
+    "result": "?x=test"
+}
diff --git a/test/test-suite/groups/function-decodeUrlComponent/case001.json b/test/test-suite/groups/function-decodeUrlComponent/case001.json
new file mode 100644
index 0000000..e2f8958
--- /dev/null
+++ b/test/test-suite/groups/function-decodeUrlComponent/case001.json
@@ -0,0 +1,6 @@
+{
+    "expr": "$decodeUrlComponent(blah)",
+    "dataset": "dataset5",
+    "bindings": {},
+    "undefinedResult": true
+}
diff --git a/test/test-suite/groups/function-decodeUrlComponent/case002.json b/test/test-suite/groups/function-decodeUrlComponent/case002.json
new file mode 100644
index 0000000..3c4338f
--- /dev/null
+++ b/test/test-suite/groups/function-decodeUrlComponent/case002.json
@@ -0,0 +1,9 @@
+{
+    "expr": "$decodeUrlComponent('%E0%A4%A')",
+    "dataset": "dataset5",
+    "bindings": {},
+    "error": {
+        "code": "D3140",
+        "message": "Malformed URL passed to $decodeUrlComponent(): \"%E0%A4%A\""
+    }
+}
diff --git a/test/test-suite/groups/function-encodeUrl/case000.json b/test/test-suite/groups/function-encodeUrl/case000.json
new file mode 100644
index 0000000..d570e35
--- /dev/null
+++ b/test/test-suite/groups/function-encodeUrl/case000.json
@@ -0,0 +1,6 @@
+{
+    "expr": "$encodeUrl(\"https://mozilla.org/?x=шеллы\")",
+    "dataset": "dataset5",
+    "bindings": {},
+    "result": "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"
+}
diff --git a/test/test-suite/groups/function-encodeUrl/case001.json b/test/test-suite/groups/function-encodeUrl/case001.json
new file mode 100644
index 0000000..833546c
--- /dev/null
+++ b/test/test-suite/groups/function-encodeUrl/case001.json
@@ -0,0 +1,6 @@
+{
+    "expr": "$encodeUrl(blah)",
+    "dataset": "dataset5",
+    "bindings": {},
+    "undefinedResult": true
+}
diff --git a/test/test-suite/groups/function-encodeUrl/case002.json b/test/test-suite/groups/function-encodeUrl/case002.json
new file mode 100644
index 0000000..cf716fe
--- /dev/null
+++ b/test/test-suite/groups/function-encodeUrl/case002.json
@@ -0,0 +1,9 @@
+{
+    "expr": "$encodeUrl('\uD800')",
+    "dataset": "dataset5",
+    "bindings": {},
+    "error": {
+        "code": "D3140",
+        "message": "Malformed URL passed to $encodeUrl(): \"\uD800\""
+    }
+}
diff --git a/test/test-suite/groups/function-encodeUrlComponent/case000.json b/test/test-suite/groups/function-encodeUrlComponent/case000.json
new file mode 100644
index 0000000..bf4b2c2
--- /dev/null
+++ b/test/test-suite/groups/function-encodeUrlComponent/case000.json
@@ -0,0 +1,6 @@
+{
+    "expr": "$encodeUrlComponent(\"?x=test\")",
+    "dataset": "dataset5",
+    "bindings": {},
+    "result": "%3Fx%3Dtest"
+}
diff --git a/test/test-suite/groups/function-encodeUrlComponent/case001.json b/test/test-suite/groups/function-encodeUrlComponent/case001.json
new file mode 100644
index 0000000..d88ebc8
--- /dev/null
+++ b/test/test-suite/groups/function-encodeUrlComponent/case001.json
@@ -0,0 +1,6 @@
+{
+    "expr": "$encodeUrlComponent(blah)",
+    "dataset": "dataset5",
+    "bindings": {},
+    "undefinedResult": true
+}
diff --git a/test/test-suite/groups/function-encodeUrlComponent/case002.json b/test/test-suite/groups/function-encodeUrlComponent/case002.json
new file mode 100644
index 0000000..77d94f6
--- /dev/null
+++ b/test/test-suite/groups/function-encodeUrlComponent/case002.json
@@ -0,0 +1,9 @@
+{
+    "expr": "$encodeUrlComponent('\uD800')",
+    "dataset": "dataset5",
+    "bindings": {},
+    "error": {
+        "code": "D3140",
+        "message": "Malformed URL passed to $encodeUrlComponent(): \"\uD800\""
+    }
+}