diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95955992..12f0259c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -168,4 +168,5 @@ jobs: run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..822bd42e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "name": "vscode-jest-tests", + "request": "launch", + "args": [ + "--runInBand" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + }, + { + "type": "node", + "name": "vscode-integration-tests", + "request": "launch", + "args": [ + "--runInBand", + "--config=${workspaceFolder}/integrationTests/basic/jest.config.js" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index db81063a..b191861d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ By default, this action pulls from [Version 2](https://www.vaultproject.io/docs - [Simple Key](#simple-key) - [Set Output Variable Name](#set-output-variable-name) - [Multiple Secrets](#multiple-secrets) + - [Nested Secrets](#nested-secrets) - [Using K/V version 1](#using-kv-version-1) - [Custom K/V Engine Path](#custom-kv-engine-path) - [Other Secret Engines](#other-secret-engines) @@ -20,6 +21,7 @@ By default, this action pulls from [Version 2](https://www.vaultproject.io/docs - [Namespace](#namespace) - [Reference](#reference) - [Masking - Hiding Secrets from Logs](#masking---hiding-secrets-from-logs) +- [Normalization](#normalization) @@ -81,7 +83,7 @@ The `secrets` parameter is a set of multiple secret requests separated by the `; Each secret request is comprised of the `path` and the `key` of the desired secret, and optionally the desired Env Var output name. ```raw -{{ Secret Path }} {{ Secret Key }} | {{ Output Variable Name }} +{{ Secret Path }} {{ Secret Key or Selector }} | {{ Env/Output Variable Name }} ``` ### Simple Key @@ -151,6 +153,21 @@ with: ci/aws secretKey | AWS_SECRET_ACCESS_KEY ``` +### Nested Secrets + +By default, `vault-action` will read the key from `data.data` in the response for the K/V v2 engine (default), or `data` for K/V v1 and other Secret Engines (see below for more info). +If you need to retrieve a sub-key from a JSON document, or are interested in some other component of the Vault response, you can specify a different key as the path to the desired out. + +_**Important**_: You must specify an [Output Variable Name](#set-output-variable-name) when using this method. + +```yaml +with: + secrets: | + my/path pair.key | NESTED_SECRET ; +``` + +Under the covers, we're using [JSONata](https://jsonata.org/) to provide the querying functionality. Any valid JSONata syntax is valid here, and will be outputed as a `JSON.stringify`-ied result. + ### Using K/V version 1 By default, `vault-action` expects a K/V engine using [version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2.html). @@ -179,7 +196,7 @@ This way, the `ci` secret in the example above will be retrieved from `my-secret While this action primarily supports the K/V engine, it is possible to request secrets from other engines in Vault. -To do so when specifying the `Secret Path`, just append a leading formard slash (`/`) and specify the path as described in the Vault API documentation. +To do so when specifying the `Secret Path`, just append a leading forward slash (`/`) and specify the path as described in the Vault API documentation. For example, to retrieve a stored secret from the [`cubbyhole` engine](https://www.vaultproject.io/api-docs/secret/cubbyhole/), assuming you have a stored secret at the path `foo` with the contents: @@ -231,7 +248,7 @@ with: would work fine. -NOTE: The `Secret Key` is pulled from the `data` property of the response. +*NOTE: Per [Nested Secrets](#nested-secrets), the `Key` is pulled from the `data` property of the response.* ## Adding Extra Headers @@ -247,13 +264,13 @@ with: X-Secure-Secret: ${{ secrets.SECURE_SECRET }} ``` -This will automatically add the `x-secure-id` and `x-secure-secret` headers to every request to vault. +This will automatically add the `x-secure-id` and `x-secure-secret` headers to every request to Vault. ## Vault Enterprise Features ### Namespace -If you need to retrieve secrets from a specific vault namespace, all that's required is an additional parameter specifying the namespace. +If you need to retrieve secrets from a specific Vault namespace, all that's required is an additional parameter specifying the namespace. ```yaml steps: @@ -273,7 +290,7 @@ steps: ## Reference -Here is all the inputs available through `with`: +Here are all the inputs available through `with`: | Input | Description | Default | Required | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | @@ -295,3 +312,7 @@ Here is all the inputs available through `with`: This action uses GitHub Action's built-in masking, so all variables will automatically be masked (aka hidden) if printed to the console or to logs. **This only obscures secrets from output logs.** If someone has the ability to edit your workflows, then they are able to read and therefore write secrets to somewhere else just like normal GitHub Secrets. + +## Normalization + +To make it simpler to consume certain secrets as env vars, if no Env/Output Var Name is specified `vault-action` will replace and `.` chars with `__`, remove any other non-letter or number characters. If you're concerned about the result, it's recommended to provider an explicit Output Var Key. diff --git a/dist/index.js b/dist/index.js index 4c624b34..2f0681e8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -34,7 +34,7 @@ module.exports = /******/ // the startup function /******/ function startup() { /******/ // Load entry module and return exports -/******/ return __webpack_require__(676); +/******/ return __webpack_require__(492); /******/ }; /******/ // initialize runtime /******/ runtime(__webpack_require__); @@ -3049,6 +3049,7426 @@ const cloneResponse = response => { module.exports = cloneResponse; +/***/ }), + +/***/ 350: +/***/ (function(module) { + +(function(f){if(true){module.exports=f()}else { var g; }})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c=require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u=require,i=0;i 0) { + words += '-' + lookup(remainder, false, ord); + } else if (ord) { + words = words.substring(0, words.length - 1) + 'ieth'; + } + } else if (num < 1000) { + const hundreds = Math.floor(num / 100); + const remainder = num % 100; + words = (prev ? ', ' : '') + few[hundreds] + ' Hundred'; + if (remainder > 0) { + words += lookup(remainder, true, ord); + } else if (ord) { + words += 'th'; + } + } else { + var mag = Math.floor(Math.log10(num) / 3); + if (mag > magnitudes.length) { + mag = magnitudes.length; // the largest word + } + const factor = Math.pow(10, mag * 3); + const mant = Math.floor(num / factor); + const remainder = num - mant * factor; + words = (prev ? ', ' : '') + lookup(mant, false, false) + ' ' + magnitudes[mag - 1]; + if (remainder > 0) { + words += lookup(remainder, true, ord); + } else if (ord) { + words += 'th'; + } + } + return words; + }; + + var words = lookup(value, false, ordinal); + return words; + } + + const wordValues = {}; + few.forEach(function (word, index) { + wordValues[word.toLowerCase()] = index; + }); + ordinals.forEach(function (word, index) { + wordValues[word.toLowerCase()] = index; + }); + decades.forEach(function (word, index) { + const lword = word.toLowerCase(); + wordValues[lword] = (index + 2) * 10; + wordValues[lword.substring(0, word.length - 1) + 'ieth'] = wordValues[lword]; + }); + wordValues.hundredth = 100; + magnitudes.forEach(function (word, index) { + const lword = word.toLowerCase(); + const val = Math.pow(10, (index + 1) * 3); + wordValues[lword] = val; + wordValues[lword + 'th'] = val; + }); + + /** + * Converts a number in english words to numeric value + * @param {string} text - the number in words + * @returns {number} - the numeric value + */ + function wordsToNumber(text) { + const parts = text.split(/,\s|\sand\s|[\s\\-]/); + const values = parts.map(part => wordValues[part]); + let segs = [0]; + values.forEach(value => { + if (value < 100) { + let top = segs.pop(); + if (top >= 1000) { + segs.push(top); + top = 0; + } + segs.push(top + value); + } else { + segs.push(segs.pop() * value); + } + }); + const result = segs.reduce((a, b) => a + b, 0); + return result; + } + + const romanNumerals = [ + [1000, 'm'], + [900, 'cm'], + [500, 'd'], + [400, 'cd'], + [100, 'c'], + [90, 'xc'], + [50, 'l'], + [40, 'xl'], + [10, 'x'], + [9, 'ix'], + [5, 'v'], + [4, 'iv'], + [1, 'i'] + ]; + + const romanValues = {'M': 1000, 'D': 500, 'C': 100, 'L': 50, 'X': 10, 'V': 5, 'I': 1}; + + /** + * converts a number to roman numerals + * @param {number} value - the number + * @returns {string} - the number in roman numerals + */ + function decimalToRoman(value) { + for (var index = 0; index < romanNumerals.length; index++) { + const numeral = romanNumerals[index]; + if (value >= numeral[0]) { + return numeral[1] + decimalToRoman(value - numeral[0]); + } + } + return ''; + } + + /** + * converts roman numerals to a number + * @param {string} roman - roman number + * @returns {number} - the numeric value + */ + function romanToDecimal(roman) { + var decimal = 0; + var max = 1; + for (var i = roman.length - 1; i >= 0; i--) { + const digit = roman[i]; + const value = romanValues[digit]; + if (value < max) { + decimal -= value; + } else { + max = value; + decimal += value; + } + } + return decimal; + } + + /** + * converts a number to spreadsheet style letters + * @param {number} value - the number + * @param {string} aChar - the character representing the start of the sequence, e.g. 'A' + * @returns {string} - the letters + */ + function decimalToLetters(value, aChar) { + var letters = []; + var aCode = aChar.charCodeAt(0); + while (value > 0) { + letters.unshift(String.fromCharCode((value - 1) % 26 + aCode)); + value = Math.floor((value - 1) / 26); + } + return letters.join(''); + } + + /** + * converts spreadsheet style letters to a number + * @param {string} letters - the letters + * @param {string} aChar - the character representing the start of the sequence, e.g. 'A' + * @returns {number} - the numeric value + */ + function lettersToDecimal(letters, aChar) { + var aCode = aChar.charCodeAt(0); + var decimal = 0; + for (var i = 0; i < letters.length; i++) { + decimal += (letters.charCodeAt(letters.length - i - 1) - aCode + 1) * Math.pow(26, i); + } + return decimal; + } + + /** + * Formats an integer as specified by the XPath fn:format-integer function + * See https://www.w3.org/TR/xpath-functions-31/#func-format-integer + * @param {number} value - the number to be formatted + * @param {string} picture - the picture string that specifies the format + * @returns {string} - the formatted number + */ + function formatInteger(value, picture) { + if (typeof value === 'undefined') { + return undefined; + } + + value = Math.floor(value); + + const format = analyseIntegerPicture(picture); + return _formatInteger(value, format); + } + + const formats = { + DECIMAL: 'decimal', + LETTERS: 'letters', + ROMAN: 'roman', + WORDS: 'words', + SEQUENCE: 'sequence' + }; + + const tcase = { + UPPER: 'upper', + LOWER: 'lower', + TITLE: 'title' + }; + + /** + * formats an integer using a preprocessed representation of the picture string + * @param {number} value - the number to be formatted + * @param {object} format - the preprocessed representation of the pucture string + * @returns {string} - the formatted number + * @private + */ + function _formatInteger(value, format) { + let formattedInteger; + const negative = value < 0; + value = Math.abs(value); + switch (format.primary) { + case formats.LETTERS: + formattedInteger = decimalToLetters(value, format.case === tcase.UPPER ? 'A' : 'a'); + break; + case formats.ROMAN: + formattedInteger = decimalToRoman(value); + if (format.case === tcase.UPPER) { + formattedInteger = formattedInteger.toUpperCase(); + } + break; + case formats.WORDS: + formattedInteger = numberToWords(value, format.ordinal); + if (format.case === tcase.UPPER) { + formattedInteger = formattedInteger.toUpperCase(); + } else if (format.case === tcase.LOWER) { + formattedInteger = formattedInteger.toLowerCase(); + } + break; + case formats.DECIMAL: + formattedInteger = '' + value; + // TODO use functionPad + var padLength = format.mandatoryDigits - formattedInteger.length; + if (padLength > 0) { + var padding = (new Array(padLength + 1)).join('0'); + formattedInteger = padding + formattedInteger; + } + if (format.zeroCode !== 0x30) { + formattedInteger = Array.from(formattedInteger).map(code => { + return String.fromCodePoint(code.codePointAt(0) + format.zeroCode - 0x30); + }).join(''); + } + // insert the grouping-separator-signs, if any + if (format.regular) { + const n = Math.floor((formattedInteger.length - 1) / format.groupingSeparators.position); + for (let ii = n; ii > 0; ii--) { + const pos = formattedInteger.length - ii * format.groupingSeparators.position; + formattedInteger = formattedInteger.substr(0, pos) + format.groupingSeparators.character + formattedInteger.substr(pos); + } + } else { + format.groupingSeparators.reverse().forEach(separator => { + const pos = formattedInteger.length - separator.position; + formattedInteger = formattedInteger.substr(0, pos) + separator.character + formattedInteger.substr(pos); + }); + } + + if (format.ordinal) { + var suffix123 = {'1': 'st', '2': 'nd', '3': 'rd'}; + var lastDigit = formattedInteger[formattedInteger.length - 1]; + var suffix = suffix123[lastDigit]; + if (!suffix || (formattedInteger.length > 1 && formattedInteger[formattedInteger.length - 2] === '1')) { + suffix = 'th'; + } + formattedInteger = formattedInteger + suffix; + } + break; + case formats.SEQUENCE: + throw { + code: 'D3130', + value: format.token + }; + } + if (negative) { + formattedInteger = '-' + formattedInteger; + } + + return formattedInteger; + } + + //TODO what about decimal groups in the unicode supplementary planes (surrogate pairs) ??? + const decimalGroups = [0x30, 0x0660, 0x06F0, 0x07C0, 0x0966, 0x09E6, 0x0A66, 0x0AE6, 0x0B66, 0x0BE6, 0x0C66, 0x0CE6, 0x0D66, 0x0DE6, 0x0E50, 0x0ED0, 0x0F20, 0x1040, 0x1090, 0x17E0, 0x1810, 0x1946, 0x19D0, 0x1A80, 0x1A90, 0x1B50, 0x1BB0, 0x1C40, 0x1C50, 0xA620, 0xA8D0, 0xA900, 0xA9D0, 0xA9F0, 0xAA50, 0xABF0, 0xFF10]; + + /** + * preprocesses the picture string + * @param {string} picture - picture string + * @returns {{type: string, primary: string, case: string, ordinal: boolean}} - analysed picture + */ + function analyseIntegerPicture(picture) { + const format = { + type: 'integer', + primary: formats.DECIMAL, + case: tcase.LOWER, + ordinal: false + }; + + let primaryFormat, formatModifier; + const semicolon = picture.lastIndexOf(';'); + if (semicolon === -1) { + primaryFormat = picture; + } else { + primaryFormat = picture.substring(0, semicolon); + formatModifier = picture.substring(semicolon + 1); + if (formatModifier[0] === 'o') { + format.ordinal = true; + } + } + + /* eslnt-disable-next no-fallthrough */ + switch (primaryFormat) { + case 'A': + format.case = tcase.UPPER; + /* eslnt-disable-next-line no-fallthrough */ + case 'a': + format.primary = formats.LETTERS; + break; + case 'I': + format.case = tcase.UPPER; + /* eslnt-disable-next-line no-fallthrough */ + case 'i': + format.primary = formats.ROMAN; + break; + case 'W': + format.case = tcase.UPPER; + format.primary = formats.WORDS; + break; + case 'Ww': + format.case = tcase.TITLE; + format.primary = formats.WORDS; + break; + case 'w': + format.primary = formats.WORDS; + break; + default: { + // this is a decimal-digit-pattern if it contains a decimal digit (from any unicode decimal digit group) + let zeroCode = null; + let mandatoryDigits = 0; + let optionalDigits = 0; + let groupingSeparators = []; + let separatorPosition = 0; + const formatCodepoints = Array.from(primaryFormat, c => c.codePointAt(0)).reverse(); // reverse the array to determine positions of grouping-separator-signs + formatCodepoints.forEach((codePoint) => { + // step though each char in the picture to determine the digit group + let digit = false; + for (let ii = 0; ii < decimalGroups.length; ii++) { + const group = decimalGroups[ii]; + if (codePoint >= group && codePoint <= group + 9) { + // codepoint is part of this decimal group + digit = true; + mandatoryDigits++; + separatorPosition++; + if (zeroCode === null) { + zeroCode = group; + } else if (group !== zeroCode) { + // error! different decimal groups in the same pattern + throw { + code: 'D3131' + }; + } + break; + } + } + if (!digit) { + if (codePoint === 0x23) { // # - optional-digit-sign + separatorPosition++; + optionalDigits++; + } else { + // neither a decimal-digit-sign ot optional-digit-sign, assume it is a grouping-separator-sign + groupingSeparators.push({ + position: separatorPosition, + character: String.fromCodePoint(codePoint) + }); + } + } + }); + if (mandatoryDigits > 0) { + format.primary = formats.DECIMAL; + // TODO validate decimal-digit-pattern + + // the decimal digit family (codepoint offset) + format.zeroCode = zeroCode; + // the number of mandatory digits + format.mandatoryDigits = mandatoryDigits; + // the number of optional digits + format.optionalDigits = optionalDigits; + // grouping separator template + // are the grouping-separator-signs 'regular'? + const regularRepeat = function (separators) { + // are the grouping positions regular? i.e. same interval between each of them + // is there at least one separator? + if (separators.length === 0) { + return 0; + } + // are all the characters the same? + const sepChar = separators[0].character; + for (let ii = 1; ii < separators.length; ii++) { + if (separators[ii].character !== sepChar) { + return 0; + } + } + // are they equally spaced? + const indexes = separators.map(separator => separator.position); + const gcd = function (a, b) { + return b === 0 ? a : gcd(b, a % b); + }; + // find the greatest common divisor of all the positions + const factor = indexes.reduce(gcd); + // is every position separated by this divisor? If so, it's regular + for (let index = 1; index <= indexes.length; index++) { + if (indexes.indexOf(index * factor) === -1) { + return 0; + } + } + return factor; + }; + + const regular = regularRepeat(groupingSeparators); + if (regular > 0) { + format.regular = true; + format.groupingSeparators = { + position: regular, + character: groupingSeparators[0].character + }; + } else { + format.regular = false; + format.groupingSeparators = groupingSeparators; + } + + } else { + // this is a 'numbering sequence' which the spec says is implementation-defined + // this implementation doesn't support any numbering sequences at the moment. + format.primary = formats.SEQUENCE; + format.token = primaryFormat; + } + } + } + + return format; + } + + const defaultPresentationModifiers = { + Y: '1', M: '1', D: '1', d: '1', F: 'n', W: '1', w: '1', X: '1', x: '1', H: '1', h: '1', + P: 'n', m: '01', s: '01', f: '1', Z: '01:01', z: '01:01', C: 'n', E: 'n' + }; + + // §9.8.4.1 the format specifier is an array of string literals and variable markers + /** + * analyse the date-time picture string + * @param {string} picture - picture string + * @returns {{type: string, parts: Array}} - the analysed string + */ + function analyseDateTimePicture(picture) { + var spec = []; + const format = { + type: 'datetime', + parts: spec + }; + const addLiteral = function (start, end) { + if (end > start) { + let literal = picture.substring(start, end); + // replace any doubled ]] with single ] + // what if there are instances of single ']' ? - the spec doesn't say + literal = literal.split(']]').join(']'); + spec.push({type: 'literal', value: literal}); + } + }; + + var start = 0, pos = 0; + while (pos < picture.length) { + if (picture.charAt(pos) === '[') { + // check it's not a doubled [[ + if (picture.charAt(pos + 1) === '[') { + // literal [ + addLiteral(start, pos); + spec.push({type: 'literal', value: '['}); + pos += 2; + start = pos; + continue; + } + // start of variable marker + // push the string literal (if there is one) onto the array + addLiteral(start, pos); + start = pos; + // search forward to closing ] + pos = picture.indexOf(']', start); + // TODO handle error case if pos === -1 + if(pos === -1) { + // error - no closing bracket + throw { + code: 'D3135' + }; + } + let marker = picture.substring(start + 1, pos); + // whitespace within a variable marker is ignored (i.e. remove it) + marker = marker.split(/\s+/).join(''); + var def = { + type: 'marker', + component: marker.charAt(0) // 1. The component specifier is always present and is always a single letter. + }; + var comma = marker.lastIndexOf(','); // 2. The width modifier may be recognized by the presence of a comma + var presMod; // the presentation modifiers + if (comma !== -1) { + // §9.8.4.2 The Width Modifier + const widthMod = marker.substring(comma + 1); + const dash = widthMod.indexOf('-'); + let min, max; + const parseWidth = function (wm) { + if (typeof wm === 'undefined' || wm === '*') { + return undefined; + } else { + // TODO validate wm is an unsigned int + return parseInt(wm); + } + }; + if (dash === -1) { + min = widthMod; + } else { + min = widthMod.substring(0, dash); + max = widthMod.substring(dash + 1); + } + const widthDef = { + min: parseWidth(min), + max: parseWidth(max) + }; + def.width = widthDef; + presMod = marker.substring(1, comma); + } else { + presMod = marker.substring(1); + } + if (presMod.length === 1) { + def.presentation1 = presMod; // first presentation modifier + //TODO validate the first presentation modifier - it's either N, n, Nn or it passes analyseIntegerPicture + } else if (presMod.length > 1) { + var lastChar = presMod.charAt(presMod.length - 1); + if ('atco'.indexOf(lastChar) !== -1) { + def.presentation2 = lastChar; + if (lastChar === 'o') { + def.ordinal = true; + } + // 'c' means 'cardinal' and is the default (i.e. not 'ordinal') + // 'a' & 't' are ignored (not sure of their relevance to English numbering) + def.presentation1 = presMod.substring(0, presMod.length - 1); + } else { + def.presentation1 = presMod; + //TODO validate the first presentation modifier - it's either N, n, Nn or it passes analyseIntegerPicture, + // doesn't use ] as grouping separator, and if grouping separator is , then must have width modifier + } + } else { + // no presentation modifier specified - apply the default; + def.presentation1 = defaultPresentationModifiers[def.component]; + } + if (typeof def.presentation1 === 'undefined') { + // unknown component specifier + throw { + code: 'D3132', + value: def.component + }; + } + if (def.presentation1[0] === 'n') { + def.names = tcase.LOWER; + } else if (def.presentation1[0] === 'N') { + if (def.presentation1[1] === 'n') { + def.names = tcase.TITLE; + } else { + def.names = tcase.UPPER; + } + } else if ('YMDdFWwXxHhmsf'.indexOf(def.component) !== -1) { + var integerPattern = def.presentation1; + if (def.presentation2) { + integerPattern += ';' + def.presentation2; + } + def.integerFormat = analyseIntegerPicture(integerPattern); + if (def.width && def.width.min !== undefined) { + if (def.integerFormat.mandatoryDigits < def.width.min) { + def.integerFormat.mandatoryDigits = def.width.min; + } + } + if (def.component === 'Y') { + // §9.8.4.4 + def.n = -1; + if (def.width && def.width.max !== undefined) { + def.n = def.width.max; + def.integerFormat.mandatoryDigits = def.n; + } else { + var w = def.integerFormat.mandatoryDigits + def.integerFormat.optionalDigits; + if (w >= 2) { + def.n = w; + } + } + } + } + if (def.component === 'Z' || def.component === 'z') { + def.integerFormat = analyseIntegerPicture(def.presentation1); + } + spec.push(def); + start = pos + 1; + } + pos++; + } + addLiteral(start, pos); + return format; + } + + const days = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const millisInADay = 1000 * 60 * 60 * 24; + + const startOfFirstWeek = function (ym) { + // ISO 8601 defines the first week of the year to be the week that contains the first Thursday + // XPath F&O extends this same definition for the first week of a month + // the week starts on a Monday - calculate the millis for the start of the first week + // millis for given 1st Jan of that year (at 00:00 UTC) + const jan1 = Date.UTC(ym.year, ym.month); + var dayOfJan1 = (new Date(jan1)).getUTCDay(); + if (dayOfJan1 === 0) { + dayOfJan1 = 7; + } + // if Jan 1 is Fri, Sat or Sun, then add the number of days (in millis) to jan1 to get the start of week 1 + return dayOfJan1 > 4 ? jan1 + (8 - dayOfJan1) * millisInADay : jan1 - (dayOfJan1 - 1) * millisInADay; + }; + + const yearMonth = function (year, month) { + return { + year: year, + month: month, + nextMonth: function () { + return (month === 11) ? yearMonth(year + 1, 0) : yearMonth(year, month + 1); + }, + previousMonth: function () { + return (month === 0) ? yearMonth(year - 1, 11) : yearMonth(year, month - 1); + }, + nextYear: function () { + return yearMonth(year + 1, month); + }, + previousYear: function () { + return yearMonth(year - 1, month); + } + }; + }; + + const deltaWeeks = function (start, end) { + return (end - start) / (millisInADay * 7) + 1; + }; + + const getDateTimeFragment = (date, component) => { + let componentValue; + switch (component) { + case 'Y': // year + componentValue = date.getUTCFullYear(); + break; + case 'M': // month in year + componentValue = date.getUTCMonth() + 1; + break; + case 'D': // day in month + componentValue = date.getUTCDate(); + break; + case 'd': { // day in year + // millis for given date (at 00:00 UTC) + const today = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); + // millis for given 1st Jan of that year (at 00:00 UTC) + const firstJan = Date.UTC(date.getUTCFullYear(), 0); + componentValue = (today - firstJan) / millisInADay + 1; + break; + } + case 'F': // day of week + componentValue = date.getUTCDay(); + if (componentValue === 0) { + // ISO 8601 defines days 1-7: Mon-Sun + componentValue = 7; + } + break; + case 'W': { // week in year + const thisYear = yearMonth(date.getUTCFullYear(), 0); + const startOfWeek1 = startOfFirstWeek(thisYear); + const today = Date.UTC(thisYear.year, date.getUTCMonth(), date.getUTCDate()); + let week = deltaWeeks(startOfWeek1, today); + if (week > 52) { + // might be first week of the following year + const startOfFollowingYear = startOfFirstWeek(thisYear.nextYear()); + if (today >= startOfFollowingYear) { + week = 1; + } + } else if (week < 1) { + // must be end of the previous year + const startOfPreviousYear = startOfFirstWeek(thisYear.previousYear()); + week = deltaWeeks(startOfPreviousYear, today); + } + componentValue = Math.floor(week); + break; + } + case 'w': { // week in month + const thisMonth = yearMonth(date.getUTCFullYear(), date.getUTCMonth()); + const startOfWeek1 = startOfFirstWeek(thisMonth); + const today = Date.UTC(thisMonth.year, thisMonth.month, date.getUTCDate()); + let week = deltaWeeks(startOfWeek1, today); + if (week > 4) { + // might be first week of the following month + const startOfFollowingMonth = startOfFirstWeek(thisMonth.nextMonth()); + if (today >= startOfFollowingMonth) { + week = 1; + } + } else if (week < 1) { + // must be end of the previous month + const startOfPreviousMonth = startOfFirstWeek(thisMonth.previousMonth()); + week = deltaWeeks(startOfPreviousMonth, today); + } + componentValue = Math.floor(week); + break; + } + case 'X': { // ISO week-numbering year + // Extension: The F&O spec says nothing about how to access the year associated with the week-of-the-year + // e.g. Sat 1 Jan 2005 is in the 53rd week of 2004. + // The 'W' component specifier gives 53, but 'Y' will give 2005. + // I propose to add 'X' as the component specifier to give the ISO week-numbering year (2004 in this example) + const thisYear = yearMonth(date.getUTCFullYear(), 0); + const startOfISOYear = startOfFirstWeek(thisYear); + const endOfISOYear = startOfFirstWeek(thisYear.nextYear()); + const now = date.getTime(); + if (now < startOfISOYear) { + componentValue = thisYear.year - 1; + } else if (now >= endOfISOYear) { + componentValue = thisYear.year + 1; + } else { + componentValue = thisYear.year; + } + break; + } + case 'x': { // ISO week-numbering month + // Extension: The F&O spec says nothing about how to access the month associated with the week-of-the-month + // e.g. Sat 1 Jan 2005 is in the 5th week of December 2004. + // The 'w' component specifier gives 5, but 'W' will give January and 'Y' will give 2005. + // I propose to add 'x' as the component specifier to give the 'week-numbering' month (December in this example) + const thisMonth = yearMonth(date.getUTCFullYear(), date.getUTCMonth()); + const startOfISOMonth = startOfFirstWeek(thisMonth); + const nextMonth = thisMonth.nextMonth(); + const endOfISOMonth = startOfFirstWeek(nextMonth); + const now = date.getTime(); + if (now < startOfISOMonth) { + componentValue = thisMonth.previousMonth().month + 1; + } else if (now >= endOfISOMonth) { + componentValue = nextMonth.month + 1; + } else { + componentValue = thisMonth.month + 1; + } + break; + } + case 'H': // hour in day (24 hours) + componentValue = date.getUTCHours(); + break; + case 'h': // hour in half-day (12 hours) + componentValue = date.getUTCHours(); + componentValue = componentValue % 12; + if (componentValue === 0) { + componentValue = 12; + } + break; + case 'P': // am/pm marker + componentValue = date.getUTCHours() >= 12 ? 'pm' : 'am'; + break; + case 'm': // minute in hour + componentValue = date.getUTCMinutes(); + break; + case 's': // second in minute + componentValue = date.getUTCSeconds(); + break; + case 'f': // fractional seconds + componentValue = date.getUTCMilliseconds(); + break; + case 'Z': // timezone + case 'z': + // since the date object is constructed from epoch millis, the TZ component is always be UTC. + break; + case 'C': // calendar name + componentValue = 'ISO'; + break; + case 'E': // era + componentValue = 'ISO'; + break; + } + return componentValue; + }; + + const iso8601Spec = analyseDateTimePicture('[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01].[f001][Z01:01t]'); + + /** + * formats the date/time as specified by the XPath fn:format-dateTime function + * @param {number} millis - the timestamp to be formatted, in millis since the epoch + * @param {string} picture - the picture string that specifies the format + * @param {string} timezone - the timezone to use + * @returns {string} - the formatted timestamp + */ + function formatDateTime(millis, picture, timezone) { + var offsetHours = 0; + var offsetMinutes = 0; + + if (typeof timezone !== 'undefined') { + // parse the hour and minute offsets + // assume for now the format supplied is +hhmm + const offset = parseInt(timezone); + offsetHours = Math.floor(offset / 100); + offsetMinutes = offset % 100; + } + + var formatComponent = function (date, markerSpec) { + var componentValue = getDateTimeFragment(date, markerSpec.component); + + // §9.8.4.3 Formatting Integer-Valued Date/Time Components + if ('YMDdFWwXxHhms'.indexOf(markerSpec.component) !== -1) { + if (markerSpec.component === 'Y') { + // §9.8.4.4 Formatting the Year Component + if (markerSpec.n !== -1) { + componentValue = componentValue % Math.pow(10, markerSpec.n); + } + } + if (markerSpec.names) { + if (markerSpec.component === 'M' || markerSpec.component === 'x') { + componentValue = months[componentValue - 1]; + } else if (markerSpec.component === 'F') { + componentValue = days[componentValue]; + } else { + throw { + code: 'D3133', + value: markerSpec.component + }; + } + if (markerSpec.names === tcase.UPPER) { + componentValue = componentValue.toUpperCase(); + } else if (markerSpec.names === tcase.LOWER) { + componentValue = componentValue.toLowerCase(); + } + if (markerSpec.width && componentValue.length > markerSpec.width.max) { + componentValue = componentValue.substring(0, markerSpec.width.max); + } + } else { + componentValue = _formatInteger(componentValue, markerSpec.integerFormat); + } + } else if (markerSpec.component === 'f') { + // TODO §9.8.4.5 Formatting Fractional Seconds + componentValue = _formatInteger(componentValue, markerSpec.integerFormat); + } else if (markerSpec.component === 'Z' || markerSpec.component === 'z') { + // §9.8.4.6 Formatting timezones + const offset = offsetHours * 100 + offsetMinutes; + if (markerSpec.integerFormat.regular) { + componentValue = _formatInteger(offset, markerSpec.integerFormat); + } else { + const numDigits = markerSpec.integerFormat.mandatoryDigits; + if (numDigits === 1 || numDigits === 2) { + componentValue = _formatInteger(offsetHours, markerSpec.integerFormat); + if (offsetMinutes !== 0) { + componentValue += ':' + formatInteger(offsetMinutes, '00'); + } + } else if (numDigits === 3 || numDigits === 4) { + componentValue = _formatInteger(offset, markerSpec.integerFormat); + } else { + throw { + code: 'D3134', + value: numDigits + }; + } + } + if (offset >= 0) { + componentValue = '+' + componentValue; + } + if (markerSpec.component === 'z') { + componentValue = 'GMT' + componentValue; + } + if (offset === 0 && markerSpec.presentation2 === 't') { + componentValue = 'Z'; + } + } + return componentValue; + }; + + let formatSpec; + if(typeof picture === 'undefined') { + // default to ISO 8601 format + formatSpec = iso8601Spec; + } else { + formatSpec = analyseDateTimePicture(picture); + } + + const offsetMillis = (60 * offsetHours + offsetMinutes) * 60 * 1000; + const dateTime = new Date(millis + offsetMillis); + + let result = ''; + formatSpec.parts.forEach(function (part) { + if (part.type === 'literal') { + result += part.value; + } else { + result += formatComponent(dateTime, part); + } + }); + + return result; + } + + /** + * Generate a regex to parse integers or timestamps + * @param {object} formatSpec - object representing the format + * @returns {object} - regex + */ + function generateRegex(formatSpec) { + var matcher = {}; + if (formatSpec.type === 'datetime') { + matcher.type = 'datetime'; + matcher.parts = formatSpec.parts.map(function (part) { + var res = {}; + if (part.type === 'literal') { + res.regex = part.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } else if (part.integerFormat) { + res = generateRegex(part.integerFormat); + } else { + // must be a month or day name + res.regex = '[a-zA-Z]+'; + var lookup = {}; + if (part.component === 'M' || part.component === 'x') { + // months + months.forEach(function (name, index) { + if (part.width && part.width.max) { + lookup[name.substring(0, part.width.max)] = index + 1; + } else { + lookup[name] = index + 1; + } + }); + } else if (part.component === 'F') { + // days + days.forEach(function (name, index) { + if (index > 0) { + if (part.width && part.width.max) { + lookup[name.substring(0, part.width.max)] = index; + } else { + lookup[name] = index; + } + } + }); + } else if (part.component === 'P') { + lookup = {'am': 0, 'AM': 0, 'pm': 1, 'PM': 1}; + } else { + // unsupported 'name' option for this component + throw { + code: 'D3133', + value: part.component + }; + } + res.parse = function (value) { + return lookup[value]; + }; + } + res.component = part.component; + return res; + }); + } else { // type === 'integer' + matcher.type = 'integer'; + const isUpper = formatSpec.case === tcase.UPPER; + switch (formatSpec.primary) { + case formats.LETTERS: + matcher.regex = isUpper ? '[A-Z]+' : '[a-z]+'; + matcher.parse = function (value) { + return lettersToDecimal(value, isUpper ? 'A' : 'a'); + }; + break; + case formats.ROMAN: + matcher.regex = isUpper ? '[MDCLXVI]+' : '[mdclxvi]+'; + matcher.parse = function (value) { + return romanToDecimal(isUpper ? value : value.toUpperCase()); + }; + break; + case formats.WORDS: + matcher.regex = '(?:' + Object.keys(wordValues).concat('and', '[\\-, ]').join('|') + ')+'; + matcher.parse = function (value) { + return wordsToNumber(value.toLowerCase()); + }; + break; + case formats.DECIMAL: + matcher.regex = '[0-9]+'; + if (formatSpec.ordinal) { + // ordinals + matcher.regex += '(?:th|st|nd|rd)'; + } + matcher.parse = function (value) { + let digits = value; + if (formatSpec.ordinal) { + // strip off the suffix + digits = value.substring(0, value.length - 2); + } + // strip out the separators + if (formatSpec.regular) { + digits = digits.split(',').join(''); + } else { + formatSpec.groupingSeparators.forEach(sep => { + digits = digits.split(sep.character).join(''); + }); + } + if (formatSpec.zeroCode !== 0x30) { + // apply offset + digits = digits.split('').map(char => String.fromCodePoint(char.codePointAt(0) - formatSpec.zeroCode + 0x30)).join(''); + } + return parseInt(digits); + }; + break; + case formats.SEQUENCE: + throw { + code: 'D3130', + value: formatSpec.token + }; + } + + } + return matcher; + } + + /** + * parse a string containing an integer as specified by the picture string + * @param {string} value - the string to parse + * @param {string} picture - the picture string + * @returns {number} - the parsed number + */ + function parseInteger(value, picture) { + if (typeof value === 'undefined') { + return undefined; + } + + const formatSpec = analyseIntegerPicture(picture); + const matchSpec = generateRegex(formatSpec); + //const fullRegex = '^' + matchSpec.regex + '$'; + //const matcher = new RegExp(fullRegex); + // TODO validate input based on the matcher regex + const result = matchSpec.parse(value); + return result; + } + + /** + * parse a string containing a timestamp as specified by the picture string + * @param {string} timestamp - the string to parse + * @param {string} picture - the picture string + * @returns {number} - the parsed timestamp in millis since the epoch + */ + function parseDateTime(timestamp, picture) { + const formatSpec = analyseDateTimePicture(picture); + const matchSpec = generateRegex(formatSpec); + const fullRegex = '^' + matchSpec.parts.map(part => '(' + part.regex + ')').join('') + '$'; + + const matcher = new RegExp(fullRegex, 'i'); // TODO can cache this against the picture + var info = matcher.exec(timestamp); + if (info !== null) { + // validate what we've just parsed - do we have enough information to create a timestamp? + // rules: + // The date is specified by one of: + // {Y, M, D} (dateA) + // or {Y, d} (dateB) + // or {Y, x, w, F} (dateC) + // or {X, W, F} (dateD) + // The time is specified by one of: + // {H, m, s, f} (timeA) + // or {P, h, m, s, f} (timeB) + // All sets can have an optional Z + // To create a timestamp (epoch millis) we need both date and time, but we can default missing + // information according to the following rules: + // - line up one combination of the above from date, and one from time, most significant value (MSV) to least significant (LSV + // - for the values that have been captured, if there are any gaps between MSV and LSV, then throw an error + // (e.g.) if hour and seconds, but not minutes is given - throw + // (e.g.) if month, hour and minutes, but not day-of-month is given - throw + // - anything right of the LSV should be defaulted to zero + // (e.g.) if hour and minutes given, default seconds and fractional seconds to zero + // (e.g.) if date only given, default the time to 0:00:00.000 (midnight) + // - anything left of the MSV should be defaulted to the value of that component returned by $now() + // (e.g.) if time only given, default the date to today + // (e.g.) if month and date given, default to this year (and midnight, by previous rule) + // -- default values for X, x, W, w, F will be derived from the values returned by $now() + + // implement the above rules + // determine which of the above date/time combinations we have by using bit masks + + // Y X M x W w d D F P H h m s f Z + // dateA 1 0 1 0 0 0 0 1 ? 0 - must not appear + // dateB 1 0 0 0 0 0 1 0 ? 1 - can appear - relevant + // dateC 0 1 0 1 0 1 0 0 1 ? - can appear - ignored + // dateD 0 1 0 0 1 0 0 0 1 + // timeA 0 1 0 1 1 1 + // timeB 1 0 1 1 1 1 + + // create bitmasks based on the above + // date mask YXMxWwdD + const dmA = 161; // binary 10100001 + const dmB = 130; // binary 10000010 + const dmC = 84; // binary 01010100 + const dmD = 72; // binary 01001000 + // time mask PHhmsf + const tmA = 23; // binary 010111 + const tmB = 47; // binary 101111 + + const components = {}; + for (let i = 1; i < info.length; i++) { + const mpart = matchSpec.parts[i - 1]; + if (mpart.parse) { + components[mpart.component] = mpart.parse(info[i]); + } + } + + if(Object.getOwnPropertyNames(components).length === 0) { + // nothing specified + return undefined; + } + + let mask = 0; + + const shift = bit => { + mask <<= 1; + mask += bit ? 1 : 0; + }; + + const isType = type => { + // shouldn't match any 0's, must match at least one 1 + return !(~type & mask) && !!(type & mask); + }; + + 'YXMxWwdD'.split('').forEach(part => shift(components[part])); + + const dateA = isType(dmA); + const dateB = !dateA && isType(dmB); + const dateC = isType(dmC); + const dateD = !dateC && isType(dmD); + + mask = 0; + 'PHhmsf'.split('').forEach(part => shift(components[part])); + + const timeA = isType(tmA); + const timeB = !timeA && isType(tmB); + + // should only be zero or one date type and zero or one time type + + const dateComps = dateB ? 'YD' : dateC ? 'XxwF' : dateD? 'XWF' : 'YMD'; + const timeComps = timeB ? 'Phmsf' : 'Hmsf'; + + const comps = dateComps + timeComps; + + // step through the candidate parts from most significant to least significant + // default the most significant unspecified parts to current timestamp component + // default the least significant unspecified parts to zero + // if any gaps in between the specified parts, throw an error + + const now = this.environment.timestamp; // must get the fixed timestamp from jsonata + + let startSpecified = false; + let endSpecified = false; + comps.split('').forEach(part => { + if(typeof components[part] === 'undefined') { + if(startSpecified) { + // past the specified block - default to zero + components[part] = ('MDd'.indexOf(part) !== -1) ? 1 : 0; + endSpecified = true; + } else { + // haven't hit the specified block yet, default to current timestamp + components[part] = getDateTimeFragment(now, part); + } + } else { + startSpecified = true; + if(endSpecified) { + throw { + code: 'D3136' + }; + } + } + }); + + // validate and fill in components + if (components.M > 0) { + components.M -= 1; // Date.UTC requires a zero-indexed month + } else { + components.M = 0; // default to January + } + if (dateB) { + // millis for given 1st Jan of that year (at 00:00 UTC) + const firstJan = Date.UTC(components.Y, 0); + const offsetMillis = (components.d - 1) * 1000 * 60 * 60 * 24; + const derivedDate = new Date(firstJan + offsetMillis); + components.M = derivedDate.getUTCMonth(); + components.D = derivedDate.getUTCDate(); + } + if (dateC) { + // TODO implement this + // parsing this format not currently supported + throw { + code: 'D3136' + }; + } + if (dateD) { + // TODO implement this + // parsing this format (ISO week date) not currently supported + throw { + code: 'D3136' + }; + } + if (timeB) { + // 12hr to 24hr + components.H = components.h === 12 ? 0 : components.h; + if (components.P === 1) { + components.H += 12; + } + } + + var millis = Date.UTC(components.Y, components.M, components.D, components.H, components.m, components.s, components.f); + return millis; + } + } + + // Regular expression to match an ISO 8601 formatted timestamp + var iso8601regex = new RegExp('^\\d{4}(-[01]\\d)*(-[0-3]\\d)*(T[0-2]\\d:[0-5]\\d:[0-5]\\d)*(\\.\\d+)?([+-][0-2]\\d:?[0-5]\\d|Z)?$'); + + /** + * Converts an ISO 8601 timestamp to milliseconds since the epoch + * + * @param {string} timestamp - the timestamp to be converted + * @param {string} [picture] - the picture string defining the format of the timestamp (defaults to ISO 8601) + * @returns {Number} - milliseconds since the epoch + */ + function toMillis(timestamp, picture) { + // undefined inputs always return undefined + if(typeof timestamp === 'undefined') { + return undefined; + } + + if(typeof picture === 'undefined') { + if (!iso8601regex.test(timestamp)) { + throw { + stack: (new Error()).stack, + code: "D3110", + value: timestamp + }; + } + + return Date.parse(timestamp); + } else { + return parseDateTime.call(this, timestamp, picture); + } + } + + /** + * Converts milliseconds since the epoch to an ISO 8601 timestamp + * @param {Number} millis - milliseconds since the epoch to be converted + * @param {string} [picture] - the picture string defining the format of the timestamp (defaults to ISO 8601) + * @param {string} [timezone] - the timezone to format the timestamp in (defaults to UTC) + * @returns {String} - the formatted timestamp + */ + function fromMillis(millis, picture, timezone) { + // undefined inputs always return undefined + if(typeof millis === 'undefined') { + return undefined; + } + + return formatDateTime.call(this, millis, picture, timezone); + } + + return { + formatInteger, parseInteger, fromMillis, toMillis + }; +})(); + +module.exports = dateTime; + +},{}],2:[function(require,module,exports){ +(function (global){ +/** + * © Copyright IBM Corp. 2016, 2018 All Rights Reserved + * Project name: JSONata + * This project is licensed under the MIT License, see LICENSE + */ + +var utils = require('./utils'); + +const functions = (() => { + 'use strict'; + + var isNumeric = utils.isNumeric; + var isArrayOfStrings = utils.isArrayOfStrings; + var isArrayOfNumbers = utils.isArrayOfNumbers; + var createSequence = utils.createSequence; + var isSequence = utils.isSequence; + var isFunction = utils.isFunction; + var isLambda = utils.isLambda; + var isIterable = utils.isIterable; + var getFunctionArity = utils.getFunctionArity; + var deepEquals = utils.isDeepEqual; + + /** + * Sum function + * @param {Object} args - Arguments + * @returns {number} Total value of arguments + */ + function sum(args) { + // undefined inputs always return undefined + if (typeof args === 'undefined') { + return undefined; + } + + var total = 0; + args.forEach(function (num) { + total += num; + }); + return total; + } + + /** + * Count function + * @param {Object} args - Arguments + * @returns {number} Number of elements in the array + */ + function count(args) { + // undefined inputs always return undefined + if (typeof args === 'undefined') { + return 0; + } + + return args.length; + } + + /** + * Max function + * @param {Object} args - Arguments + * @returns {number} Max element in the array + */ + function max(args) { + // undefined inputs always return undefined + if (typeof args === 'undefined' || args.length === 0) { + return undefined; + } + + return Math.max.apply(Math, args); + } + + /** + * Min function + * @param {Object} args - Arguments + * @returns {number} Min element in the array + */ + function min(args) { + // undefined inputs always return undefined + if (typeof args === 'undefined' || args.length === 0) { + return undefined; + } + + return Math.min.apply(Math, args); + } + + /** + * Average function + * @param {Object} args - Arguments + * @returns {number} Average element in the array + */ + function average(args) { + // undefined inputs always return undefined + if (typeof args === 'undefined' || args.length === 0) { + return undefined; + } + + var total = 0; + args.forEach(function (num) { + total += num; + }); + return total / args.length; + } + + /** + * Stringify arguments + * @param {Object} arg - Arguments + * @param {boolean} [prettify] - Pretty print the result + * @returns {String} String from arguments + */ + function string(arg, prettify = false) { + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + var str; + + if (typeof arg === 'string') { + // already a string + str = arg; + } else if (isFunction(arg)) { + // functions (built-in and lambda convert to empty string + str = ''; + } else if (typeof arg === 'number' && !isFinite(arg)) { + throw { + code: "D3001", + value: arg, + stack: (new Error()).stack + }; + } else { + var space = prettify ? 2 : 0; + if(Array.isArray(arg) && arg.outerWrapper) { + arg = arg[0]; + } + str = JSON.stringify(arg, function (key, val) { + return (typeof val !== 'undefined' && val !== null && val.toPrecision && isNumeric(val)) ? Number(val.toPrecision(15)) : + (val && isFunction(val)) ? '' : val; + }, space); + } + return str; + } + + /** + * Create substring based on character number and length + * @param {String} str - String to evaluate + * @param {Integer} start - Character number to start substring + * @param {Integer} [length] - Number of characters in substring + * @returns {string|*} Substring + */ + function substring(str, start, length) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + var strArray = Array.from(str); + var strLength = strArray.length; + + if (strLength + start < 0) { + start = 0; + } + + if (typeof length !== 'undefined') { + if (length <= 0) { + return ''; + } + var end = start >= 0 ? start + length : strLength + start + length; + return strArray.slice(start, end).join(''); + } + + return strArray.slice(start).join(''); + } + + /** + * Create substring up until a character + * @param {String} str - String to evaluate + * @param {String} chars - Character to define substring boundary + * @returns {*} Substring + */ + function substringBefore(str, chars) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + var pos = str.indexOf(chars); + if (pos > -1) { + return str.substr(0, pos); + } else { + return str; + } + } + + /** + * Create substring after a character + * @param {String} str - String to evaluate + * @param {String} chars - Character to define substring boundary + * @returns {*} Substring + */ + function substringAfter(str, chars) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + var pos = str.indexOf(chars); + if (pos > -1) { + return str.substr(pos + chars.length); + } else { + return str; + } + } + + /** + * Lowercase a string + * @param {String} str - String to evaluate + * @returns {string} Lowercase string + */ + function lowercase(str) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + return str.toLowerCase(); + } + + /** + * Uppercase a string + * @param {String} str - String to evaluate + * @returns {string} Uppercase string + */ + function uppercase(str) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + return str.toUpperCase(); + } + + /** + * length of a string + * @param {String} str - string + * @returns {Number} The number of characters in the string + */ + function length(str) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + return Array.from(str).length; + } + + /** + * Normalize and trim whitespace within a string + * @param {string} str - string to be trimmed + * @returns {string} - trimmed string + */ + function trim(str) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + // normalize whitespace + var result = str.replace(/[ \t\n\r]+/gm, ' '); + if (result.charAt(0) === ' ') { + // strip leading space + result = result.substring(1); + } + if (result.charAt(result.length - 1) === ' ') { + // strip trailing space + result = result.substring(0, result.length - 1); + } + return result; + } + + /** + * Pad a string to a minimum width by adding characters to the start or end + * @param {string} str - string to be padded + * @param {number} width - the minimum width; +ve pads to the right, -ve pads to the left + * @param {string} [char] - the pad character(s); defaults to ' ' + * @returns {string} - padded string + */ + function pad(str, width, char) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + if (typeof char === 'undefined' || char.length === 0) { + char = ' '; + } + + var result; + var padLength = Math.abs(width) - length(str); + if (padLength > 0) { + var padding = (new Array(padLength + 1)).join(char); + if (char.length > 1) { + padding = substring(padding, 0, padLength); + } + if (width > 0) { + result = str + padding; + } else { + result = padding + str; + } + } else { + result = str; + } + return result; + } + + /** + * Evaluate the matcher function against the str arg + * + * @param {*} matcher - matching function (native or lambda) + * @param {string} str - the string to match against + * @returns {object} - structure that represents the match(es) + */ + function* evaluateMatcher(matcher, str) { + var result = matcher.apply(this, [str]); // eslint-disable-line no-useless-call + if(isIterable(result)) { + result = yield * result; + } + if(result && !(typeof result.start === 'number' || result.end === 'number' || Array.isArray(result.groups) || isFunction(result.next))) { + // the matcher function didn't return the correct structure + throw { + code: "T1010", + stack: (new Error()).stack, + }; + } + return result; + } + + /** + * Tests if the str contains the token + * @param {String} str - string to test + * @param {String} token - substring or regex to find + * @returns {Boolean} - true if str contains token + */ + function* contains(str, token) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + var result; + + if (typeof token === 'string') { + result = (str.indexOf(token) !== -1); + } else { + var matches = yield* evaluateMatcher(token, str); + result = (typeof matches !== 'undefined'); + } + + return result; + } + + /** + * Match a string with a regex returning an array of object containing details of each match + * @param {String} str - string + * @param {String} regex - the regex applied to the string + * @param {Integer} [limit] - max number of matches to return + * @returns {Array} The array of match objects + */ + function* match(str, regex, limit) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + // limit, if specified, must be a non-negative number + if (limit < 0) { + throw { + stack: (new Error()).stack, + value: limit, + code: 'D3040', + index: 3 + }; + } + + var result = createSequence(); + + if (typeof limit === 'undefined' || limit > 0) { + var count = 0; + var matches = yield* evaluateMatcher(regex, str); + if (typeof matches !== 'undefined') { + while (typeof matches !== 'undefined' && (typeof limit === 'undefined' || count < limit)) { + result.push({ + match: matches.match, + index: matches.start, + groups: matches.groups + }); + matches = yield* evaluateMatcher(matches.next); + count++; + } + } + } + + return result; + } + + /** + * Match a string with a regex returning an array of object containing details of each match + * @param {String} str - string + * @param {String} pattern - the substring/regex applied to the string + * @param {String} replacement - text to replace the matched substrings + * @param {Integer} [limit] - max number of matches to return + * @returns {Array} The array of match objects + */ + function* replace(str, pattern, replacement, limit) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + var self = this; + + // pattern cannot be an empty string + if (pattern === '') { + throw { + code: "D3010", + stack: (new Error()).stack, + value: pattern, + index: 2 + }; + } + + // limit, if specified, must be a non-negative number + if (limit < 0) { + throw { + code: "D3011", + stack: (new Error()).stack, + value: limit, + index: 4 + }; + } + + var replacer; + if (typeof replacement === 'string') { + replacer = function (regexMatch) { + var substitute = ''; + // scan forward, copying the replacement text into the substitute string + // and replace any occurrence of $n with the values matched by the regex + var position = 0; + var index = replacement.indexOf('$', position); + while (index !== -1 && position < replacement.length) { + substitute += replacement.substring(position, index); + position = index + 1; + var dollarVal = replacement.charAt(position); + if (dollarVal === '$') { + // literal $ + substitute += '$'; + position++; + } else if (dollarVal === '0') { + substitute += regexMatch.match; + position++; + } else { + var maxDigits; + if (regexMatch.groups.length === 0) { + // no sub-matches; any $ followed by a digit will be replaced by an empty string + maxDigits = 1; + } else { + // max number of digits to parse following the $ + maxDigits = Math.floor(Math.log(regexMatch.groups.length) * Math.LOG10E) + 1; + } + index = parseInt(replacement.substring(position, position + maxDigits), 10); + if (maxDigits > 1 && index > regexMatch.groups.length) { + index = parseInt(replacement.substring(position, position + maxDigits - 1), 10); + } + if (!isNaN(index)) { + if (regexMatch.groups.length > 0) { + var submatch = regexMatch.groups[index - 1]; + if (typeof submatch !== 'undefined') { + substitute += submatch; + } + } + position += index.toString().length; + } else { + // not a capture group, treat the $ as literal + substitute += '$'; + } + } + index = replacement.indexOf('$', position); + } + substitute += replacement.substring(position); + return substitute; + }; + } else { + replacer = replacement; + } + + var result = ''; + var position = 0; + + if (typeof limit === 'undefined' || limit > 0) { + var count = 0; + if (typeof pattern === 'string') { + var index = str.indexOf(pattern, position); + while (index !== -1 && (typeof limit === 'undefined' || count < limit)) { + result += str.substring(position, index); + result += replacement; + position = index + pattern.length; + count++; + index = str.indexOf(pattern, position); + } + result += str.substring(position); + } else { + var matches = yield* evaluateMatcher(pattern, str); + if (typeof matches !== 'undefined') { + while (typeof matches !== 'undefined' && (typeof limit === 'undefined' || count < limit)) { + result += str.substring(position, matches.start); + var replacedWith = replacer.apply(self, [matches]); + if (isIterable(replacedWith)) { + replacedWith = yield* replacedWith; + } + // check replacedWith is a string + if (typeof replacedWith === 'string') { + result += replacedWith; + } else { + // not a string - throw error + throw { + code: "D3012", + stack: (new Error()).stack, + value: replacedWith + }; + } + position = matches.start + matches.match.length; + count++; + matches = yield* evaluateMatcher(matches.next); + } + result += str.substring(position); + } else { + result = str; + } + } + } else { + result = str; + } + + return result; + } + + /** + * Base64 encode a string + * @param {String} str - string + * @returns {String} Base 64 encoding of the binary data + */ + function base64encode(str) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + // Use btoa in a browser, or Buffer in Node.js + + var btoa = typeof window !== 'undefined' ? + /* istanbul ignore next */ window.btoa : + function (str) { + // Simply doing `new Buffer` at this point causes Browserify to pull + // in the entire Buffer browser library, which is large and unnecessary. + // Using `global.Buffer` defeats this. + return new global.Buffer.from(str, 'binary').toString('base64'); // eslint-disable-line new-cap + }; + return btoa(str); + } + + /** + * Base64 decode a string + * @param {String} str - string + * @returns {String} Base 64 encoding of the binary data + */ + function base64decode(str) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + // Use btoa in a browser, or Buffer in Node.js + var atob = typeof window !== 'undefined' ? + /* istanbul ignore next */ window.atob : + function (str) { + // Simply doing `new Buffer` at this point causes Browserify to pull + // in the entire Buffer browser library, which is large and unnecessary. + // Using `global.Buffer` defeats this. + return new global.Buffer(str, 'base64').toString('binary'); + }; + 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 + * @param {String} separator - the token or regex that splits the string + * @param {Integer} [limit] - max number of substrings + * @returns {Array} The array of string + */ + function* split(str, separator, limit) { + // undefined inputs always return undefined + if (typeof str === 'undefined') { + return undefined; + } + + // limit, if specified, must be a non-negative number + if (limit < 0) { + throw { + code: "D3020", + stack: (new Error()).stack, + value: limit, + index: 3 + }; + } + + var result = []; + + if (typeof limit === 'undefined' || limit > 0) { + if (typeof separator === 'string') { + result = str.split(separator, limit); + } else { + var count = 0; + var matches = yield* evaluateMatcher(separator, str); + if (typeof matches !== 'undefined') { + var start = 0; + while (typeof matches !== 'undefined' && (typeof limit === 'undefined' || count < limit)) { + result.push(str.substring(start, matches.start)); + start = matches.end; + matches = yield* evaluateMatcher(matches.next); + count++; + } + if (typeof limit === 'undefined' || count < limit) { + result.push(str.substring(start)); + } + } else { + result.push(str); + } + } + } + + return result; + } + + /** + * Join an array of strings + * @param {Array} strs - array of string + * @param {String} [separator] - the token that splits the string + * @returns {String} The concatenated string + */ + function join(strs, separator) { + // undefined inputs always return undefined + if (typeof strs === 'undefined') { + return undefined; + } + + // if separator is not specified, default to empty string + if (typeof separator === 'undefined') { + separator = ""; + } + + return strs.join(separator); + } + + /** + * Formats a number into a decimal string representation using XPath 3.1 F&O fn:format-number spec + * @param {number} value - number to format + * @param {String} picture - picture string definition + * @param {Object} [options] - override locale defaults + * @returns {String} The formatted string + */ + function formatNumber(value, picture, options) { + // undefined inputs always return undefined + if (typeof value === 'undefined') { + return undefined; + } + + var defaults = { + "decimal-separator": ".", + "grouping-separator": ",", + "exponent-separator": "e", + "infinity": "Infinity", + "minus-sign": "-", + "NaN": "NaN", + "percent": "%", + "per-mille": "\u2030", + "zero-digit": "0", + "digit": "#", + "pattern-separator": ";" + }; + + // if `options` is specified, then its entries override defaults + var properties = defaults; + if (typeof options !== 'undefined') { + Object.keys(options).forEach(function (key) { + properties[key] = options[key]; + }); + } + + var decimalDigitFamily = []; + var zeroCharCode = properties['zero-digit'].charCodeAt(0); + for (var ii = zeroCharCode; ii < zeroCharCode + 10; ii++) { + decimalDigitFamily.push(String.fromCharCode(ii)); + } + + var activeChars = decimalDigitFamily.concat([properties['decimal-separator'], properties['exponent-separator'], properties['grouping-separator'], properties.digit, properties['pattern-separator']]); + + var subPictures = picture.split(properties['pattern-separator']); + + if (subPictures.length > 2) { + throw { + code: 'D3080', + stack: (new Error()).stack + }; + } + + var splitParts = function (subpicture) { + var prefix = (function () { + var ch; + for (var ii = 0; ii < subpicture.length; ii++) { + ch = subpicture.charAt(ii); + if (activeChars.indexOf(ch) !== -1 && ch !== properties['exponent-separator']) { + return subpicture.substring(0, ii); + } + } + })(); + var suffix = (function () { + var ch; + for (var ii = subpicture.length - 1; ii >= 0; ii--) { + ch = subpicture.charAt(ii); + if (activeChars.indexOf(ch) !== -1 && ch !== properties['exponent-separator']) { + return subpicture.substring(ii + 1); + } + } + })(); + var activePart = subpicture.substring(prefix.length, subpicture.length - suffix.length); + var mantissaPart, exponentPart, integerPart, fractionalPart; + var exponentPosition = subpicture.indexOf(properties['exponent-separator'], prefix.length); + if (exponentPosition === -1 || exponentPosition > subpicture.length - suffix.length) { + mantissaPart = activePart; + exponentPart = undefined; + } else { + mantissaPart = activePart.substring(0, exponentPosition); + exponentPart = activePart.substring(exponentPosition + 1); + } + var decimalPosition = mantissaPart.indexOf(properties['decimal-separator']); + if (decimalPosition === -1) { + integerPart = mantissaPart; + fractionalPart = suffix; + } else { + integerPart = mantissaPart.substring(0, decimalPosition); + fractionalPart = mantissaPart.substring(decimalPosition + 1); + } + return { + prefix: prefix, + suffix: suffix, + activePart: activePart, + mantissaPart: mantissaPart, + exponentPart: exponentPart, + integerPart: integerPart, + fractionalPart: fractionalPart, + subpicture: subpicture + }; + }; + + // validate the picture string, F&O 4.7.3 + var validate = function (parts) { + var error; + var ii; + var subpicture = parts.subpicture; + var decimalPos = subpicture.indexOf(properties['decimal-separator']); + if (decimalPos !== subpicture.lastIndexOf(properties['decimal-separator'])) { + error = 'D3081'; + } + if (subpicture.indexOf(properties.percent) !== subpicture.lastIndexOf(properties.percent)) { + error = 'D3082'; + } + if (subpicture.indexOf(properties['per-mille']) !== subpicture.lastIndexOf(properties['per-mille'])) { + error = 'D3083'; + } + if (subpicture.indexOf(properties.percent) !== -1 && subpicture.indexOf(properties['per-mille']) !== -1) { + error = 'D3084'; + } + var valid = false; + for (ii = 0; ii < parts.mantissaPart.length; ii++) { + var ch = parts.mantissaPart.charAt(ii); + if (decimalDigitFamily.indexOf(ch) !== -1 || ch === properties.digit) { + valid = true; + break; + } + } + if (!valid) { + error = 'D3085'; + } + var charTypes = parts.activePart.split('').map(function (char) { + return activeChars.indexOf(char) === -1 ? 'p' : 'a'; + }).join(''); + if (charTypes.indexOf('p') !== -1) { + error = 'D3086'; + } + if (decimalPos !== -1) { + if (subpicture.charAt(decimalPos - 1) === properties['grouping-separator'] || subpicture.charAt(decimalPos + 1) === properties['grouping-separator']) { + error = 'D3087'; + } + } else if (parts.integerPart.charAt(parts.integerPart.length - 1) === properties['grouping-separator']) { + error = 'D3088'; + } + if (subpicture.indexOf(properties['grouping-separator'] + properties['grouping-separator']) !== -1) { + error = 'D3089'; + } + var optionalDigitPos = parts.integerPart.indexOf(properties.digit); + if (optionalDigitPos !== -1 && parts.integerPart.substring(0, optionalDigitPos).split('').filter(function (char) { + return decimalDigitFamily.indexOf(char) > -1; + }).length > 0) { + error = 'D3090'; + } + optionalDigitPos = parts.fractionalPart.lastIndexOf(properties.digit); + if (optionalDigitPos !== -1 && parts.fractionalPart.substring(optionalDigitPos).split('').filter(function (char) { + return decimalDigitFamily.indexOf(char) > -1; + }).length > 0) { + error = 'D3091'; + } + var exponentExists = (typeof parts.exponentPart === 'string'); + if (exponentExists && parts.exponentPart.length > 0 && (subpicture.indexOf(properties.percent) !== -1 || subpicture.indexOf(properties['per-mille']) !== -1)) { + error = 'D3092'; + } + if (exponentExists && (parts.exponentPart.length === 0 || parts.exponentPart.split('').filter(function (char) { + return decimalDigitFamily.indexOf(char) === -1; + }).length > 0)) { + error = 'D3093'; + } + if (error) { + throw { + code: error, + stack: (new Error()).stack + }; + } + }; + + // analyse the picture string, F&O 4.7.4 + var analyse = function (parts) { + var getGroupingPositions = function (part, toLeft) { + var positions = []; + var groupingPosition = part.indexOf(properties['grouping-separator']); + while (groupingPosition !== -1) { + var charsToTheRight = (toLeft ? part.substring(0, groupingPosition) : part.substring(groupingPosition)).split('').filter(function (char) { + return decimalDigitFamily.indexOf(char) !== -1 || char === properties.digit; + }).length; + positions.push(charsToTheRight); + groupingPosition = parts.integerPart.indexOf(properties['grouping-separator'], groupingPosition + 1); + } + return positions; + }; + var integerPartGroupingPositions = getGroupingPositions(parts.integerPart); + var regular = function (indexes) { + // are the grouping positions regular? i.e. same interval between each of them + if (indexes.length === 0) { + return 0; + } + var gcd = function (a, b) { + return b === 0 ? a : gcd(b, a % b); + }; + // find the greatest common divisor of all the positions + var factor = indexes.reduce(gcd); + // is every position separated by this divisor? If so, it's regular + for (var index = 1; index <= indexes.length; index++) { + if (indexes.indexOf(index * factor) === -1) { + return 0; + } + } + return factor; + }; + + var regularGrouping = regular(integerPartGroupingPositions); + var fractionalPartGroupingPositions = getGroupingPositions(parts.fractionalPart, true); + + var minimumIntegerPartSize = parts.integerPart.split('').filter(function (char) { + return decimalDigitFamily.indexOf(char) !== -1; + }).length; + var scalingFactor = minimumIntegerPartSize; + + var fractionalPartArray = parts.fractionalPart.split(''); + var minimumFactionalPartSize = fractionalPartArray.filter(function (char) { + return decimalDigitFamily.indexOf(char) !== -1; + }).length; + var maximumFactionalPartSize = fractionalPartArray.filter(function (char) { + return decimalDigitFamily.indexOf(char) !== -1 || char === properties.digit; + }).length; + var exponentPresent = typeof parts.exponentPart === 'string'; + if (minimumIntegerPartSize === 0 && maximumFactionalPartSize === 0) { + if (exponentPresent) { + minimumFactionalPartSize = 1; + maximumFactionalPartSize = 1; + } else { + minimumIntegerPartSize = 1; + } + } + if (exponentPresent && minimumIntegerPartSize === 0 && parts.integerPart.indexOf(properties.digit) !== -1) { + minimumIntegerPartSize = 1; + } + if (minimumIntegerPartSize === 0 && minimumFactionalPartSize === 0) { + minimumFactionalPartSize = 1; + } + var minimumExponentSize = 0; + if (exponentPresent) { + minimumExponentSize = parts.exponentPart.split('').filter(function (char) { + return decimalDigitFamily.indexOf(char) !== -1; + }).length; + } + + return { + integerPartGroupingPositions: integerPartGroupingPositions, + regularGrouping: regularGrouping, + minimumIntegerPartSize: minimumIntegerPartSize, + scalingFactor: scalingFactor, + prefix: parts.prefix, + fractionalPartGroupingPositions: fractionalPartGroupingPositions, + minimumFactionalPartSize: minimumFactionalPartSize, + maximumFactionalPartSize: maximumFactionalPartSize, + minimumExponentSize: minimumExponentSize, + suffix: parts.suffix, + picture: parts.subpicture + }; + }; + + var parts = subPictures.map(splitParts); + parts.forEach(validate); + + var variables = parts.map(analyse); + + var minus_sign = properties['minus-sign']; + var zero_digit = properties['zero-digit']; + var decimal_separator = properties['decimal-separator']; + var grouping_separator = properties['grouping-separator']; + + if (variables.length === 1) { + variables.push(JSON.parse(JSON.stringify(variables[0]))); + variables[1].prefix = minus_sign + variables[1].prefix; + } + + // TODO cache the result of the analysis + + // format the number + // bullet 1: TODO: NaN - not sure we'd ever get this in JSON + var pic; + // bullet 2: + if (value >= 0) { + pic = variables[0]; + } else { + pic = variables[1]; + } + var adjustedNumber; + // bullet 3: + if (pic.picture.indexOf(properties.percent) !== -1) { + adjustedNumber = value * 100; + } else if (pic.picture.indexOf(properties['per-mille']) !== -1) { + adjustedNumber = value * 1000; + } else { + adjustedNumber = value; + } + // bullet 4: + // TODO: infinity - not sure we'd ever get this in JSON + // bullet 5: + var mantissa, exponent; + if (pic.minimumExponentSize === 0) { + mantissa = adjustedNumber; + } else { + // mantissa * 10^exponent = adjustedNumber + var maxMantissa = Math.pow(10, pic.scalingFactor); + var minMantissa = Math.pow(10, pic.scalingFactor - 1); + mantissa = adjustedNumber; + exponent = 0; + while (mantissa < minMantissa) { + mantissa *= 10; + exponent -= 1; + } + while (mantissa > maxMantissa) { + mantissa /= 10; + exponent += 1; + } + } + // bullet 6: + var roundedNumber = round(mantissa, pic.maximumFactionalPartSize); + // bullet 7: + var makeString = function (value, dp) { + var str = Math.abs(value).toFixed(dp); + if (zero_digit !== '0') { + str = str.split('').map(function (digit) { + if (digit >= '0' && digit <= '9') { + return decimalDigitFamily[digit.charCodeAt(0) - 48]; + } else { + return digit; + } + }).join(''); + } + return str; + }; + var stringValue = makeString(roundedNumber, pic.maximumFactionalPartSize); + var decimalPos = stringValue.indexOf('.'); + if (decimalPos === -1) { + stringValue = stringValue + decimal_separator; + } else { + stringValue = stringValue.replace('.', decimal_separator); + } + while (stringValue.charAt(0) === zero_digit) { + stringValue = stringValue.substring(1); + } + while (stringValue.charAt(stringValue.length - 1) === zero_digit) { + stringValue = stringValue.substring(0, stringValue.length - 1); + } + // bullets 8 & 9: + decimalPos = stringValue.indexOf(decimal_separator); + var padLeft = pic.minimumIntegerPartSize - decimalPos; + var padRight = pic.minimumFactionalPartSize - (stringValue.length - decimalPos - 1); + stringValue = (padLeft > 0 ? new Array(padLeft + 1).join(zero_digit) : '') + stringValue; + stringValue = stringValue + (padRight > 0 ? new Array(padRight + 1).join(zero_digit) : ''); + decimalPos = stringValue.indexOf(decimal_separator); + // bullet 10: + if (pic.regularGrouping > 0) { + var groupCount = Math.floor((decimalPos - 1) / pic.regularGrouping); + for (var group = 1; group <= groupCount; group++) { + stringValue = [stringValue.slice(0, decimalPos - group * pic.regularGrouping), grouping_separator, stringValue.slice(decimalPos - group * pic.regularGrouping)].join(''); + } + } else { + pic.integerPartGroupingPositions.forEach(function (pos) { + stringValue = [stringValue.slice(0, decimalPos - pos), grouping_separator, stringValue.slice(decimalPos - pos)].join(''); + decimalPos++; + }); + } + // bullet 11: + decimalPos = stringValue.indexOf(decimal_separator); + pic.fractionalPartGroupingPositions.forEach(function (pos) { + stringValue = [stringValue.slice(0, pos + decimalPos + 1), grouping_separator, stringValue.slice(pos + decimalPos + 1)].join(''); + }); + // bullet 12: + decimalPos = stringValue.indexOf(decimal_separator); + if (pic.picture.indexOf(decimal_separator) === -1 || decimalPos === stringValue.length - 1) { + stringValue = stringValue.substring(0, stringValue.length - 1); + } + // bullet 13: + if (typeof exponent !== 'undefined') { + var stringExponent = makeString(exponent, 0); + padLeft = pic.minimumExponentSize - stringExponent.length; + if (padLeft > 0) { + stringExponent = new Array(padLeft + 1).join(zero_digit) + stringExponent; + } + stringValue = stringValue + properties['exponent-separator'] + (exponent < 0 ? minus_sign : '') + stringExponent; + } + // bullet 14: + stringValue = pic.prefix + stringValue + pic.suffix; + return stringValue; + } + + /** + * Converts a number to a string using a specified number base + * @param {number} value - the number to convert + * @param {number} [radix] - the number base; must be between 2 and 36. Defaults to 10 + * @returns {string} - the converted string + */ + function formatBase(value, radix) { + // undefined inputs always return undefined + if (typeof value === 'undefined') { + return undefined; + } + + value = round(value); + + if (typeof radix === 'undefined') { + radix = 10; + } else { + radix = round(radix); + } + + if (radix < 2 || radix > 36) { + throw { + code: 'D3100', + stack: (new Error()).stack, + value: radix + }; + + } + + var result = value.toString(radix); + + return result; + } + + /** + * Cast argument to number + * @param {Object} arg - Argument + * @returns {Number} numeric value of argument + */ + function number(arg) { + var result; + + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + if (typeof arg === 'number') { + // already a number + result = arg; + } else if (typeof arg === 'string' && /^-?[0-9]+(\.[0-9]+)?([Ee][-+]?[0-9]+)?$/.test(arg) && !isNaN(parseFloat(arg)) && isFinite(arg)) { + result = parseFloat(arg); + } else if (arg === true) { + // boolean true casts to 1 + result = 1; + } else if (arg === false) { + // boolean false casts to 0 + result = 0; + } else { + throw { + code: "D3030", + value: arg, + stack: (new Error()).stack, + index: 1 + }; + } + return result; + } + + /** + * Absolute value of a number + * @param {Number} arg - Argument + * @returns {Number} absolute value of argument + */ + function abs(arg) { + var result; + + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + result = Math.abs(arg); + return result; + } + + /** + * Rounds a number down to integer + * @param {Number} arg - Argument + * @returns {Number} rounded integer + */ + function floor(arg) { + var result; + + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + result = Math.floor(arg); + return result; + } + + /** + * Rounds a number up to integer + * @param {Number} arg - Argument + * @returns {Number} rounded integer + */ + function ceil(arg) { + var result; + + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + result = Math.ceil(arg); + return result; + } + + /** + * Round to half even + * @param {Number} arg - Argument + * @param {Number} [precision] - number of decimal places + * @returns {Number} rounded integer + */ + function round(arg, precision) { + var result; + + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + if (precision) { + // shift the decimal place - this needs to be done in a string since multiplying + // by a power of ten can introduce floating point precision errors which mess up + // this rounding algorithm - See 'Decimal rounding' in + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round + // Shift + var value = arg.toString().split('e'); + arg = +(value[0] + 'e' + (value[1] ? (+value[1] + precision) : precision)); + + } + + // round up to nearest int + result = Math.round(arg); + var diff = result - arg; + if (Math.abs(diff) === 0.5 && Math.abs(result % 2) === 1) { + // rounded the wrong way - adjust to nearest even number + result = result - 1; + } + if (precision) { + // Shift back + value = result.toString().split('e'); + /* istanbul ignore next */ + result = +(value[0] + 'e' + (value[1] ? (+value[1] - precision) : -precision)); + } + if (Object.is(result, -0)) { // ESLint rule 'no-compare-neg-zero' suggests this way + // JSON doesn't do -0 + result = 0; + } + return result; + } + + /** + * Square root of number + * @param {Number} arg - Argument + * @returns {Number} square root + */ + function sqrt(arg) { + var result; + + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + if (arg < 0) { + throw { + stack: (new Error()).stack, + code: "D3060", + index: 1, + value: arg + }; + } + + result = Math.sqrt(arg); + + return result; + } + + /** + * Raises number to the power of the second number + * @param {Number} arg - the base + * @param {Number} exp - the exponent + * @returns {Number} rounded integer + */ + function power(arg, exp) { + var result; + + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + result = Math.pow(arg, exp); + + if (!isFinite(result)) { + throw { + stack: (new Error()).stack, + code: "D3061", + index: 1, + value: arg, + exp: exp + }; + } + + return result; + } + + /** + * Returns a random number 0 <= n < 1 + * @returns {number} random number + */ + function random() { + return Math.random(); + } + + /** + * Evaluate an input and return a boolean + * @param {*} arg - Arguments + * @returns {boolean} Boolean + */ + function boolean(arg) { + // cast arg to its effective boolean value + // boolean: unchanged + // string: zero-length -> false; otherwise -> true + // number: 0 -> false; otherwise -> true + // null -> false + // array: empty -> false; length > 1 -> true + // object: empty -> false; non-empty -> true + // function -> false + + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + var result = false; + if (Array.isArray(arg)) { + if (arg.length === 1) { + result = boolean(arg[0]); + } else if (arg.length > 1) { + var trues = arg.filter(function (val) { + return boolean(val); + }); + result = trues.length > 0; + } + } else if (typeof arg === 'string') { + if (arg.length > 0) { + result = true; + } + } else if (isNumeric(arg)) { + if (arg !== 0) { + result = true; + } + } else if (arg !== null && typeof arg === 'object') { + if (Object.keys(arg).length > 0) { + result = true; + } + } else if (typeof arg === 'boolean' && arg === true) { + result = true; + } + return result; + } + + /** + * returns the Boolean NOT of the arg + * @param {*} arg - argument + * @returns {boolean} - NOT arg + */ + function not(arg) { + return !boolean(arg); + } + + /** + * Helper function to build the arguments to be supplied to the function arg of the + * HOFs map, filter, each, sift and single + * @param {function} func - the function to be invoked + * @param {*} arg1 - the first (required) arg - the value + * @param {*} arg2 - the second (optional) arg - the position (index or key) + * @param {*} arg3 - the third (optional) arg - the whole structure (array or object) + * @returns {*[]} the argument list + */ + function hofFuncArgs(func, arg1, arg2, arg3) { + var func_args = [arg1]; // the first arg (the value) is required + // the other two are optional - only supply it if the function can take it + var length = getFunctionArity(func); + if (length >= 2) { + func_args.push(arg2); + } + if (length >= 3) { + func_args.push(arg3); + } + return func_args; + } + + /** + * Create a map from an array of arguments + * @param {Array} [arr] - array to map over + * @param {Function} func - function to apply + * @returns {Array} Map array + */ + function* map(arr, func) { + // undefined inputs always return undefined + if (typeof arr === 'undefined') { + return undefined; + } + + var result = createSequence(); + // do the map - iterate over the arrays, and invoke func + for (var i = 0; i < arr.length; i++) { + var func_args = hofFuncArgs(func, arr[i], i, arr); + // invoke func + var res = yield* func.apply(this, func_args); + if (typeof res !== 'undefined') { + result.push(res); + } + } + + return result; + } + + /** + * Create a map from an array of arguments + * @param {Array} [arr] - array to filter + * @param {Function} func - predicate function + * @returns {Array} Map array + */ + function* filter(arr, func) { // eslint-disable-line require-yield + // undefined inputs always return undefined + if (typeof arr === 'undefined') { + return undefined; + } + + var result = createSequence(); + + for (var i = 0; i < arr.length; i++) { + var entry = arr[i]; + var func_args = hofFuncArgs(func, entry, i, arr); + // invoke func + var res = yield* func.apply(this, func_args); + if (boolean(res)) { + result.push(entry); + } + } + + return result; + } + + /** + * Given an array, find the single element matching a specified condition + * Throws an exception if the number of matching elements is not exactly one + * @param {Array} [arr] - array to filter + * @param {Function} [func] - predicate function + * @returns {*} Matching element + */ + function* single(arr, func) { // eslint-disable-line require-yield + // undefined inputs always return undefined + if (typeof arr === 'undefined') { + return undefined; + } + + var hasFoundMatch = false; + var result; + + for (var i = 0; i < arr.length; i++) { + var entry = arr[i]; + var positiveResult = true; + if (typeof func !== 'undefined') { + var func_args = hofFuncArgs(func, entry, i, arr); + // invoke func + var res = yield* func.apply(this, func_args); + positiveResult = boolean(res); + } + if (positiveResult) { + if(!hasFoundMatch) { + result = entry; + hasFoundMatch = true; + } else { + throw { + stack: (new Error()).stack, + code: "D3138", + index: i + }; + } + } + } + + if(!hasFoundMatch) { + throw { + stack: (new Error()).stack, + code: "D3139" + }; + } + + return result; + } + + /** + * Convolves (zips) each value from a set of arrays + * @param {Array} [args] - arrays to zip + * @returns {Array} Zipped array + */ + function zip() { + // this can take a variable number of arguments + var result = []; + var args = Array.prototype.slice.call(arguments); + // length of the shortest array + var length = Math.min.apply(Math, args.map(function (arg) { + if (Array.isArray(arg)) { + return arg.length; + } + return 0; + })); + for (var i = 0; i < length; i++) { + var tuple = args.map((arg) => { + return arg[i]; + }); + result.push(tuple); + } + return result; + } + + /** + * Fold left function + * @param {Array} sequence - Sequence + * @param {Function} func - Function + * @param {Object} init - Initial value + * @returns {*} Result + */ + function* foldLeft(sequence, func, init) { + // undefined inputs always return undefined + if (typeof sequence === 'undefined') { + return undefined; + } + + var result; + + var arity = getFunctionArity(func); + if (arity < 2) { + throw { + stack: (new Error()).stack, + code: "D3050", + index: 1 + }; + } + + var index; + if (typeof init === 'undefined' && sequence.length > 0) { + result = sequence[0]; + index = 1; + } else { + result = init; + index = 0; + } + + while (index < sequence.length) { + var args = [result, sequence[index]]; + if (arity >= 3) { + args.push(index); + } + if (arity >= 4) { + args.push(sequence); + } + result = yield* func.apply(this, args); + index++; + } + + return result; + } + + /** + * Return keys for an object + * @param {Object} arg - Object + * @returns {Array} Array of keys + */ + function keys(arg) { + var result = createSequence(); + + if (Array.isArray(arg)) { + // merge the keys of all of the items in the array + var merge = {}; + arg.forEach(function (item) { + var allkeys = keys(item); + allkeys.forEach(function (key) { + merge[key] = true; + }); + }); + result = keys(merge); + } else if (arg !== null && typeof arg === 'object' && !(isLambda(arg))) { + Object.keys(arg).forEach(key => result.push(key)); + } + return result; + } + + /** + * Return value from an object for a given key + * @param {Object} input - Object/Array + * @param {String} key - Key in object + * @returns {*} Value of key in object + */ + function lookup(input, key) { + // lookup the 'name' item in the input + var result; + if (Array.isArray(input)) { + result = createSequence(); + for(var ii = 0; ii < input.length; ii++) { + var res = lookup(input[ii], key); + if (typeof res !== 'undefined') { + if (Array.isArray(res)) { + result.push(...res); + } else { + result.push(res); + } + } + } + } else if (input !== null && typeof input === 'object') { + result = input[key]; + } + return result; + } + + /** + * Append second argument to first + * @param {Array|Object} arg1 - First argument + * @param {Array|Object} arg2 - Second argument + * @returns {*} Appended arguments + */ + function append(arg1, arg2) { + // disregard undefined args + if (typeof arg1 === 'undefined') { + return arg2; + } + if (typeof arg2 === 'undefined') { + return arg1; + } + // if either argument is not an array, make it so + if (!Array.isArray(arg1)) { + arg1 = createSequence(arg1); + } + if (!Array.isArray(arg2)) { + arg2 = [arg2]; + } + return arg1.concat(arg2); + } + + /** + * Determines if the argument is undefined + * @param {*} arg - argument + * @returns {boolean} False if argument undefined, otherwise true + */ + function exists(arg) { + if (typeof arg === 'undefined') { + return false; + } else { + return true; + } + } + + /** + * Splits an object into an array of object with one property each + * @param {*} arg - the object to split + * @returns {*} - the array + */ + function spread(arg) { + var result = createSequence(); + + if (Array.isArray(arg)) { + // spread all of the items in the array + arg.forEach(function (item) { + result = append(result, spread(item)); + }); + } else if (arg !== null && typeof arg === 'object' && !isLambda(arg)) { + for (var key in arg) { + var obj = {}; + obj[key] = arg[key]; + result.push(obj); + } + } else { + result = arg; + } + return result; + } + + /** + * Merges an array of objects into a single object. Duplicate properties are + * overridden by entries later in the array + * @param {*} arg - the objects to merge + * @returns {*} - the object + */ + function merge(arg) { + // undefined inputs always return undefined + if (typeof arg === 'undefined') { + return undefined; + } + + var result = {}; + + arg.forEach(function (obj) { + for (var prop in obj) { + result[prop] = obj[prop]; + } + }); + return result; + } + + /** + * Reverses the order of items in an array + * @param {Array} arr - the array to reverse + * @returns {Array} - the reversed array + */ + function reverse(arr) { + // undefined inputs always return undefined + if (typeof arr === 'undefined') { + return undefined; + } + + if (arr.length <= 1) { + return arr; + } + + var length = arr.length; + var result = new Array(length); + for (var i = 0; i < length; i++) { + result[length - i - 1] = arr[i]; + } + + return result; + } + + /** + * + * @param {*} obj - the input object to iterate over + * @param {*} func - the function to apply to each key/value pair + * @returns {Array} - the resultant array + */ + function* each(obj, func) { + var result = createSequence(); + + for (var key in obj) { + var func_args = hofFuncArgs(func, obj[key], key, obj); + // invoke func + var val = yield* func.apply(this, func_args); + if(typeof val !== 'undefined') { + result.push(val); + } + } + + return result; + } + + /** + * + * @param {string} [message] - the message to attach to the error + * @throws custom error with code 'D3137' + */ + function error(message) { + throw { + code: "D3137", + stack: (new Error()).stack, + message: message || "$error() function evaluated" + }; + } + + /** + * + * @param {boolean} condition - the condition to evaluate + * @param {string} [message] - the message to attach to the error + * @throws custom error with code 'D3137' + * @returns {undefined} + */ + function assert(condition, message) { + if(!condition) { + throw { + code: "D3141", + stack: (new Error()).stack, + message: message || "$assert() statement failed" + }; + } + + return undefined; + } + + /** + * + * @param {*} [value] - the input to which the type will be checked + * @returns {string} - the type of the input + */ + function type(value) { + if (value === undefined) { + return undefined; + } + + if (value === null) { + return 'null'; + } + + if (isNumeric(value)) { + return 'number'; + } + + if (typeof value === 'string') { + return 'string'; + } + + if (typeof value === 'boolean') { + return 'boolean'; + } + + if(Array.isArray(value)) { + return 'array'; + } + + if(isFunction(value)) { + return 'function'; + } + + return 'object'; + } + + /** + * Implements the merge sort (stable) with optional comparator function + * + * @param {Array} arr - the array to sort + * @param {*} comparator - comparator function + * @returns {Array} - sorted array + */ + function* sort(arr, comparator) { + // undefined inputs always return undefined + if (typeof arr === 'undefined') { + return undefined; + } + + if (arr.length <= 1) { + return arr; + } + + var comp; + if (typeof comparator === 'undefined') { + // inject a default comparator - only works for numeric or string arrays + if (!isArrayOfNumbers(arr) && !isArrayOfStrings(arr)) { + throw { + stack: (new Error()).stack, + code: "D3070", + index: 1 + }; + } + + comp = function* (a, b) { // eslint-disable-line require-yield + return a > b; + }; + } else { + // for internal usage of functionSort (i.e. order-by syntax) + comp = comparator; + } + + var merge = function* (l, r) { + var merge_iter = function* (result, left, right) { + if (left.length === 0) { + Array.prototype.push.apply(result, right); + } else if (right.length === 0) { + Array.prototype.push.apply(result, left); + } else if (yield* comp(left[0], right[0])) { // invoke the comparator function + // if it returns true - swap left and right + result.push(right[0]); + yield* merge_iter(result, left, right.slice(1)); + } else { + // otherwise keep the same order + result.push(left[0]); + yield* merge_iter(result, left.slice(1), right); + } + }; + var merged = []; + yield* merge_iter(merged, l, r); + return merged; + }; + + var msort = function* (array) { + if (!Array.isArray(array) || array.length <= 1) { + return array; + } else { + var middle = Math.floor(array.length / 2); + var left = array.slice(0, middle); + var right = array.slice(middle); + left = yield* msort(left); + right = yield* msort(right); + return yield* merge(left, right); + } + }; + + var result = yield* msort(arr); + + return result; + } + + /** + * Randomly shuffles the contents of an array + * @param {Array} arr - the input array + * @returns {Array} the shuffled array + */ + function shuffle(arr) { + // undefined inputs always return undefined + if (typeof arr === 'undefined') { + return undefined; + } + + if (arr.length <= 1) { + return arr; + } + + // shuffle using the 'inside-out' variant of the Fisher-Yates algorithm + var result = new Array(arr.length); + for (var i = 0; i < arr.length; i++) { + var j = Math.floor(Math.random() * (i + 1)); // random integer such that 0 ≤ j ≤ i + if (i !== j) { + result[i] = result[j]; + } + result[j] = arr[i]; + } + + return result; + } + + /** + * Returns the values that appear in a sequence, with duplicates eliminated. + * @param {Array} arr - An array or sequence of values + * @returns {Array} - sequence of distinct values + */ + function distinct(arr) { + // undefined inputs always return undefined + if (typeof arr === 'undefined') { + return undefined; + } + + if(!Array.isArray(arr) || arr.length <= 1) { + return arr; + } + + var results = isSequence(arr) ? createSequence() : []; + + for(var ii = 0; ii < arr.length; ii++) { + var value = arr[ii]; + // is this value already in the result sequence? + var includes = false; + for(var jj = 0; jj < results.length; jj++) { + if (deepEquals(value, results[jj])) { + includes = true; + break; + } + } + if(!includes) { + results.push(value); + } + } + return results; + } + + /** + * Applies a predicate function to each key/value pair in an object, and returns an object containing + * only the key/value pairs that passed the predicate + * + * @param {object} arg - the object to be sifted + * @param {object} func - the predicate function (lambda or native) + * @returns {object} - sifted object + */ + function* sift(arg, func) { + var result = {}; + + for (var item in arg) { + var entry = arg[item]; + var func_args = hofFuncArgs(func, entry, item, arg); + // invoke func + var res = yield* func.apply(this, func_args); + if (boolean(res)) { + result[item] = entry; + } + } + + // empty objects should be changed to undefined + if (Object.keys(result).length === 0) { + result = undefined; + } + + return result; + } + + return { + sum, count, max, min, average, + string, substring, substringBefore, substringAfter, lowercase, uppercase, length, trim, pad, + match, contains, replace, split, join, + formatNumber, formatBase, number, floor, ceil, round, abs, sqrt, power, random, + boolean, not, + map, zip, filter, single, foldLeft, sift, + keys, lookup, append, exists, spread, merge, reverse, each, error, assert, type, sort, shuffle, distinct, + base64encode, base64decode, encodeUrlComponent, encodeUrl, decodeUrlComponent, decodeUrl + }; +})(); + +module.exports = functions; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./utils":6}],3:[function(require,module,exports){ +/** + * © Copyright IBM Corp. 2016, 2017 All Rights Reserved + * Project name: JSONata + * This project is licensed under the MIT License, see LICENSE + */ + +/** + * @module JSONata + * @description JSON query and transformation language + */ + +var datetime = require('./datetime'); +var fn = require('./functions'); +var utils = require('./utils'); +var parser = require('./parser'); +var parseSignature = require('./signature'); + +/** + * jsonata + * @function + * @param {Object} expr - JSONata expression + * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression + */ +var jsonata = (function() { + 'use strict'; + + var isNumeric = utils.isNumeric; + var isArrayOfStrings = utils.isArrayOfStrings; + var isArrayOfNumbers = utils.isArrayOfNumbers; + var createSequence = utils.createSequence; + var isSequence = utils.isSequence; + var isFunction = utils.isFunction; + var isLambda = utils.isLambda; + var isIterable = utils.isIterable; + var getFunctionArity = utils.getFunctionArity; + var isDeepEqual = utils.isDeepEqual; + + // Start of Evaluator code + + var staticFrame = createFrame(null); + + /** + * Evaluate expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluate(expr, input, environment) { + var result; + + var entryCallback = environment.lookup('__evaluate_entry'); + if(entryCallback) { + entryCallback(expr, input, environment); + } + + switch (expr.type) { + case 'path': + result = yield * evaluatePath(expr, input, environment); + break; + case 'binary': + result = yield * evaluateBinary(expr, input, environment); + break; + case 'unary': + result = yield * evaluateUnary(expr, input, environment); + break; + case 'name': + result = evaluateName(expr, input, environment); + break; + case 'string': + case 'number': + case 'value': + result = evaluateLiteral(expr, input, environment); + break; + case 'wildcard': + result = evaluateWildcard(expr, input, environment); + break; + case 'descendant': + result = evaluateDescendants(expr, input, environment); + break; + case 'parent': + result = environment.lookup(expr.slot.label); + break; + case 'condition': + result = yield * evaluateCondition(expr, input, environment); + break; + case 'block': + result = yield * evaluateBlock(expr, input, environment); + break; + case 'bind': + result = yield * evaluateBindExpression(expr, input, environment); + break; + case 'regex': + result = evaluateRegex(expr, input, environment); + break; + case 'function': + result = yield * evaluateFunction(expr, input, environment); + break; + case 'variable': + result = evaluateVariable(expr, input, environment); + break; + case 'lambda': + result = evaluateLambda(expr, input, environment); + break; + case 'partial': + result = yield * evaluatePartialApplication(expr, input, environment); + break; + case 'apply': + result = yield * evaluateApplyExpression(expr, input, environment); + break; + case 'transform': + result = evaluateTransformExpression(expr, input, environment); + break; + } + + if(environment.async && + (typeof result === 'undefined' || result === null || typeof result.then !== 'function')) { + result = Promise.resolve(result); + } + if(environment.async && typeof result.then === 'function' && expr.nextFunction && typeof result[expr.nextFunction] === 'function') { + // although this is a 'thenable', it is chaining a different function + // so don't yield since yielding will trigger the .then() + } else { + result = yield result; + } + + if (Object.prototype.hasOwnProperty.call(expr, 'predicate')) { + for(var ii = 0; ii < expr.predicate.length; ii++) { + result = yield * evaluateFilter(expr.predicate[ii].expr, result, environment); + } + } + + if (expr.type !== 'path' && Object.prototype.hasOwnProperty.call(expr, 'group')) { + result = yield * evaluateGroupExpression(expr.group, result, environment); + } + + var exitCallback = environment.lookup('__evaluate_exit'); + if(exitCallback) { + exitCallback(expr, input, environment, result); + } + + if(result && isSequence(result) && !result.tupleStream) { + if(expr.keepArray) { + result.keepSingleton = true; + } + if(result.length === 0) { + result = undefined; + } else if(result.length === 1) { + result = result.keepSingleton ? result : result[0]; + } + + } + + return result; + } + + /** + * Evaluate path expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluatePath(expr, input, environment) { + var inputSequence; + // expr is an array of steps + // if the first step is a variable reference ($...), including root reference ($$), + // then the path is absolute rather than relative + if (Array.isArray(input) && expr.steps[0].type !== 'variable') { + inputSequence = input; + } else { + // if input is not an array, make it so + inputSequence = createSequence(input); + } + + var resultSequence; + var isTupleStream = false; + var tupleBindings = undefined; + + // evaluate each step in turn + for(var ii = 0; ii < expr.steps.length; ii++) { + var step = expr.steps[ii]; + + if(step.tuple) { + isTupleStream = true; + } + + // if the first step is an explicit array constructor, then just evaluate that (i.e. don't iterate over a context array) + if(ii === 0 && step.consarray) { + resultSequence = yield * evaluate(step, inputSequence, environment); + } else { + if(isTupleStream) { + tupleBindings = yield * evaluateTupleStep(step, inputSequence, tupleBindings, environment); + } else { + resultSequence = yield * evaluateStep(step, inputSequence, environment, ii === expr.steps.length - 1); + } + } + + if (!isTupleStream && (typeof resultSequence === 'undefined' || resultSequence.length === 0)) { + break; + } + + if(typeof step.focus === 'undefined') { + inputSequence = resultSequence; + } + + } + + if(isTupleStream) { + if(expr.tuple) { + // tuple stream is carrying ancestry information - keep this + resultSequence = tupleBindings; + } else { + resultSequence = createSequence(); + for (ii = 0; ii < tupleBindings.length; ii++) { + resultSequence.push(tupleBindings[ii]['@']); + } + } + } + + if(expr.keepSingletonArray) { + if(!isSequence(resultSequence)) { + resultSequence = createSequence(resultSequence); + } + resultSequence.keepSingleton = true; + } + + if (expr.hasOwnProperty('group')) { + resultSequence = yield* evaluateGroupExpression(expr.group, isTupleStream ? tupleBindings : resultSequence, environment) + } + + return resultSequence; + } + + function createFrameFromTuple(environment, tuple) { + var frame = createFrame(environment); + for(const prop in tuple) { + frame.bind(prop, tuple[prop]); + } + return frame; + } + + /** + * Evaluate a step within a path + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @param {boolean} lastStep - flag the last step in a path + * @returns {*} Evaluated input data + */ + function* evaluateStep(expr, input, environment, lastStep) { + var result; + if(expr.type === 'sort') { + result = yield* evaluateSortExpression(expr, input, environment); + if(expr.stages) { + result = yield* evaluateStages(expr.stages, result, environment); + } + return result; + } + + result = createSequence(); + + for(var ii = 0; ii < input.length; ii++) { + var res = yield * evaluate(expr, input[ii], environment); + if(expr.stages) { + for(var ss = 0; ss < expr.stages.length; ss++) { + res = yield* evaluateFilter(expr.stages[ss].expr, res, environment); + } + } + if(typeof res !== 'undefined') { + result.push(res); + } + } + + var resultSequence = createSequence(); + if(lastStep && result.length === 1 && Array.isArray(result[0]) && !isSequence(result[0])) { + resultSequence = result[0]; + } else { + // flatten the sequence + result.forEach(function(res) { + if (!Array.isArray(res) || res.cons) { + // it's not an array - just push into the result sequence + resultSequence.push(res); + } else { + // res is a sequence - flatten it into the parent sequence + Array.prototype.push.apply(resultSequence, res); + } + }); + } + + return resultSequence; + } + + function* evaluateStages(stages, input, environment) { + var result = input; + for(var ss = 0; ss < stages.length; ss++) { + var stage = stages[ss]; + switch(stage.type) { + case 'filter': + result = yield * evaluateFilter(stage.expr, result, environment); + break; + case 'index': + for(var ee = 0; ee < result.length; ee++) { + var tuple = result[ee]; + tuple[stage.value] = ee; + } + break; + } + } + return result; + } + + /** + * Evaluate a step within a path + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} tupleBindings - The tuple stream + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluateTupleStep(expr, input, tupleBindings, environment) { + var result; + if(expr.type === 'sort') { + if(tupleBindings) { + result = yield* evaluateSortExpression(expr, tupleBindings, environment); + } else { + var sorted = yield* evaluateSortExpression(expr, input, environment); + result = createSequence(); + result.tupleStream = true; + for(var ss = 0; ss < sorted.length; ss++) { + var tuple = {'@': sorted[ss]}; + tuple[expr.index] = ss; + result.push(tuple); + } + } + if(expr.stages) { + result = yield* evaluateStages(expr.stages, result, environment); + } + return result; + } + + result = createSequence(); + result.tupleStream = true; + var stepEnv = environment; + if(tupleBindings === undefined) { + tupleBindings = input.map(item => { return {'@': item} }); + } + + for(var ee = 0; ee < tupleBindings.length; ee++) { + stepEnv = createFrameFromTuple(environment, tupleBindings[ee]); + var res = yield* evaluate(expr, tupleBindings[ee]['@'], stepEnv); + // res is the binding sequence for the output tuple stream + if(typeof res !== 'undefined') { + if (!Array.isArray(res)) { + res = [res]; + } + for (var bb = 0; bb < res.length; bb++) { + tuple = {}; + Object.assign(tuple, tupleBindings[ee]); + if(res.tupleStream) { + Object.assign(tuple, res[bb]); + } else { + if (expr.focus) { + tuple[expr.focus] = res[bb]; + tuple['@'] = tupleBindings[ee]['@']; + } else { + tuple['@'] = res[bb]; + } + if (expr.index) { + tuple[expr.index] = bb; + } + if (expr.ancestor) { + tuple[expr.ancestor.label] = tupleBindings[ee]['@']; + } + } + result.push(tuple); + } + } + } + + if(expr.stages) { + result = yield * evaluateStages(expr.stages, result, environment); + } + + return result; + } + + /** + * Apply filter predicate to input data + * @param {Object} predicate - filter expression + * @param {Object} input - Input data to apply predicates against + * @param {Object} environment - Environment + * @returns {*} Result after applying predicates + */ + function* evaluateFilter(predicate, input, environment) { + var results = createSequence(); + if( input && input.tupleStream) { + results.tupleStream = true; + } + if (!Array.isArray(input)) { + input = createSequence(input); + } + if (predicate.type === 'number') { + var index = Math.floor(predicate.value); // round it down + if (index < 0) { + // count in from end of array + index = input.length + index; + } + var item = input[index]; + if(typeof item !== 'undefined') { + if(Array.isArray(item)) { + results = item; + } else { + results.push(item); + } + } + } else { + for (index = 0; index < input.length; index++) { + var item = input[index]; + var context = item; + var env = environment; + if(input.tupleStream) { + context = item['@']; + env = createFrameFromTuple(environment, item); + } + var res = yield* evaluate(predicate, context, env); + if (isNumeric(res)) { + res = [res]; + } + if (isArrayOfNumbers(res)) { + res.forEach(function (ires) { + // round it down + var ii = Math.floor(ires); + if (ii < 0) { + // count in from end of array + ii = input.length + ii; + } + if (ii === index) { + results.push(item); + } + }); + } else if (fn.boolean(res)) { // truthy + results.push(item); + } + } + } + return results; + } + + /** + * Evaluate binary expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function * evaluateBinary(expr, input, environment) { + var result; + var lhs = yield * evaluate(expr.lhs, input, environment); + var rhs = yield * evaluate(expr.rhs, input, environment); + var op = expr.value; + + try { + switch (op) { + case '+': + case '-': + case '*': + case '/': + case '%': + result = evaluateNumericExpression(lhs, rhs, op); + break; + case '=': + case '!=': + result = evaluateEqualityExpression(lhs, rhs, op); + break; + case '<': + case '<=': + case '>': + case '>=': + result = evaluateComparisonExpression(lhs, rhs, op); + break; + case '&': + result = evaluateStringConcat(lhs, rhs); + break; + case 'and': + case 'or': + result = evaluateBooleanExpression(lhs, rhs, op); + break; + case '..': + result = evaluateRangeExpression(lhs, rhs); + break; + case 'in': + result = evaluateIncludesExpression(lhs, rhs); + break; + } + } catch(err) { + err.position = expr.position; + err.token = op; + throw err; + } + return result; + } + + /** + * Evaluate unary expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluateUnary(expr, input, environment) { + var result; + + switch (expr.value) { + case '-': + result = yield * evaluate(expr.expression, input, environment); + if(typeof result === 'undefined') { + result = undefined; + } else if (isNumeric(result)) { + result = -result; + } else { + throw { + code: "D1002", + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: result + }; + } + break; + case '[': + // array constructor - evaluate each item + result = []; + for(var ii = 0; ii < expr.expressions.length; ii++) { + var item = expr.expressions[ii]; + var value = yield * evaluate(item, input, environment); + if (typeof value !== 'undefined') { + if(item.value === '[') { + result.push(value); + } else { + result = fn.append(result, value); + } + } + } + if(expr.consarray) { + Object.defineProperty(result, 'cons', { + enumerable: false, + configurable: false, + value: true + }); + } + break; + case '{': + // object constructor - apply grouping + result = yield * evaluateGroupExpression(expr, input, environment); + break; + + } + return result; + } + + /** + * Evaluate name object against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateName(expr, input, environment) { + // lookup the 'name' item in the input + return fn.lookup(input, expr.value); + } + + /** + * Evaluate literal against input data + * @param {Object} expr - JSONata expression + * @returns {*} Evaluated input data + */ + function evaluateLiteral(expr) { + return expr.value; + } + + /** + * Evaluate wildcard against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @returns {*} Evaluated input data + */ + function evaluateWildcard(expr, input) { + var results = createSequence(); + if (input !== null && typeof input === 'object') { + Object.keys(input).forEach(function (key) { + var value = input[key]; + if(Array.isArray(value)) { + value = flatten(value); + results = fn.append(results, value); + } else { + results.push(value); + } + }); + } + + // result = normalizeSequence(results); + return results; + } + + /** + * Returns a flattened array + * @param {Array} arg - the array to be flatten + * @param {Array} flattened - carries the flattened array - if not defined, will initialize to [] + * @returns {Array} - the flattened array + */ + function flatten(arg, flattened) { + if(typeof flattened === 'undefined') { + flattened = []; + } + if(Array.isArray(arg)) { + arg.forEach(function (item) { + flatten(item, flattened); + }); + } else { + flattened.push(arg); + } + return flattened; + } + + /** + * Evaluate descendants against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @returns {*} Evaluated input data + */ + function evaluateDescendants(expr, input) { + var result; + var resultSequence = createSequence(); + if (typeof input !== 'undefined') { + // traverse all descendants of this object/array + recurseDescendants(input, resultSequence); + if (resultSequence.length === 1) { + result = resultSequence[0]; + } else { + result = resultSequence; + } + } + return result; + } + + /** + * Recurse through descendants + * @param {Object} input - Input data + * @param {Object} results - Results + */ + function recurseDescendants(input, results) { + // this is the equivalent of //* in XPath + if (!Array.isArray(input)) { + results.push(input); + } + if (Array.isArray(input)) { + input.forEach(function (member) { + recurseDescendants(member, results); + }); + } else if (input !== null && typeof input === 'object') { + Object.keys(input).forEach(function (key) { + recurseDescendants(input[key], results); + }); + } + } + + /** + * Evaluate numeric expression against input data + * @param {Object} lhs - LHS value + * @param {Object} rhs - RHS value + * @param {Object} op - opcode + * @returns {*} Result + */ + function evaluateNumericExpression(lhs, rhs, op) { + var result; + + if (typeof lhs !== 'undefined' && !isNumeric(lhs)) { + throw { + code: "T2001", + stack: (new Error()).stack, + value: lhs + }; + } + if (typeof rhs !== 'undefined' && !isNumeric(rhs)) { + throw { + code: "T2002", + stack: (new Error()).stack, + value: rhs + }; + } + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + // if either side is undefined, the result is undefined + return result; + } + + switch (op) { + case '+': + result = lhs + rhs; + break; + case '-': + result = lhs - rhs; + break; + case '*': + result = lhs * rhs; + break; + case '/': + result = lhs / rhs; + break; + case '%': + result = lhs % rhs; + break; + } + return result; + } + + /** + * Evaluate equality expression against input data + * @param {Object} lhs - LHS value + * @param {Object} rhs - RHS value + * @param {Object} op - opcode + * @returns {*} Result + */ + function evaluateEqualityExpression(lhs, rhs, op) { + var result; + + // type checks + var ltype = typeof lhs; + var rtype = typeof rhs; + + if (ltype === 'undefined' || rtype === 'undefined') { + // if either side is undefined, the result is false + return false; + } + + switch (op) { + case '=': + result = isDeepEqual(lhs, rhs); + break; + case '!=': + result = !isDeepEqual(lhs, rhs); + break; + } + return result; + } + + /** + * Evaluate comparison expression against input data + * @param {Object} lhs - LHS value + * @param {Object} rhs - RHS value + * @param {Object} op - opcode + * @returns {*} Result + */ + function evaluateComparisonExpression(lhs, rhs, op) { + var result; + + // type checks + var ltype = typeof lhs; + var rtype = typeof rhs; + + var lcomparable = (ltype === 'undefined' || ltype === 'string' || ltype === 'number'); + var rcomparable = (rtype === 'undefined' || rtype === 'string' || rtype === 'number'); + + // if either aa or bb are not comparable (string or numeric) values, then throw an error + if (!lcomparable || !rcomparable) { + throw { + code: "T2010", + stack: (new Error()).stack, + value: !(ltype === 'string' || ltype === 'number') ? lhs : rhs + }; + } + + // if either side is undefined, the result is undefined + if (ltype === 'undefined' || rtype === 'undefined') { + return undefined; + } + + //if aa and bb are not of the same type + if (ltype !== rtype) { + throw { + code: "T2009", + stack: (new Error()).stack, + value: lhs, + value2: rhs + }; + } + + switch (op) { + case '<': + result = lhs < rhs; + break; + case '<=': + result = lhs <= rhs; + break; + case '>': + result = lhs > rhs; + break; + case '>=': + result = lhs >= rhs; + break; + } + return result; + } + + /** + * Inclusion operator - in + * + * @param {Object} lhs - LHS value + * @param {Object} rhs - RHS value + * @returns {boolean} - true if lhs is a member of rhs + */ + function evaluateIncludesExpression(lhs, rhs) { + var result = false; + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + // if either side is undefined, the result is false + return false; + } + + if(!Array.isArray(rhs)) { + rhs = [rhs]; + } + + for(var i = 0; i < rhs.length; i++) { + if(rhs[i] === lhs) { + result = true; + break; + } + } + + return result; + } + + /** + * Evaluate boolean expression against input data + * @param {Object} lhs - LHS value + * @param {Object} rhs - RHS value + * @param {Object} op - opcode + * @returns {*} Result + */ + function evaluateBooleanExpression(lhs, rhs, op) { + var result; + + var lBool = fn.boolean(lhs); + var rBool = fn.boolean(rhs); + + if (typeof lBool === 'undefined') { + lBool = false; + } + + if (typeof rBool === 'undefined') { + rBool = false; + } + + switch (op) { + case 'and': + result = lBool && rBool; + break; + case 'or': + result = lBool || rBool; + break; + } + return result; + } + + /** + * Evaluate string concatenation against input data + * @param {Object} lhs - LHS value + * @param {Object} rhs - RHS value + * @returns {string|*} Concatenated string + */ + function evaluateStringConcat(lhs, rhs) { + var result; + + var lstr = ''; + var rstr = ''; + if (typeof lhs !== 'undefined') { + lstr = fn.string(lhs); + } + if (typeof rhs !== 'undefined') { + rstr = fn.string(rhs); + } + + result = lstr.concat(rstr); + return result; + } + + /** + * Evaluate group expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {{}} Evaluated input data + */ + function* evaluateGroupExpression(expr, input, environment) { + var result = {}; + var groups = {}; + var reduce = input && input.tupleStream ? true : false; + // group the input sequence by 'key' expression + if (!Array.isArray(input)) { + input = createSequence(input); + } + + for(var itemIndex = 0; itemIndex < input.length; itemIndex++) { + var item = input[itemIndex]; + var env = reduce ? createFrameFromTuple(environment, item) : environment; + for(var pairIndex = 0; pairIndex < expr.lhs.length; pairIndex++) { + var pair = expr.lhs[pairIndex]; + var key = yield * evaluate(pair[0], reduce ? item['@'] : item, env); + // key has to be a string + if (typeof key !== 'string') { + throw { + code: "T1003", + stack: (new Error()).stack, + position: expr.position, + value: key + }; + } + var entry = {data: item, exprIndex: pairIndex}; + if (groups.hasOwnProperty(key)) { + // a value already exists in this slot + if(groups[key].exprIndex !== pairIndex) { + // this key has been generated by another expression in this group + // when multiple key expressions evaluate to the same key, then error D1009 must be thrown + throw { + code: "D1009", + stack: (new Error()).stack, + position: expr.position, + value: key + }; + } + + // append it as an array + groups[key].data = fn.append(groups[key].data, item); + } else { + groups[key] = entry; + } + } + } + + // iterate over the groups to evaluate the 'value' expression + for (key in groups) { + entry = groups[key]; + var context = entry.data; + var env = environment; + if (reduce) { + var tuple = reduceTupleStream(entry.data); + context = tuple['@']; + delete tuple['@']; + env = createFrameFromTuple(environment, tuple); + } + var value = yield * evaluate(expr.lhs[entry.exprIndex][1], context, env); + if(typeof value !== 'undefined') { + result[key] = value; + } + } + + return result; + } + + function reduceTupleStream(tupleStream) { + if(!Array.isArray(tupleStream)) { + return tupleStream; + } + var result = {}; + Object.assign(result, tupleStream[0]); + for(var ii = 1; ii < tupleStream.length; ii++) { + for(const prop in tupleStream[ii]) { + result[prop] = fn.append(result[prop], tupleStream[ii][prop]); + } + } + return result; + } + + /** + * Evaluate range expression against input data + * @param {Object} lhs - LHS value + * @param {Object} rhs - RHS value + * @returns {Array} Resultant array + */ + function evaluateRangeExpression(lhs, rhs) { + var result; + + if (typeof lhs !== 'undefined' && !Number.isInteger(lhs)) { + throw { + code: "T2003", + stack: (new Error()).stack, + value: lhs + }; + } + if (typeof rhs !== 'undefined' && !Number.isInteger(rhs)) { + throw { + code: "T2004", + stack: (new Error()).stack, + value: rhs + }; + } + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + // if either side is undefined, the result is undefined + return result; + } + + if (lhs > rhs) { + // if the lhs is greater than the rhs, return undefined + return result; + } + + // limit the size of the array to ten million entries (1e7) + // this is an implementation defined limit to protect against + // memory and performance issues. This value may increase in the future. + var size = rhs - lhs + 1; + if(size > 1e7) { + throw { + code: "D2014", + stack: (new Error()).stack, + value: size + }; + } + + result = new Array(size); + for (var item = lhs, index = 0; item <= rhs; item++, index++) { + result[index] = item; + } + result.sequence = true; + return result; + } + + /** + * Evaluate bind expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluateBindExpression(expr, input, environment) { + // The RHS is the expression to evaluate + // The LHS is the name of the variable to bind to - should be a VARIABLE token (enforced by parser) + var value = yield * evaluate(expr.rhs, input, environment); + environment.bind(expr.lhs.value, value); + return value; + } + + /** + * Evaluate condition against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluateCondition(expr, input, environment) { + var result; + var condition = yield * evaluate(expr.condition, input, environment); + if (fn.boolean(condition)) { + result = yield * evaluate(expr.then, input, environment); + } else if (typeof expr.else !== 'undefined') { + result = yield * evaluate(expr.else, input, environment); + } + return result; + } + + /** + * Evaluate block against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluateBlock(expr, input, environment) { + var result; + // create a new frame to limit the scope of variable assignments + // TODO, only do this if the post-parse stage has flagged this as required + var frame = createFrame(environment); + // invoke each expression in turn + // only return the result of the last one + for(var ii = 0; ii < expr.expressions.length; ii++) { + result = yield * evaluate(expr.expressions[ii], input, frame); + } + + return result; + } + + /** + * Prepare a regex + * @param {Object} expr - expression containing regex + * @returns {Function} Higher order function representing prepared regex + */ + function evaluateRegex(expr) { + var re = new RegExp(expr.value); + var closure = function(str, fromIndex) { + var result; + re.lastIndex = fromIndex || 0; + var match = re.exec(str); + if(match !== null) { + result = { + match: match[0], + start: match.index, + end: match.index + match[0].length, + groups: [] + }; + if(match.length > 1) { + for(var i = 1; i < match.length; i++) { + result.groups.push(match[i]); + } + } + result.next = function() { + if(re.lastIndex >= str.length) { + return undefined; + } else { + var next = closure(str, re.lastIndex); + if(next && next.match === '') { + // matches zero length string; this will never progress + throw { + code: "D1004", + stack: (new Error()).stack, + position: expr.position, + value: expr.value.source + }; + } + return next; + } + }; + } + + return result; + }; + return closure; + } + + /** + * Evaluate variable against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateVariable(expr, input, environment) { + // lookup the variable value in the environment + var result; + // if the variable name is empty string, then it refers to context value + if (expr.value === '') { + result = input && input.outerWrapper ? input[0] : input; + } else { + result = environment.lookup(expr.value); + } + return result; + } + + /** + * sort / order-by operator + * @param {Object} expr - AST for operator + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Ordered sequence + */ + function* evaluateSortExpression(expr, input, environment) { + var result; + + // evaluate the lhs, then sort the results in order according to rhs expression + //var lhs = yield * evaluate(expr.lhs, input, environment); + var lhs = input; + var isTupleSort = input.tupleStream ? true : false; + + // sort the lhs array + // use comparator function + var comparator = function*(a, b) { // eslint-disable-line require-yield + // expr.terms is an array of order-by in priority order + var comp = 0; + for(var index = 0; comp === 0 && index < expr.terms.length; index++) { + var term = expr.terms[index]; + //evaluate the sort term in the context of a + var context = a; + var env = environment; + if(isTupleSort) { + context = a['@']; + env = createFrameFromTuple(environment, a); + } + var aa = yield * evaluate(term.expression, context, env); + //evaluate the sort term in the context of b + context = b; + env = environment; + if(isTupleSort) { + context = b['@']; + env = createFrameFromTuple(environment, b); + } + var bb = yield * evaluate(term.expression, context, env); + + // type checks + var atype = typeof aa; + var btype = typeof bb; + // undefined should be last in sort order + if(atype === 'undefined') { + // swap them, unless btype is also undefined + comp = (btype === 'undefined') ? 0 : 1; + continue; + } + if(btype === 'undefined') { + comp = -1; + continue; + } + + // if aa or bb are not string or numeric values, then throw an error + if(!(atype === 'string' || atype === 'number') || !(btype === 'string' || btype === 'number')) { + throw { + code: "T2008", + stack: (new Error()).stack, + position: expr.position, + value: !(atype === 'string' || atype === 'number') ? aa : bb + }; + } + + //if aa and bb are not of the same type + if(atype !== btype) { + throw { + code: "T2007", + stack: (new Error()).stack, + position: expr.position, + value: aa, + value2: bb + }; + } + if(aa === bb) { + // both the same - move on to next term + continue; + } else if (aa < bb) { + comp = -1; + } else { + comp = 1; + } + if(term.descending === true) { + comp = -comp; + } + } + // only swap a & b if comp equals 1 + return comp === 1; + }; + + var focus = { + environment: environment, + input: input + }; + // the `focus` is passed in as the `this` for the invoked function + result = yield * fn.sort.apply(focus, [lhs, comparator]); + + return result; + } + + /** + * create a transformer function + * @param {Object} expr - AST for operator + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} tranformer function + */ + function evaluateTransformExpression(expr, input, environment) { + // create a function to implement the transform definition + var transformer = function*(obj) { // signature <(oa):o> + // undefined inputs always return undefined + if(typeof obj === 'undefined') { + return undefined; + } + + // this function returns a copy of obj with changes specified by the pattern/operation + var cloneFunction = environment.lookup('clone'); + if(!isFunction(cloneFunction)) { + // throw type error + throw { + code: "T2013", + stack: (new Error()).stack, + position: expr.position + }; + } + var result = yield * apply(cloneFunction, [obj], null, environment); + var matches = yield * evaluate(expr.pattern, result, environment); + if(typeof matches !== 'undefined') { + if(!Array.isArray(matches)) { + matches = [matches]; + } + for(var ii = 0; ii < matches.length; ii++) { + var match = matches[ii]; + // evaluate the update value for each match + var update = yield * evaluate(expr.update, match, environment); + // update must be an object + var updateType = typeof update; + if(updateType !== 'undefined') { + if(updateType !== 'object' || update === null || Array.isArray(update)) { + // throw type error + throw { + code: "T2011", + stack: (new Error()).stack, + position: expr.update.position, + value: update + }; + } + // merge the update + for(var prop in update) { + match[prop] = update[prop]; + } + } + + // delete, if specified, must be an array of strings (or single string) + if(typeof expr.delete !== 'undefined') { + var deletions = yield * evaluate(expr.delete, match, environment); + if(typeof deletions !== 'undefined') { + var val = deletions; + if (!Array.isArray(deletions)) { + deletions = [deletions]; + } + if (!isArrayOfStrings(deletions)) { + // throw type error + throw { + code: "T2012", + stack: (new Error()).stack, + position: expr.delete.position, + value: val + }; + } + for (var jj = 0; jj < deletions.length; jj++) { + if(typeof match === 'object' && match !== null) { + delete match[deletions[jj]]; + } + } + } + } + } + } + + return result; + }; + + return defineFunction(transformer, '<(oa):o>'); + } + + var chainAST = parser('function($f, $g) { function($x){ $g($f($x)) } }'); + + /** + * Apply the function on the RHS using the sequence on the LHS as the first argument + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluateApplyExpression(expr, input, environment) { + var result; + + + var lhs = yield * evaluate(expr.lhs, input, environment); + if(expr.rhs.type === 'function') { + // this is a function _invocation_; invoke it with lhs expression as the first argument + result = yield * evaluateFunction(expr.rhs, input, environment, { context: lhs }); + } else { + var func = yield * evaluate(expr.rhs, input, environment); + + if(!isFunction(func)) { + throw { + code: "T2006", + stack: (new Error()).stack, + position: expr.position, + value: func + }; + } + + if(isFunction(lhs)) { + // this is function chaining (func1 ~> func2) + // λ($f, $g) { λ($x){ $g($f($x)) } } + var chain = yield * evaluate(chainAST, null, environment); + result = yield * apply(chain, [lhs, func], null, environment); + } else { + result = yield * apply(func, [lhs], null, environment); + } + + } + + return result; + } + + /** + * Evaluate function against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluateFunction(expr, input, environment, applyto) { + var result; + + // create the procedure + // can't assume that expr.procedure is a lambda type directly + // could be an expression that evaluates to a function (e.g. variable reference, parens expr etc. + // evaluate it generically first, then check that it is a function. Throw error if not. + var proc = yield * evaluate(expr.procedure, input, environment); + + if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure.steps[0].value)) { + // help the user out here if they simply forgot the leading $ + throw { + code: "T1005", + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.steps[0].value + }; + } + + var evaluatedArgs = []; + if(typeof applyto !== 'undefined') { + evaluatedArgs.push(applyto.context); + } + // eager evaluation - evaluate the arguments + for (var jj = 0; jj < expr.arguments.length; jj++) { + const arg = yield* evaluate(expr.arguments[jj], input, environment); + if(isFunction(arg)) { + // wrap this in a closure + const closure = function* (...params) { + // invoke func + return yield * apply(arg, params, null, environment); + }; + closure.arity = getFunctionArity(arg); + evaluatedArgs.push(closure); + } else { + evaluatedArgs.push(arg); + } + } + // apply the procedure + var procName = expr.procedure.type === 'path' ? expr.procedure.steps[0].value : expr.procedure.value; + try { + if(typeof proc === 'object') { + proc.token = procName; + proc.position = expr.position; + } + result = yield * apply(proc, evaluatedArgs, input, environment); + } catch (err) { + if(!err.position) { + // add the position field to the error + err.position = expr.position; + } + if (!err.token) { + // and the function identifier + err.token = procName; + } + throw err; + } + return result; + } + + /** + * Apply procedure or function + * @param {Object} proc - Procedure + * @param {Array} args - Arguments + * @param {Object} input - input + * @param {Object} environment - environment + * @returns {*} Result of procedure + */ + function* apply(proc, args, input, environment) { + var result; + result = yield * applyInner(proc, args, input, environment); + while(isLambda(result) && result.thunk === true) { + // trampoline loop - this gets invoked as a result of tail-call optimization + // the function returned a tail-call thunk + // unpack it, evaluate its arguments, and apply the tail call + var next = yield * evaluate(result.body.procedure, result.input, result.environment); + if(result.body.procedure.type === 'variable') { + next.token = result.body.procedure.value; + } + next.position = result.body.procedure.position; + var evaluatedArgs = []; + for(var ii = 0; ii < result.body.arguments.length; ii++) { + evaluatedArgs.push(yield * evaluate(result.body.arguments[ii], result.input, result.environment)); + } + + result = yield * applyInner(next, evaluatedArgs, input, environment); + } + return result; + } + + /** + * Apply procedure or function + * @param {Object} proc - Procedure + * @param {Array} args - Arguments + * @param {Object} input - input + * @param {Object} environment - environment + * @returns {*} Result of procedure + */ + function* applyInner(proc, args, input, environment) { + var result; + try { + var validatedArgs = args; + if (proc) { + validatedArgs = validateArguments(proc.signature, args, input); + } + + if (isLambda(proc)) { + result = yield* applyProcedure(proc, validatedArgs); + } else if (proc && proc._jsonata_function === true) { + var focus = { + environment: environment, + input: input + }; + // the `focus` is passed in as the `this` for the invoked function + result = proc.implementation.apply(focus, validatedArgs); + // `proc.implementation` might be a generator function + // and `result` might be a generator - if so, yield + if (isIterable(result)) { + result = yield* result; + } + } else if (typeof proc === 'function') { + // typically these are functions that are returned by the invocation of plugin functions + // the `input` is being passed in as the `this` for the invoked function + // this is so that functions that return objects containing functions can chain + // e.g. $func().next().next() + result = proc.apply(input, validatedArgs); + /* istanbul ignore next */ + if (isIterable(result)) { + result = yield* result; + } + } else { + throw { + code: "T1006", + stack: (new Error()).stack + }; + } + } catch(err) { + if(proc) { + if (typeof err.token == 'undefined' && typeof proc.token !== 'undefined') { + err.token = proc.token; + } + err.position = proc.position; + } + throw err; + } + return result; + } + + /** + * Evaluate lambda against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {{lambda: boolean, input: *, environment: *, arguments: *, body: *}} Evaluated input data + */ + function evaluateLambda(expr, input, environment) { + // make a function (closure) + var procedure = { + _jsonata_lambda: true, + input: input, + environment: environment, + arguments: expr.arguments, + signature: expr.signature, + body: expr.body + }; + if(expr.thunk === true) { + procedure.thunk = true; + } + procedure.apply = function*(self, args) { + return yield * apply(procedure, args, input, self.environment); + }; + return procedure; + } + + /** + * Evaluate partial application + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function* evaluatePartialApplication(expr, input, environment) { + // partially apply a function + var result; + // evaluate the arguments + var evaluatedArgs = []; + for(var ii = 0; ii < expr.arguments.length; ii++) { + var arg = expr.arguments[ii]; + if (arg.type === 'operator' && arg.value === '?') { + evaluatedArgs.push(arg); + } else { + evaluatedArgs.push(yield * evaluate(arg, input, environment)); + } + } + // lookup the procedure + var proc = yield * evaluate(expr.procedure, input, environment); + if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure.steps[0].value)) { + // help the user out here if they simply forgot the leading $ + throw { + code: "T1007", + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.steps[0].value + }; + } + if (isLambda(proc)) { + result = partialApplyProcedure(proc, evaluatedArgs); + } else if (proc && proc._jsonata_function === true) { + result = partialApplyNativeFunction(proc.implementation, evaluatedArgs); + } else if (typeof proc === 'function') { + result = partialApplyNativeFunction(proc, evaluatedArgs); + } else { + throw { + code: "T1008", + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.type === 'path' ? expr.procedure.steps[0].value : expr.procedure.value + }; + } + return result; + } + + /** + * Validate the arguments against the signature validator (if it exists) + * @param {Function} signature - validator function + * @param {Array} args - function arguments + * @param {*} context - context value + * @returns {Array} - validated arguments + */ + function validateArguments(signature, args, context) { + if(typeof signature === 'undefined') { + // nothing to validate + return args; + } + var validatedArgs = signature.validate(args, context); + return validatedArgs; + } + + /** + * Apply procedure + * @param {Object} proc - Procedure + * @param {Array} args - Arguments + * @returns {*} Result of procedure + */ + function* applyProcedure(proc, args) { + var result; + var env = createFrame(proc.environment); + proc.arguments.forEach(function (param, index) { + env.bind(param.value, args[index]); + }); + if (typeof proc.body === 'function') { + // this is a lambda that wraps a native function - generated by partially evaluating a native + result = yield * applyNativeFunction(proc.body, env); + } else { + result = yield * evaluate(proc.body, proc.input, env); + } + return result; + } + + /** + * Partially apply procedure + * @param {Object} proc - Procedure + * @param {Array} args - Arguments + * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applied procedure + */ + function partialApplyProcedure(proc, args) { + // create a closure, bind the supplied parameters and return a function that takes the remaining (?) parameters + var env = createFrame(proc.environment); + var unboundArgs = []; + proc.arguments.forEach(function (param, index) { + var arg = args[index]; + if (arg && arg.type === 'operator' && arg.value === '?') { + unboundArgs.push(param); + } else { + env.bind(param.value, arg); + } + }); + var procedure = { + _jsonata_lambda: true, + input: proc.input, + environment: env, + arguments: unboundArgs, + body: proc.body + }; + return procedure; + } + + /** + * Partially apply native function + * @param {Function} native - Native function + * @param {Array} args - Arguments + * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applying native function + */ + function partialApplyNativeFunction(native, args) { + // create a lambda function that wraps and invokes the native function + // get the list of declared arguments from the native function + // this has to be picked out from the toString() value + var sigArgs = getNativeFunctionArguments(native); + sigArgs = sigArgs.map(function (sigArg) { + return '$' + sigArg.trim(); + }); + var body = 'function(' + sigArgs.join(', ') + '){ _ }'; + + var bodyAST = parser(body); + bodyAST.body = native; + + var partial = partialApplyProcedure(bodyAST, args); + return partial; + } + + /** + * Apply native function + * @param {Object} proc - Procedure + * @param {Object} env - Environment + * @returns {*} Result of applying native function + */ + function* applyNativeFunction(proc, env) { + var sigArgs = getNativeFunctionArguments(proc); + // generate the array of arguments for invoking the function - look them up in the environment + var args = sigArgs.map(function (sigArg) { + return env.lookup(sigArg.trim()); + }); + + var focus = { + environment: env + }; + var result = proc.apply(focus, args); + if(isIterable(result)) { + result = yield * result; + } + return result; + } + + /** + * Get native function arguments + * @param {Function} func - Function + * @returns {*|Array} Native function arguments + */ + function getNativeFunctionArguments(func) { + var signature = func.toString(); + var sigParens = /\(([^)]*)\)/.exec(signature)[1]; // the contents of the parens + var sigArgs = sigParens.split(','); + return sigArgs; + } + + /** + * Creates a function definition + * @param {Function} func - function implementation in Javascript + * @param {string} signature - JSONata function signature definition + * @returns {{implementation: *, signature: *}} function definition + */ + function defineFunction(func, signature) { + var definition = { + _jsonata_function: true, + implementation: func + }; + if(typeof signature !== 'undefined') { + definition.signature = parseSignature(signature); + } + return definition; + } + + + /** + * parses and evaluates the supplied expression + * @param {string} expr - expression to evaluate + * @returns {*} - result of evaluating the expression + */ + function* functionEval(expr, focus) { + // undefined inputs always return undefined + if(typeof expr === 'undefined') { + return undefined; + } + var input = this.input; + if(typeof focus !== 'undefined') { + input = focus; + } + + try { + var ast = parser(expr, false); + } catch(err) { + // error parsing the expression passed to $eval + populateMessage(err); + throw { + stack: (new Error()).stack, + code: "D3120", + value: err.message, + error: err + }; + } + try { + var result = yield* evaluate(ast, input, this.environment); + } catch(err) { + // error evaluating the expression passed to $eval + populateMessage(err); + throw { + stack: (new Error()).stack, + code: "D3121", + value:err.message, + error: err + }; + } + + return result; + } + + /** + * Clones an object + * @param {Object} arg - object to clone (deep copy) + * @returns {*} - the cloned object + */ + function functionClone(arg) { + // undefined inputs always return undefined + if(typeof arg === 'undefined') { + return undefined; + } + + return JSON.parse(fn.string(arg)); + } + + /** + * Create frame + * @param {Object} enclosingEnvironment - Enclosing environment + * @returns {{bind: bind, lookup: lookup}} Created frame + */ + function createFrame(enclosingEnvironment) { + var bindings = {}; + return { + bind: function (name, value) { + bindings[name] = value; + }, + lookup: function (name) { + var value; + if(bindings.hasOwnProperty(name)) { + value = bindings[name]; + } else if (enclosingEnvironment) { + value = enclosingEnvironment.lookup(name); + } + return value; + }, + timestamp: enclosingEnvironment ? enclosingEnvironment.timestamp : null, + async: enclosingEnvironment ? enclosingEnvironment.async : false, + global: enclosingEnvironment ? enclosingEnvironment.global : { + ancestry: [ null ] + } + }; + } + + // Function registration + staticFrame.bind('sum', defineFunction(fn.sum, ':n>')); + staticFrame.bind('count', defineFunction(fn.count, '')); + staticFrame.bind('max', defineFunction(fn.max, ':n>')); + staticFrame.bind('min', defineFunction(fn.min, ':n>')); + staticFrame.bind('average', defineFunction(fn.average, ':n>')); + staticFrame.bind('string', defineFunction(fn.string, '')); + staticFrame.bind('substring', defineFunction(fn.substring, '')); + staticFrame.bind('substringBefore', defineFunction(fn.substringBefore, '')); + staticFrame.bind('substringAfter', defineFunction(fn.substringAfter, '')); + staticFrame.bind('lowercase', defineFunction(fn.lowercase, '')); + staticFrame.bind('uppercase', defineFunction(fn.uppercase, '')); + staticFrame.bind('length', defineFunction(fn.length, '')); + staticFrame.bind('trim', defineFunction(fn.trim, '')); + staticFrame.bind('pad', defineFunction(fn.pad, '')); + staticFrame.bind('match', defineFunction(fn.match, 'n?:a>')); + staticFrame.bind('contains', defineFunction(fn.contains, '')); // TODO ):b> + staticFrame.bind('replace', defineFunction(fn.replace, '')); // TODO )(sf)n?:s> + staticFrame.bind('split', defineFunction(fn.split, '>')); // TODO )n?:a> + staticFrame.bind('join', defineFunction(fn.join, 's?:s>')); + staticFrame.bind('formatNumber', defineFunction(fn.formatNumber, '')); + staticFrame.bind('formatBase', defineFunction(fn.formatBase, '')); + staticFrame.bind('formatInteger', defineFunction(datetime.formatInteger, '')); + staticFrame.bind('parseInteger', defineFunction(datetime.parseInteger, '')); + staticFrame.bind('number', defineFunction(fn.number, '<(nsb)-:n>')); + staticFrame.bind('floor', defineFunction(fn.floor, '')); + staticFrame.bind('ceil', defineFunction(fn.ceil, '')); + staticFrame.bind('round', defineFunction(fn.round, '')); + staticFrame.bind('abs', defineFunction(fn.abs, '')); + staticFrame.bind('sqrt', defineFunction(fn.sqrt, '')); + staticFrame.bind('power', defineFunction(fn.power, '')); + staticFrame.bind('random', defineFunction(fn.random, '<:n>')); + staticFrame.bind('boolean', defineFunction(fn.boolean, '')); + staticFrame.bind('not', defineFunction(fn.not, '')); + staticFrame.bind('map', defineFunction(fn.map, '')); + staticFrame.bind('zip', defineFunction(fn.zip, '')); + staticFrame.bind('filter', defineFunction(fn.filter, '')); + staticFrame.bind('single', defineFunction(fn.single, '')); + staticFrame.bind('reduce', defineFunction(fn.foldLeft, '')); // TODO aj?:j> + staticFrame.bind('sift', defineFunction(fn.sift, '')); + staticFrame.bind('keys', defineFunction(fn.keys, '>')); + staticFrame.bind('lookup', defineFunction(fn.lookup, '')); + staticFrame.bind('append', defineFunction(fn.append, '')); + staticFrame.bind('exists', defineFunction(fn.exists, '')); + staticFrame.bind('spread', defineFunction(fn.spread, '>')); + staticFrame.bind('merge', defineFunction(fn.merge, ':o>')); + staticFrame.bind('reverse', defineFunction(fn.reverse, '')); + staticFrame.bind('each', defineFunction(fn.each, '')); + staticFrame.bind('error', defineFunction(fn.error, '')); + staticFrame.bind('assert', defineFunction(fn.assert, '')); + staticFrame.bind('type', defineFunction(fn.type, '')); + staticFrame.bind('sort', defineFunction(fn.sort, '')); + staticFrame.bind('shuffle', defineFunction(fn.shuffle, '')); + staticFrame.bind('distinct', defineFunction(fn.distinct, '')); + staticFrame.bind('base64encode', defineFunction(fn.base64encode, '')); + staticFrame.bind('base64decode', defineFunction(fn.base64decode, '')); + staticFrame.bind('encodeUrlComponent', defineFunction(fn.encodeUrlComponent, '')); + staticFrame.bind('encodeUrl', defineFunction(fn.encodeUrl, '')); + staticFrame.bind('decodeUrlComponent', defineFunction(fn.decodeUrlComponent, '')); + staticFrame.bind('decodeUrl', defineFunction(fn.decodeUrl, '')); + staticFrame.bind('eval', defineFunction(functionEval, '')); + staticFrame.bind('toMillis', defineFunction(datetime.toMillis, '')); + staticFrame.bind('fromMillis', defineFunction(datetime.fromMillis, '')); + staticFrame.bind('clone', defineFunction(functionClone, '<(oa)-:o>')); + + /** + * Error codes + * + * Sxxxx - Static errors (compile time) + * Txxxx - Type errors + * Dxxxx - Dynamic errors (evaluate time) + * 01xx - tokenizer + * 02xx - parser + * 03xx - regex parser + * 04xx - function signature parser/evaluator + * 10xx - evaluator + * 20xx - operators + * 3xxx - functions (blocks of 10 for each function) + */ + var errorCodes = { + "S0101": "String literal must be terminated by a matching quote", + "S0102": "Number out of range: {{token}}", + "S0103": "Unsupported escape sequence: \\{{token}}", + "S0104": "The escape sequence \\u must be followed by 4 hex digits", + "S0105": "Quoted property name must be terminated with a backquote (`)", + "S0106": "Comment has no closing tag", + "S0201": "Syntax error: {{token}}", + "S0202": "Expected {{value}}, got {{token}}", + "S0203": "Expected {{value}} before end of expression", + "S0204": "Unknown operator: {{token}}", + "S0205": "Unexpected token: {{token}}", + "S0206": "Unknown expression type: {{token}}", + "S0207": "Unexpected end of expression", + "S0208": "Parameter {{value}} of function definition must be a variable name (start with $)", + "S0209": "A predicate cannot follow a grouping expression in a step", + "S0210": "Each step can only have one grouping expression", + "S0211": "The symbol {{token}} cannot be used as a unary operator", + "S0212": "The left side of := must be a variable name (start with $)", + "S0213": "The literal value {{value}} cannot be used as a step within a path expression", + "S0214": "The right side of {{token}} must be a variable name (start with $)", + "S0215": "A context variable binding must precede any predicates on a step", + "S0216": "A context variable binding must precede the 'order-by' clause on a step", + "S0217": "The object representing the 'parent' cannot be derived from this expression", + "S0301": "Empty regular expressions are not allowed", + "S0302": "No terminating / in regular expression", + "S0402": "Choice groups containing parameterized types are not supported", + "S0401": "Type parameters can only be applied to functions and arrays", + "S0500": "Attempted to evaluate an expression containing syntax error(s)", + "T0410": "Argument {{index}} of function {{token}} does not match function signature", + "T0411": "Context value is not a compatible type with argument {{index}} of function {{token}}", + "T0412": "Argument {{index}} of function {{token}} must be an array of {{type}}", + "D1001": "Number out of range: {{value}}", + "D1002": "Cannot negate a non-numeric value: {{value}}", + "T1003": "Key in object structure must evaluate to a string; got: {{value}}", + "D1004": "Regular expression matches zero length string", + "T1005": "Attempted to invoke a non-function. Did you mean ${{{token}}}?", + "T1006": "Attempted to invoke a non-function", + "T1007": "Attempted to partially apply a non-function. Did you mean ${{{token}}}?", + "T1008": "Attempted to partially apply a non-function", + "D1009": "Multiple key definitions evaluate to same key: {{value}}", + "T1010": "The matcher function argument passed to function {{token}} does not return the correct object structure", + "T2001": "The left side of the {{token}} operator must evaluate to a number", + "T2002": "The right side of the {{token}} operator must evaluate to a number", + "T2003": "The left side of the range operator (..) must evaluate to an integer", + "T2004": "The right side of the range operator (..) must evaluate to an integer", + "D2005": "The left side of := must be a variable name (start with $)", // defunct - replaced by S0212 parser error + "T2006": "The right side of the function application operator ~> must be a function", + "T2007": "Type mismatch when comparing values {{value}} and {{value2}} in order-by clause", + "T2008": "The expressions within an order-by clause must evaluate to numeric or string values", + "T2009": "The values {{value}} and {{value2}} either side of operator {{token}} must be of the same data type", + "T2010": "The expressions either side of operator {{token}} must evaluate to numeric or string values", + "T2011": "The insert/update clause of the transform expression must evaluate to an object: {{value}}", + "T2012": "The delete clause of the transform expression must evaluate to a string or array of strings: {{value}}", + "T2013": "The transform expression clones the input object using the $clone() function. This has been overridden in the current scope by a non-function.", + "D2014": "The size of the sequence allocated by the range operator (..) must not exceed 1e6. Attempted to allocate {{value}}.", + "D3001": "Attempting to invoke string function on Infinity or NaN", + "D3010": "Second argument of replace function cannot be an empty string", + "D3011": "Fourth argument of replace function must evaluate to a positive number", + "D3012": "Attempted to replace a matched string with a non-string value", + "D3020": "Third argument of split function must evaluate to a positive number", + "D3030": "Unable to cast value to a number: {{value}}", + "D3040": "Third argument of match function must evaluate to a positive number", + "D3050": "The second argument of reduce function must be a function with at least two arguments", + "D3060": "The sqrt function cannot be applied to a negative number: {{value}}", + "D3061": "The power function has resulted in a value that cannot be represented as a JSON number: base={{value}}, exponent={{exp}}", + "D3070": "The single argument form of the sort function can only be applied to an array of strings or an array of numbers. Use the second argument to specify a comparison function", + "D3080": "The picture string must only contain a maximum of two sub-pictures", + "D3081": "The sub-picture must not contain more than one instance of the 'decimal-separator' character", + "D3082": "The sub-picture must not contain more than one instance of the 'percent' character", + "D3083": "The sub-picture must not contain more than one instance of the 'per-mille' character", + "D3084": "The sub-picture must not contain both a 'percent' and a 'per-mille' character", + "D3085": "The mantissa part of a sub-picture must contain at least one character that is either an 'optional digit character' or a member of the 'decimal digit family'", + "D3086": "The sub-picture must not contain a passive character that is preceded by an active character and that is followed by another active character", + "D3087": "The sub-picture must not contain a 'grouping-separator' character that appears adjacent to a 'decimal-separator' character", + "D3088": "The sub-picture must not contain a 'grouping-separator' at the end of the integer part", + "D3089": "The sub-picture must not contain two adjacent instances of the 'grouping-separator' character", + "D3090": "The integer part of the sub-picture must not contain a member of the 'decimal digit family' that is followed by an instance of the 'optional digit character'", + "D3091": "The fractional part of the sub-picture must not contain an instance of the 'optional digit character' that is followed by a member of the 'decimal digit family'", + "D3092": "A sub-picture that contains a 'percent' or 'per-mille' character must not contain a character treated as an 'exponent-separator'", + "D3093": "The exponent part of the sub-picture must comprise only of one or more characters that are members of the 'decimal digit family'", + "D3100": "The radix of the formatBase function must be between 2 and 36. It was given {{value}}", + "D3110": "The argument of the toMillis function must be an ISO 8601 formatted timestamp. Given {{value}}", + "D3120": "Syntax error in expression passed to function eval: {{value}}", + "D3121": "Dynamic error evaluating the expression passed to function eval: {{value}}", + "D3130": "Formatting or parsing an integer as a sequence starting with {{value}} is not supported by this implementation", + "D3131": "In a decimal digit pattern, all digits must be from the same decimal group", + "D3132": "Unknown component specifier {{value}} in date/time picture string", + "D3133": "The 'name' modifier can only be applied to months and days in the date/time picture string, not {{value}}", + "D3134": "The timezone integer format specifier cannot have more than four digits", + "D3135": "No matching closing bracket ']' in date/time picture string", + "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.", + "D3140": "Malformed URL passed to ${{{functionName}}}(): {{value}}", + "D3141": "{{{message}}}" + }; + + /** + * lookup a message template from the catalog and substitute the inserts. + * Populates `err.message` with the substituted message. Leaves `err.message` + * untouched if code lookup fails. + * @param {string} err - error code to lookup + * @returns {undefined} - `err` is modified in place + */ + function populateMessage(err) { + var template = errorCodes[err.code]; + if(typeof template !== 'undefined') { + // if there are any handlebars, replace them with the field references + // triple braces - replace with value + // double braces - replace with json stringified value + var message = template.replace(/\{\{\{([^}]+)}}}/g, function() { + return err[arguments[1]]; + }); + message = message.replace(/\{\{([^}]+)}}/g, function() { + return JSON.stringify(err[arguments[1]]); + }); + err.message = message; + } + // Otherwise retain the original `err.message` + } + + /** + * JSONata + * @param {Object} expr - JSONata expression + * @param {boolean} options - recover: attempt to recover on parse error + * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression + */ + function jsonata(expr, options) { + var ast; + var errors; + try { + ast = parser(expr, options && options.recover); + errors = ast.errors; + delete ast.errors; + } catch(err) { + // insert error message into structure + populateMessage(err); // possible side-effects on `err` + throw err; + } + var environment = createFrame(staticFrame); + + var timestamp = new Date(); // will be overridden on each call to evalute() + environment.bind('now', defineFunction(function(picture, timezone) { + return datetime.fromMillis(timestamp.getTime(), picture, timezone); + }, '')); + environment.bind('millis', defineFunction(function() { + return timestamp.getTime(); + }, '<:n>')); + + return { + evaluate: function (input, bindings, callback) { + // throw if the expression compiled with syntax errors + if(typeof errors !== 'undefined') { + var err = { + code: 'S0500', + position: 0 + }; + populateMessage(err); // possible side-effects on `err` + throw err; + } + + if (typeof bindings !== 'undefined') { + var exec_env; + // the variable bindings have been passed in - create a frame to hold these + exec_env = createFrame(environment); + for (var v in bindings) { + exec_env.bind(v, bindings[v]); + } + } else { + exec_env = environment; + } + // put the input document into the environment as the root object + exec_env.bind('$', input); + + // capture the timestamp and put it in the execution environment + // the $now() and $millis() functions will return this value - whenever it is called + timestamp = new Date(); + exec_env.timestamp = timestamp; + + // if the input is a JSON array, then wrap it in a singleton sequence so it gets treated as a single input + if(Array.isArray(input) && !isSequence(input)) { + input = createSequence(input); + input.outerWrapper = true; + } + + var result, it; + // if a callback function is supplied, then drive the generator in a promise chain + if(typeof callback === 'function') { + exec_env.async = true; + var catchHandler = function (err) { + populateMessage(err); // possible side-effects on `err` + callback(err, null); + }; + var thenHandler = function (response) { + result = it.next(response); + if (result.done) { + callback(null, result.value); + } else { + result.value.then(thenHandler).catch(catchHandler); + } + }; + it = evaluate(ast, input, exec_env); + result = it.next(); + result.value.then(thenHandler).catch(catchHandler); + } else { + // no callback function - drive the generator to completion synchronously + try { + it = evaluate(ast, input, exec_env); + result = it.next(); + while (!result.done) { + result = it.next(result.value); + } + return result.value; + } catch (err) { + // insert error message into structure + populateMessage(err); // possible side-effects on `err` + throw err; + } + } + }, + assign: function (name, value) { + environment.bind(name, value); + }, + registerFunction: function(name, implementation, signature) { + var func = defineFunction(implementation, signature); + environment.bind(name, func); + }, + ast: function() { + return ast; + }, + errors: function() { + return errors; + } + }; + } + + jsonata.parser = parser; // TODO remove this in a future release - use ast() instead + + return jsonata; + +})(); + +module.exports = jsonata; + +},{"./datetime":1,"./functions":2,"./parser":4,"./signature":5,"./utils":6}],4:[function(require,module,exports){ +/** + * © Copyright IBM Corp. 2016, 2018 All Rights Reserved + * Project name: JSONata + * This project is licensed under the MIT License, see LICENSE + */ + +var parseSignature = require('./signature'); + +const parser = (() => { + 'use strict'; + + var operators = { + '.': 75, + '[': 80, + ']': 0, + '{': 70, + '}': 0, + '(': 80, + ')': 0, + ',': 0, + '@': 80, + '#': 80, + ';': 80, + ':': 80, + '?': 20, + '+': 50, + '-': 50, + '*': 60, + '/': 60, + '%': 60, + '|': 20, + '=': 40, + '<': 40, + '>': 40, + '^': 40, + '**': 60, + '..': 20, + ':=': 10, + '!=': 40, + '<=': 40, + '>=': 40, + '~>': 40, + 'and': 30, + 'or': 25, + 'in': 40, + '&': 50, + '!': 0, // not an operator, but needed as a stop character for name tokens + '~': 0 // not an operator, but needed as a stop character for name tokens + }; + + var escapes = { // JSON string escape sequences - see json.org + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }; + + // Tokenizer (lexer) - invoked by the parser to return one token at a time + var tokenizer = function (path) { + var position = 0; + var length = path.length; + + var create = function (type, value) { + var obj = {type: type, value: value, position: position}; + return obj; + }; + + var scanRegex = function () { + // the prefix '/' will have been previously scanned. Find the end of the regex. + // search for closing '/' ignoring any that are escaped, or within brackets + var start = position; + var depth = 0; + var pattern; + var flags; + while (position < length) { + var currentChar = path.charAt(position); + if (currentChar === '/' && path.charAt(position - 1) !== '\\' && depth === 0) { + // end of regex found + pattern = path.substring(start, position); + if (pattern === '') { + throw { + code: "S0301", + stack: (new Error()).stack, + position: position + }; + } + position++; + currentChar = path.charAt(position); + // flags + start = position; + while (currentChar === 'i' || currentChar === 'm') { + position++; + currentChar = path.charAt(position); + } + flags = path.substring(start, position) + 'g'; + return new RegExp(pattern, flags); + } + if ((currentChar === '(' || currentChar === '[' || currentChar === '{') && path.charAt(position - 1) !== '\\') { + depth++; + } + if ((currentChar === ')' || currentChar === ']' || currentChar === '}') && path.charAt(position - 1) !== '\\') { + depth--; + } + + position++; + } + throw { + code: "S0302", + stack: (new Error()).stack, + position: position + }; + }; + + var next = function (prefix) { + if (position >= length) return null; + var currentChar = path.charAt(position); + // skip whitespace + while (position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { + position++; + currentChar = path.charAt(position); + } + // skip comments + if (currentChar === '/' && path.charAt(position + 1) === '*') { + var commentStart = position; + position += 2; + currentChar = path.charAt(position); + while (!(currentChar === '*' && path.charAt(position + 1) === '/')) { + currentChar = path.charAt(++position); + if (position >= length) { + // no closing tag + throw { + code: "S0106", + stack: (new Error()).stack, + position: commentStart + }; + } + } + position += 2; + currentChar = path.charAt(position); + return next(prefix); // need this to swallow any following whitespace + } + // test for regex + if (prefix !== true && currentChar === '/') { + position++; + return create('regex', scanRegex()); + } + // handle double-char operators + if (currentChar === '.' && path.charAt(position + 1) === '.') { + // double-dot .. range operator + position += 2; + return create('operator', '..'); + } + if (currentChar === ':' && path.charAt(position + 1) === '=') { + // := assignment + position += 2; + return create('operator', ':='); + } + if (currentChar === '!' && path.charAt(position + 1) === '=') { + // != + position += 2; + return create('operator', '!='); + } + if (currentChar === '>' && path.charAt(position + 1) === '=') { + // >= + position += 2; + return create('operator', '>='); + } + if (currentChar === '<' && path.charAt(position + 1) === '=') { + // <= + position += 2; + return create('operator', '<='); + } + if (currentChar === '*' && path.charAt(position + 1) === '*') { + // ** descendant wildcard + position += 2; + return create('operator', '**'); + } + if (currentChar === '~' && path.charAt(position + 1) === '>') { + // ~> chain function + position += 2; + return create('operator', '~>'); + } + // test for single char operators + if (Object.prototype.hasOwnProperty.call(operators, currentChar)) { + position++; + return create('operator', currentChar); + } + // test for string literals + if (currentChar === '"' || currentChar === "'") { + var quoteType = currentChar; + // double quoted string literal - find end of string + position++; + var qstr = ""; + while (position < length) { + currentChar = path.charAt(position); + if (currentChar === '\\') { // escape sequence + position++; + currentChar = path.charAt(position); + if (Object.prototype.hasOwnProperty.call(escapes, currentChar)) { + qstr += escapes[currentChar]; + } else if (currentChar === 'u') { + // \u should be followed by 4 hex digits + var octets = path.substr(position + 1, 4); + if (/^[0-9a-fA-F]+$/.test(octets)) { + var codepoint = parseInt(octets, 16); + qstr += String.fromCharCode(codepoint); + position += 4; + } else { + throw { + code: "S0104", + stack: (new Error()).stack, + position: position + }; + } + } else { + // illegal escape sequence + throw { + code: "S0103", + stack: (new Error()).stack, + position: position, + token: currentChar + }; + + } + } else if (currentChar === quoteType) { + position++; + return create('string', qstr); + } else { + qstr += currentChar; + } + position++; + } + throw { + code: "S0101", + stack: (new Error()).stack, + position: position + }; + } + // test for numbers + var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/; + var match = numregex.exec(path.substring(position)); + if (match !== null) { + var num = parseFloat(match[0]); + if (!isNaN(num) && isFinite(num)) { + position += match[0].length; + return create('number', num); + } else { + throw { + code: "S0102", + stack: (new Error()).stack, + position: position, + token: match[0] + }; + } + } + // test for quoted names (backticks) + var name; + if (currentChar === '`') { + // scan for closing quote + position++; + var end = path.indexOf('`', position); + if (end !== -1) { + name = path.substring(position, end); + position = end + 1; + return create('name', name); + } + position = length; + throw { + code: "S0105", + stack: (new Error()).stack, + position: position + }; + } + // test for names + var i = position; + var ch; + for (; ;) { + ch = path.charAt(i); + if (i === length || ' \t\n\r\v'.indexOf(ch) > -1 || Object.prototype.hasOwnProperty.call(operators, ch)) { + if (path.charAt(position) === '$') { + // variable reference + name = path.substring(position + 1, i); + position = i; + return create('variable', name); + } else { + name = path.substring(position, i); + position = i; + switch (name) { + case 'or': + case 'in': + case 'and': + return create('operator', name); + case 'true': + return create('value', true); + case 'false': + return create('value', false); + case 'null': + return create('value', null); + default: + if (position === length && name === '') { + // whitespace at end of input + return null; + } + return create('name', name); + } + } + } else { + i++; + } + } + }; + + return next; + }; + + // This parser implements the 'Top down operator precedence' algorithm developed by Vaughan R Pratt; http://dl.acm.org/citation.cfm?id=512931. + // and builds on the Javascript framework described by Douglas Crockford at http://javascript.crockford.com/tdop/tdop.html + // and in 'Beautiful Code', edited by Andy Oram and Greg Wilson, Copyright 2007 O'Reilly Media, Inc. 798-0-596-51004-6 + + var parser = function (source, recover) { + var node; + var lexer; + + var symbol_table = {}; + var errors = []; + + var remainingTokens = function () { + var remaining = []; + if (node.id !== '(end)') { + remaining.push({type: node.type, value: node.value, position: node.position}); + } + var nxt = lexer(); + while (nxt !== null) { + remaining.push(nxt); + nxt = lexer(); + } + return remaining; + }; + + var base_symbol = { + nud: function () { + // error - symbol has been invoked as a unary operator + var err = { + code: 'S0211', + token: this.value, + position: this.position + }; + + if (recover) { + err.remaining = remainingTokens(); + err.type = 'error'; + errors.push(err); + return err; + } else { + err.stack = (new Error()).stack; + throw err; + } + } + }; + + var symbol = function (id, bp) { + var s = symbol_table[id]; + bp = bp || 0; + if (s) { + if (bp >= s.lbp) { + s.lbp = bp; + } + } else { + s = Object.create(base_symbol); + s.id = s.value = id; + s.lbp = bp; + symbol_table[id] = s; + } + return s; + }; + + var handleError = function (err) { + if (recover) { + // tokenize the rest of the buffer and add it to an error token + err.remaining = remainingTokens(); + errors.push(err); + var symbol = symbol_table["(error)"]; + node = Object.create(symbol); + node.error = err; + node.type = "(error)"; + return node; + } else { + err.stack = (new Error()).stack; + throw err; + } + }; + + var advance = function (id, infix) { + if (id && node.id !== id) { + var code; + if (node.id === '(end)') { + // unexpected end of buffer + code = "S0203"; + } else { + code = "S0202"; + } + var err = { + code: code, + position: node.position, + token: node.value, + value: id + }; + return handleError(err); + } + var next_token = lexer(infix); + if (next_token === null) { + node = symbol_table["(end)"]; + node.position = source.length; + return node; + } + var value = next_token.value; + var type = next_token.type; + var symbol; + switch (type) { + case 'name': + case 'variable': + symbol = symbol_table["(name)"]; + break; + case 'operator': + symbol = symbol_table[value]; + if (!symbol) { + return handleError({ + code: "S0204", + stack: (new Error()).stack, + position: next_token.position, + token: value + }); + } + break; + case 'string': + case 'number': + case 'value': + symbol = symbol_table["(literal)"]; + break; + case 'regex': + type = "regex"; + symbol = symbol_table["(regex)"]; + break; + /* istanbul ignore next */ + default: + return handleError({ + code: "S0205", + stack: (new Error()).stack, + position: next_token.position, + token: value + }); + } + + node = Object.create(symbol); + node.value = value; + node.type = type; + node.position = next_token.position; + return node; + }; + + // Pratt's algorithm + var expression = function (rbp) { + var left; + var t = node; + advance(null, true); + left = t.nud(); + while (rbp < node.lbp) { + t = node; + advance(); + left = t.led(left); + } + return left; + }; + + var terminal = function (id) { + var s = symbol(id, 0); + s.nud = function () { + return this; + }; + }; + + // match infix operators + // + // left associative + var infix = function (id, bp, led) { + var bindingPower = bp || operators[id]; + var s = symbol(id, bindingPower); + s.led = led || function (left) { + this.lhs = left; + this.rhs = expression(bindingPower); + this.type = "binary"; + return this; + }; + return s; + }; + + // match infix operators + // + // right associative + var infixr = function (id, bp, led) { + var s = symbol(id, bp); + s.led = led; + return s; + }; + + // match prefix operators + // + var prefix = function (id, nud) { + var s = symbol(id); + s.nud = nud || function () { + this.expression = expression(70); + this.type = "unary"; + return this; + }; + return s; + }; + + terminal("(end)"); + terminal("(name)"); + terminal("(literal)"); + terminal("(regex)"); + symbol(":"); + symbol(";"); + symbol(","); + symbol(")"); + symbol("]"); + symbol("}"); + symbol(".."); // range operator + infix("."); // map operator + infix("+"); // numeric addition + infix("-"); // numeric subtraction + infix("*"); // numeric multiplication + infix("/"); // numeric division + infix("%"); // numeric modulus + infix("="); // equality + infix("<"); // less than + infix(">"); // greater than + infix("!="); // not equal to + infix("<="); // less than or equal + infix(">="); // greater than or equal + infix("&"); // string concatenation + infix("and"); // Boolean AND + infix("or"); // Boolean OR + infix("in"); // is member of array + terminal("and"); // the 'keywords' can also be used as terminals (field names) + terminal("or"); // + terminal("in"); // + prefix("-"); // unary numeric negation + infix("~>"); // function application + + infixr("(error)", 10, function (left) { + this.lhs = left; + + this.error = node.error; + this.remaining = remainingTokens(); + this.type = 'error'; + return this; + }); + + // field wildcard (single level) + prefix('*', function () { + this.type = "wildcard"; + return this; + }); + + // descendant wildcard (multi-level) + prefix('**', function () { + this.type = "descendant"; + return this; + }); + + // parent operator + prefix('%', function () { + this.type = "parent"; + return this; + }); + + // function invocation + infix("(", operators['('], function (left) { + // left is is what we are trying to invoke + this.procedure = left; + this.type = 'function'; + this.arguments = []; + if (node.id !== ')') { + for (; ;) { + if (node.type === 'operator' && node.id === '?') { + // partial function application + this.type = 'partial'; + this.arguments.push(node); + advance('?'); + } else { + this.arguments.push(expression(0)); + } + if (node.id !== ',') break; + advance(','); + } + } + advance(")", true); + // if the name of the function is 'function' or λ, then this is function definition (lambda function) + if (left.type === 'name' && (left.value === 'function' || left.value === '\u03BB')) { + // all of the args must be VARIABLE tokens + this.arguments.forEach(function (arg, index) { + if (arg.type !== 'variable') { + return handleError({ + code: "S0208", + stack: (new Error()).stack, + position: arg.position, + token: arg.value, + value: index + 1 + }); + } + }); + this.type = 'lambda'; + // is the next token a '<' - if so, parse the function signature + if (node.id === '<') { + var sigPos = node.position; + var depth = 1; + var sig = '<'; + while (depth > 0 && node.id !== '{' && node.id !== '(end)') { + var tok = advance(); + if (tok.id === '>') { + depth--; + } else if (tok.id === '<') { + depth++; + } + sig += tok.value; + } + advance('>'); + try { + this.signature = parseSignature(sig); + } catch (err) { + // insert the position into this error + err.position = sigPos + err.offset; + return handleError(err); + } + } + // parse the function body + advance('{'); + this.body = expression(0); + advance('}'); + } + return this; + }); + + // parenthesis - block expression + prefix("(", function () { + var expressions = []; + while (node.id !== ")") { + expressions.push(expression(0)); + if (node.id !== ";") { + break; + } + advance(";"); + } + advance(")", true); + this.type = 'block'; + this.expressions = expressions; + return this; + }); + + // array constructor + prefix("[", function () { + var a = []; + if (node.id !== "]") { + for (; ;) { + var item = expression(0); + if (node.id === "..") { + // range operator + var range = {type: "binary", value: "..", position: node.position, lhs: item}; + advance(".."); + range.rhs = expression(0); + item = range; + } + a.push(item); + if (node.id !== ",") { + break; + } + advance(","); + } + } + advance("]", true); + this.expressions = a; + this.type = "unary"; + return this; + }); + + // filter - predicate or array index + infix("[", operators['['], function (left) { + if (node.id === "]") { + // empty predicate means maintain singleton arrays in the output + var step = left; + while (step && step.type === 'binary' && step.value === '[') { + step = step.lhs; + } + step.keepArray = true; + advance("]"); + return left; + } else { + this.lhs = left; + this.rhs = expression(operators[']']); + this.type = 'binary'; + advance("]", true); + return this; + } + }); + + // order-by + infix("^", operators['^'], function (left) { + advance("("); + var terms = []; + for (; ;) { + var term = { + descending: false + }; + if (node.id === "<") { + // ascending sort + advance("<"); + } else if (node.id === ">") { + // descending sort + term.descending = true; + advance(">"); + } else { + //unspecified - default to ascending + } + term.expression = expression(0); + terms.push(term); + if (node.id !== ",") { + break; + } + advance(","); + } + advance(")"); + this.lhs = left; + this.rhs = terms; + this.type = 'binary'; + return this; + }); + + var objectParser = function (left) { + var a = []; + if (node.id !== "}") { + for (; ;) { + var n = expression(0); + advance(":"); + var v = expression(0); + a.push([n, v]); // holds an array of name/value expression pairs + if (node.id !== ",") { + break; + } + advance(","); + } + } + advance("}", true); + if (typeof left === 'undefined') { + // NUD - unary prefix form + this.lhs = a; + this.type = "unary"; + } else { + // LED - binary infix form + this.lhs = left; + this.rhs = a; + this.type = 'binary'; + } + return this; + }; + + // object constructor + prefix("{", objectParser); + + // object grouping + infix("{", operators['{'], objectParser); + + // bind variable + infixr(":=", operators[':='], function (left) { + if (left.type !== 'variable') { + return handleError({ + code: "S0212", + stack: (new Error()).stack, + position: left.position, + token: left.value + }); + } + this.lhs = left; + this.rhs = expression(operators[':='] - 1); // subtract 1 from bindingPower for right associative operators + this.type = "binary"; + return this; + }); + + // focus variable bind + infix("@", operators['@'], function (left) { + this.lhs = left; + this.rhs = expression(operators['@']); + if(this.rhs.type !== 'variable') { + return handleError({ + code: "S0214", + stack: (new Error()).stack, + position: this.rhs.position, + token: "@" + }); + } + this.type = "binary"; + return this; + }); + + // index (position) variable bind + infix("#", operators['#'], function (left) { + this.lhs = left; + this.rhs = expression(operators['#']); + if(this.rhs.type !== 'variable') { + return handleError({ + code: "S0214", + stack: (new Error()).stack, + position: this.rhs.position, + token: "#" + }); + } + this.type = "binary"; + return this; + }); + + // if/then/else ternary operator ?: + infix("?", operators['?'], function (left) { + this.type = 'condition'; + this.condition = left; + this.then = expression(0); + if (node.id === ':') { + // else condition + advance(":"); + this.else = expression(0); + } + return this; + }); + + // object transformer + prefix("|", function () { + this.type = 'transform'; + this.pattern = expression(0); + advance('|'); + this.update = expression(0); + if (node.id === ',') { + advance(','); + this.delete = expression(0); + } + advance('|'); + return this; + }); + + // tail call optimization + // this is invoked by the post parser to analyse lambda functions to see + // if they make a tail call. If so, it is replaced by a thunk which will + // be invoked by the trampoline loop during function application. + // This enables tail-recursive functions to be written without growing the stack + var tailCallOptimize = function (expr) { + var result; + if (expr.type === 'function' && !expr.predicate) { + var thunk = {type: 'lambda', thunk: true, arguments: [], position: expr.position}; + thunk.body = expr; + result = thunk; + } else if (expr.type === 'condition') { + // analyse both branches + expr.then = tailCallOptimize(expr.then); + if (typeof expr.else !== 'undefined') { + expr.else = tailCallOptimize(expr.else); + } + result = expr; + } else if (expr.type === 'block') { + // only the last expression in the block + var length = expr.expressions.length; + if (length > 0) { + expr.expressions[length - 1] = tailCallOptimize(expr.expressions[length - 1]); + } + result = expr; + } else { + result = expr; + } + return result; + }; + + var ancestorLabel = 0; + var ancestorIndex = 0; + var ancestry = []; + + var seekParent = function (node, slot) { + switch (node.type) { + case 'name': + case 'wildcard': + slot.level--; + if(slot.level === 0) { + if (typeof node.ancestor === 'undefined') { + node.ancestor = slot; + } else { + // reuse the existing label + ancestry[slot.index].slot.label = node.ancestor.label; + node.ancestor = slot; + } + node.tuple = true; + } + break; + case 'parent': + slot.level++; + break; + case 'block': + // look in last expression in the block + if(node.expressions.length > 0) { + node.tuple = true; + slot = seekParent(node.expressions[node.expressions.length - 1], slot); + } + break; + case 'path': + // last step in path + node.tuple = true; + var index = node.steps.length - 1; + slot = seekParent(node.steps[index--], slot); + while (slot.level > 0 && index >= 0) { + // check previous steps + slot = seekParent(node.steps[index--], slot); + } + break; + default: + // error - can't derive ancestor + throw { + code: "S0217", + token: node.type, + position: node.position + }; + } + return slot; + }; + + var pushAncestry = function(result, value) { + if(typeof value.seekingParent !== 'undefined' || value.type === 'parent') { + var slots = (typeof value.seekingParent !== 'undefined') ? value.seekingParent : []; + if (value.type === 'parent') { + slots.push(value.slot); + } + if(typeof result.seekingParent === 'undefined') { + result.seekingParent = slots; + } else { + Array.prototype.push.apply(result.seekingParent, slots); + } + } + }; + + var resolveAncestry = function(path) { + var index = path.steps.length - 1; + var laststep = path.steps[index]; + var slots = (typeof laststep.seekingParent !== 'undefined') ? laststep.seekingParent : []; + if (laststep.type === 'parent') { + slots.push(laststep.slot); + } + for(var is = 0; is < slots.length; is++) { + var slot = slots[is]; + index = path.steps.length - 2; + while (slot.level > 0) { + if (index < 0) { + if(typeof path.seekingParent === 'undefined') { + path.seekingParent = [slot]; + } else { + path.seekingParent.push(slot); + } + break; + } + // try previous step + var step = path.steps[index--]; + // multiple contiguous steps that bind the focus should be skipped + while(index >= 0 && step.focus && path.steps[index].focus) { + step = path.steps[index--]; + } + slot = seekParent(step, slot); + } + } + }; + + // post-parse stage + // the purpose of this is to add as much semantic value to the parse tree as possible + // in order to simplify the work of the evaluator. + // This includes flattening the parts of the AST representing location paths, + // converting them to arrays of steps which in turn may contain arrays of predicates. + // following this, nodes containing '.' and '[' should be eliminated from the AST. + var processAST = function (expr) { + var result; + switch (expr.type) { + case 'binary': + switch (expr.value) { + case '.': + var lstep = processAST(expr.lhs); + + if (lstep.type === 'path') { + result = lstep; + } else { + result = {type: 'path', steps: [lstep]}; + } + if(lstep.type === 'parent') { + result.seekingParent = [lstep.slot]; + } + var rest = processAST(expr.rhs); + if (rest.type === 'function' && + rest.procedure.type === 'path' && + rest.procedure.steps.length === 1 && + rest.procedure.steps[0].type === 'name' && + result.steps[result.steps.length - 1].type === 'function') { + // next function in chain of functions - will override a thenable + result.steps[result.steps.length - 1].nextFunction = rest.procedure.steps[0].value; + } + if (rest.type === 'path') { + Array.prototype.push.apply(result.steps, rest.steps); + } else { + if(typeof rest.predicate !== 'undefined') { + rest.stages = rest.predicate; + delete rest.predicate; + } + result.steps.push(rest); + } + // any steps within a path that are string literals, should be changed to 'name' + result.steps.filter(function (step) { + if (step.type === 'number' || step.type === 'value') { + // don't allow steps to be numbers or the values true/false/null + throw { + code: "S0213", + stack: (new Error()).stack, + position: step.position, + value: step.value + }; + } + return step.type === 'string'; + }).forEach(function (lit) { + lit.type = 'name'; + }); + // any step that signals keeping a singleton array, should be flagged on the path + if (result.steps.filter(function (step) { + return step.keepArray === true; + }).length > 0) { + result.keepSingletonArray = true; + } + // if first step is a path constructor, flag it for special handling + var firststep = result.steps[0]; + if (firststep.type === 'unary' && firststep.value === '[') { + firststep.consarray = true; + } + // if the last step is an array constructor, flag it so it doesn't flatten + var laststep = result.steps[result.steps.length - 1]; + if (laststep.type === 'unary' && laststep.value === '[') { + laststep.consarray = true; + } + resolveAncestry(result); + break; + case '[': + // predicated step + // LHS is a step or a predicated step + // RHS is the predicate expr + result = processAST(expr.lhs); + var step = result; + var type = 'predicate'; + if (result.type === 'path') { + step = result.steps[result.steps.length - 1]; + type = 'stages'; + } + if (typeof step.group !== 'undefined') { + throw { + code: "S0209", + stack: (new Error()).stack, + position: expr.position + }; + } + if (typeof step[type] === 'undefined') { + step[type] = []; + } + var predicate = processAST(expr.rhs); + if(typeof predicate.seekingParent !== 'undefined') { + predicate.seekingParent.forEach(slot => { + if(slot.level === 1) { + seekParent(step, slot); + } else { + slot.level--; + } + }); + pushAncestry(step, predicate); + } + step[type].push({type: 'filter', expr: predicate, position: expr.position}); + break; + case '{': + // group-by + // LHS is a step or a predicated step + // RHS is the object constructor expr + result = processAST(expr.lhs); + if (typeof result.group !== 'undefined') { + throw { + code: "S0210", + stack: (new Error()).stack, + position: expr.position + }; + } + // object constructor - process each pair + result.group = { + lhs: expr.rhs.map(function (pair) { + return [processAST(pair[0]), processAST(pair[1])]; + }), + position: expr.position + }; + break; + case '^': + // order-by + // LHS is the array to be ordered + // RHS defines the terms + result = processAST(expr.lhs); + if (result.type !== 'path') { + result = {type: 'path', steps: [result]}; + } + var sortStep = {type: 'sort', position: expr.position}; + sortStep.terms = expr.rhs.map(function (terms) { + var expression = processAST(terms.expression); + pushAncestry(sortStep, expression); + return { + descending: terms.descending, + expression: expression + }; + }); + result.steps.push(sortStep); + resolveAncestry(result); + break; + case ':=': + result = {type: 'bind', value: expr.value, position: expr.position}; + result.lhs = processAST(expr.lhs); + result.rhs = processAST(expr.rhs); + pushAncestry(result, result.rhs); + break; + case '@': + result = processAST(expr.lhs); + step = result; + if (result.type === 'path') { + step = result.steps[result.steps.length - 1]; + } + // throw error if there are any predicates defined at this point + // at this point the only type of stages can be predicates + if(typeof step.stages !== 'undefined' || typeof step.predicate !== 'undefined') { + throw { + code: "S0215", + stack: (new Error()).stack, + position: expr.position + }; + } + // also throw if this is applied after an 'order-by' clause + if(step.type === 'sort') { + throw { + code: "S0216", + stack: (new Error()).stack, + position: expr.position + }; + } + if(expr.keepArray) { + step.keepArray = true; + } + step.focus = expr.rhs.value; + step.tuple = true; + break; + case '#': + result = processAST(expr.lhs); + step = result; + if (result.type === 'path') { + step = result.steps[result.steps.length - 1]; + } else { + result = {type: 'path', steps: [result]}; + if (typeof step.predicate !== 'undefined') { + step.stages = step.predicate; + delete step.predicate; + } + } + if (typeof step.stages === 'undefined') { + step.index = expr.rhs.value; + } else { + step.stages.push({type: 'index', value: expr.rhs.value, position: expr.position}); + } + step.tuple = true; + break; + case '~>': + result = {type: 'apply', value: expr.value, position: expr.position}; + result.lhs = processAST(expr.lhs); + result.rhs = processAST(expr.rhs); + break; + default: + result = {type: expr.type, value: expr.value, position: expr.position}; + result.lhs = processAST(expr.lhs); + result.rhs = processAST(expr.rhs); + pushAncestry(result, result.lhs); + pushAncestry(result, result.rhs); + } + break; + case 'unary': + result = {type: expr.type, value: expr.value, position: expr.position}; + if (expr.value === '[') { + // array constructor - process each item + result.expressions = expr.expressions.map(function (item) { + var value = processAST(item); + pushAncestry(result, value); + return value; + }); + } else if (expr.value === '{') { + // object constructor - process each pair + result.lhs = expr.lhs.map(function (pair) { + var key = processAST(pair[0]); + pushAncestry(result, key); + var value = processAST(pair[1]); + pushAncestry(result, value); + return [key, value]; + }); + } else { + // all other unary expressions - just process the expression + result.expression = processAST(expr.expression); + // if unary minus on a number, then pre-process + if (expr.value === '-' && result.expression.type === 'number') { + result = result.expression; + result.value = -result.value; + } else { + pushAncestry(result, result.expression); + } + } + break; + case 'function': + case 'partial': + result = {type: expr.type, name: expr.name, value: expr.value, position: expr.position}; + result.arguments = expr.arguments.map(function (arg) { + var argAST = processAST(arg); + pushAncestry(result, argAST); + return argAST; + }); + result.procedure = processAST(expr.procedure); + break; + case 'lambda': + result = { + type: expr.type, + arguments: expr.arguments, + signature: expr.signature, + position: expr.position + }; + var body = processAST(expr.body); + result.body = tailCallOptimize(body); + break; + case 'condition': + result = {type: expr.type, position: expr.position}; + result.condition = processAST(expr.condition); + pushAncestry(result, result.condition); + result.then = processAST(expr.then); + pushAncestry(result, result.then); + if (typeof expr.else !== 'undefined') { + result.else = processAST(expr.else); + pushAncestry(result, result.else); + } + break; + case 'transform': + result = {type: expr.type, position: expr.position}; + result.pattern = processAST(expr.pattern); + result.update = processAST(expr.update); + if (typeof expr.delete !== 'undefined') { + result.delete = processAST(expr.delete); + } + break; + case 'block': + result = {type: expr.type, position: expr.position}; + // array of expressions - process each one + result.expressions = expr.expressions.map(function (item) { + var part = processAST(item); + pushAncestry(result, part); + if (part.consarray || (part.type === 'path' && part.steps[0].consarray)) { + result.consarray = true; + } + return part; + }); + // TODO scan the array of expressions to see if any of them assign variables + // if so, need to mark the block as one that needs to create a new frame + break; + case 'name': + result = {type: 'path', steps: [expr]}; + if (expr.keepArray) { + result.keepSingletonArray = true; + } + break; + case 'parent': + result = {type: 'parent', slot: { label: '!' + ancestorLabel++, level: 1, index: ancestorIndex++ } }; + ancestry.push(result); + break; + case 'string': + case 'number': + case 'value': + case 'wildcard': + case 'descendant': + case 'variable': + case 'regex': + result = expr; + break; + case 'operator': + // the tokens 'and' and 'or' might have been used as a name rather than an operator + if (expr.value === 'and' || expr.value === 'or' || expr.value === 'in') { + expr.type = 'name'; + result = processAST(expr); + } else /* istanbul ignore else */ if (expr.value === '?') { + // partial application + result = expr; + } else { + throw { + code: "S0201", + stack: (new Error()).stack, + position: expr.position, + token: expr.value + }; + } + break; + case 'error': + result = expr; + if (expr.lhs) { + result = processAST(expr.lhs); + } + break; + default: + var code = "S0206"; + /* istanbul ignore else */ + if (expr.id === '(end)') { + code = "S0207"; + } + var err = { + code: code, + position: expr.position, + token: expr.value + }; + if (recover) { + errors.push(err); + return {type: 'error', error: err}; + } else { + err.stack = (new Error()).stack; + throw err; + } + } + if (expr.keepArray) { + result.keepArray = true; + } + return result; + }; + + // now invoke the tokenizer and the parser and return the syntax tree + lexer = tokenizer(source); + advance(); + // parse the tokens + var expr = expression(0); + if (node.id !== '(end)') { + var err = { + code: "S0201", + position: node.position, + token: node.value + }; + handleError(err); + } + expr = processAST(expr); + + if(expr.type === 'parent' || typeof expr.seekingParent !== 'undefined') { + // error - trying to derive ancestor at top level + throw { + code: "S0217", + token: expr.type, + position: expr.position + }; + } + + if (errors.length > 0) { + expr.errors = errors; + } + + return expr; + }; + + return parser; +})(); + +module.exports = parser; + +},{"./signature":5}],5:[function(require,module,exports){ +/** + * © Copyright IBM Corp. 2016, 2018 All Rights Reserved + * Project name: JSONata + * This project is licensed under the MIT License, see LICENSE + */ + +var utils = require('./utils'); + +const signature = (() => { + 'use strict'; + + // A mapping between the function signature symbols and the full plural of the type + // Expected to be used in error messages + var arraySignatureMapping = { + "a": "arrays", + "b": "booleans", + "f": "functions", + "n": "numbers", + "o": "objects", + "s": "strings" + }; + + /** + * Parses a function signature definition and returns a validation function + * @param {string} signature - the signature between the + * @returns {Function} validation function + */ + function parseSignature(signature) { + // create a Regex that represents this signature and return a function that when invoked, + // returns the validated (possibly fixed-up) arguments, or throws a validation error + // step through the signature, one symbol at a time + var position = 1; + var params = []; + var param = {}; + var prevParam = param; + while (position < signature.length) { + var symbol = signature.charAt(position); + if (symbol === ':') { + // TODO figure out what to do with the return type + // ignore it for now + break; + } + + var next = function () { + params.push(param); + prevParam = param; + param = {}; + }; + + var findClosingBracket = function (str, start, openSymbol, closeSymbol) { + // returns the position of the closing symbol (e.g. bracket) in a string + // that balances the opening symbol at position start + var depth = 1; + var position = start; + while (position < str.length) { + position++; + symbol = str.charAt(position); + if (symbol === closeSymbol) { + depth--; + if (depth === 0) { + // we're done + break; // out of while loop + } + } else if (symbol === openSymbol) { + depth++; + } + } + return position; + }; + + switch (symbol) { + case 's': // string + case 'n': // number + case 'b': // boolean + case 'l': // not so sure about expecting null? + case 'o': // object + param.regex = '[' + symbol + 'm]'; + param.type = symbol; + next(); + break; + case 'a': // array + // normally treat any value as singleton array + param.regex = '[asnblfom]'; + param.type = symbol; + param.array = true; + next(); + break; + case 'f': // function + param.regex = 'f'; + param.type = symbol; + next(); + break; + case 'j': // any JSON type + param.regex = '[asnblom]'; + param.type = symbol; + next(); + break; + case 'x': // any type + param.regex = '[asnblfom]'; + param.type = symbol; + next(); + break; + case '-': // use context if param not supplied + prevParam.context = true; + prevParam.contextRegex = new RegExp(prevParam.regex); // pre-compiled to test the context type at runtime + prevParam.regex += '?'; + break; + case '?': // optional param + case '+': // one or more + prevParam.regex += symbol; + break; + case '(': // choice of types + // search forward for matching ')' + var endParen = findClosingBracket(signature, position, '(', ')'); + var choice = signature.substring(position + 1, endParen); + if (choice.indexOf('<') === -1) { + // no parameterized types, simple regex + param.regex = '[' + choice + 'm]'; + } else { + // TODO harder + throw { + code: "S0402", + stack: (new Error()).stack, + value: choice, + offset: position + }; + } + param.type = '(' + choice + ')'; + position = endParen; + next(); + break; + case '<': // type parameter - can only be applied to 'a' and 'f' + if (prevParam.type === 'a' || prevParam.type === 'f') { + // search forward for matching '>' + var endPos = findClosingBracket(signature, position, '<', '>'); + prevParam.subtype = signature.substring(position + 1, endPos); + position = endPos; + } else { + throw { + code: "S0401", + stack: (new Error()).stack, + value: prevParam.type, + offset: position + }; + } + break; + } + position++; + } + var regexStr = '^' + + params.map(function (param) { + return '(' + param.regex + ')'; + }).join('') + + '$'; + var regex = new RegExp(regexStr); + var getSymbol = function (value) { + var symbol; + if (utils.isFunction(value)) { + symbol = 'f'; + } else { + var type = typeof value; + switch (type) { + case 'string': + symbol = 's'; + break; + case 'number': + symbol = 'n'; + break; + case 'boolean': + symbol = 'b'; + break; + case 'object': + if (value === null) { + symbol = 'l'; + } else if (Array.isArray(value)) { + symbol = 'a'; + } else { + symbol = 'o'; + } + break; + case 'undefined': + default: + // any value can be undefined, but should be allowed to match + symbol = 'm'; // m for missing + } + } + return symbol; + }; + + var throwValidationError = function (badArgs, badSig) { + // to figure out where this went wrong we need apply each component of the + // regex to each argument until we get to the one that fails to match + var partialPattern = '^'; + var goodTo = 0; + for (var index = 0; index < params.length; index++) { + partialPattern += params[index].regex; + var match = badSig.match(partialPattern); + if (match === null) { + // failed here + throw { + code: "T0410", + stack: (new Error()).stack, + value: badArgs[goodTo], + index: goodTo + 1 + }; + } + goodTo = match[0].length; + } + // if it got this far, it's probably because of extraneous arguments (we + // haven't added the trailing '$' in the regex yet. + throw { + code: "T0410", + stack: (new Error()).stack, + value: badArgs[goodTo], + index: goodTo + 1 + }; + }; + + return { + definition: signature, + validate: function (args, context) { + var suppliedSig = ''; + args.forEach(function (arg) { + suppliedSig += getSymbol(arg); + }); + var isValid = regex.exec(suppliedSig); + if (isValid) { + var validatedArgs = []; + var argIndex = 0; + params.forEach(function (param, index) { + var arg = args[argIndex]; + var match = isValid[index + 1]; + if (match === '') { + if (param.context && param.contextRegex) { + // substitute context value for missing arg + // first check that the context value is the right type + var contextType = getSymbol(context); + // test contextType against the regex for this arg (without the trailing ?) + if (param.contextRegex.test(contextType)) { + validatedArgs.push(context); + } else { + // context value not compatible with this argument + throw { + code: "T0411", + stack: (new Error()).stack, + value: context, + index: argIndex + 1 + }; + } + } else { + validatedArgs.push(arg); + argIndex++; + } + } else { + // may have matched multiple args (if the regex ends with a '+' + // split into single tokens + match.split('').forEach(function (single) { + if (param.type === 'a') { + if (single === 'm') { + // missing (undefined) + arg = undefined; + } else { + arg = args[argIndex]; + var arrayOK = true; + // is there type information on the contents of the array? + if (typeof param.subtype !== 'undefined') { + if (single !== 'a' && match !== param.subtype) { + arrayOK = false; + } else if (single === 'a') { + if (arg.length > 0) { + var itemType = getSymbol(arg[0]); + if (itemType !== param.subtype.charAt(0)) { // TODO recurse further + arrayOK = false; + } else { + // make sure every item in the array is this type + var differentItems = arg.filter(function (val) { + return (getSymbol(val) !== itemType); + }); + arrayOK = (differentItems.length === 0); + } + } + } + } + if (!arrayOK) { + throw { + code: "T0412", + stack: (new Error()).stack, + value: arg, + index: argIndex + 1, + type: arraySignatureMapping[param.subtype] + }; + } + // the function expects an array. If it's not one, make it so + if (single !== 'a') { + arg = [arg]; + } + } + validatedArgs.push(arg); + argIndex++; + } else { + validatedArgs.push(arg); + argIndex++; + } + }); + } + }); + return validatedArgs; + } + throwValidationError(args, suppliedSig); + } + }; + } + + return parseSignature; +})(); + +module.exports = signature; + +},{"./utils":6}],6:[function(require,module,exports){ +/** + * © Copyright IBM Corp. 2016, 2018 All Rights Reserved + * Project name: JSONata + * This project is licensed under the MIT License, see LICENSE + */ + +const utils = (() => { + 'use strict'; + + /** + * Check if value is a finite number + * @param {float} n - number to evaluate + * @returns {boolean} True if n is a finite number + */ + function isNumeric(n) { + var isNum = false; + if(typeof n === 'number') { + isNum = !isNaN(n); + if (isNum && !isFinite(n)) { + throw { + code: "D1001", + value: n, + stack: (new Error()).stack + }; + } + } + return isNum; + } + + /** + * Returns true if the arg is an array of strings + * @param {*} arg - the item to test + * @returns {boolean} True if arg is an array of strings + */ + function isArrayOfStrings(arg) { + var result = false; + /* istanbul ignore else */ + if(Array.isArray(arg)) { + result = (arg.filter(function(item){return typeof item !== 'string';}).length === 0); + } + return result; + } + + /** + * Returns true if the arg is an array of numbers + * @param {*} arg - the item to test + * @returns {boolean} True if arg is an array of numbers + */ + function isArrayOfNumbers(arg) { + var result = false; + if(Array.isArray(arg)) { + result = (arg.filter(function(item){return !isNumeric(item);}).length === 0); + } + return result; + } + + /** + * Create an empty sequence to contain query results + * @returns {Array} - empty sequence + */ + function createSequence() { + var sequence = []; + sequence.sequence = true; + if (arguments.length === 1) { + sequence.push(arguments[0]); + } + return sequence; + } + + /** + * Tests if a value is a sequence + * @param {*} value the value to test + * @returns {boolean} true if it's a sequence + */ + function isSequence(value) { + return value.sequence === true && Array.isArray(value); + } + + /** + * + * @param {Object} arg - expression to test + * @returns {boolean} - true if it is a function (lambda or built-in) + */ + function isFunction(arg) { + return ((arg && (arg._jsonata_function === true || arg._jsonata_lambda === true)) || typeof arg === 'function'); + } + + /** + * Returns the arity (number of arguments) of the function + * @param {*} func - the function + * @returns {*} - the arity + */ + function getFunctionArity(func) { + var arity = typeof func.arity === 'number' ? func.arity : + typeof func.implementation === 'function' ? func.implementation.length : + typeof func.length === 'number' ? func.length : func.arguments.length; + return arity; + } + + /** + * Tests whether arg is a lambda function + * @param {*} arg - the value to test + * @returns {boolean} - true if it is a lambda function + */ + function isLambda(arg) { + return arg && arg._jsonata_lambda === true; + } + + // istanbul ignore next + var $Symbol = typeof Symbol === "function" ? Symbol : {}; + // istanbul ignore next + var iteratorSymbol = $Symbol.iterator || "@@iterator"; + + /** + * @param {Object} arg - expression to test + * @returns {boolean} - true if it is iterable + */ + function isIterable(arg) { + return ( + typeof arg === 'object' && + arg !== null && + iteratorSymbol in arg && + 'next' in arg && + typeof arg.next === 'function' + ); + } + + /** + * Compares two values for equality + * @param {*} lhs first value + * @param {*} rhs second value + * @returns {boolean} true if they are deep equal + */ + function isDeepEqual(lhs, rhs) { + if (lhs === rhs) { + return true; + } + if(typeof lhs === 'object' && typeof rhs === 'object' && lhs !== null && rhs !== null) { + if(Array.isArray(lhs) && Array.isArray(rhs)) { + // both arrays (or sequences) + // must be the same length + if(lhs.length !== rhs.length) { + return false; + } + // must contain same values in same order + for(var ii = 0; ii < lhs.length; ii++) { + if(!isDeepEqual(lhs[ii], rhs[ii])) { + return false; + } + } + return true; + } + // both objects + // must have the same set of keys (in any order) + var lkeys = Object.getOwnPropertyNames(lhs); + var rkeys = Object.getOwnPropertyNames(rhs); + if(lkeys.length !== rkeys.length) { + return false; + } + lkeys = lkeys.sort(); + rkeys = rkeys.sort(); + for(ii=0; ii < lkeys.length; ii++) { + if(lkeys[ii] !== rkeys[ii]) { + return false; + } + } + // must have the same values + for(ii=0; ii < lkeys.length; ii++) { + var key = lkeys[ii]; + if(!isDeepEqual(lhs[key], rhs[key])) { + return false; + } + } + return true; + } + return false; + } + + return { + isNumeric, + isArrayOfStrings, + isArrayOfNumbers, + createSequence, + isSequence, + isFunction, + isLambda, + isIterable, + getFunctionArity, + isDeepEqual + }; +})(); + +module.exports = utils; + +},{}]},{},[3])(3) +}); + + /***/ }), /***/ 375: @@ -3542,6 +10962,88 @@ var pump = function () { module.exports = pump +/***/ }), + +/***/ 461: +/***/ (function(module, __unusedexports, __webpack_require__) { + +const jsonata = __webpack_require__(350); + + +/** + * @typedef {Object} SecretRequest + * @property {string} path + * @property {string} selector + */ + +/** + * @template {SecretRequest} TRequest + * @typedef {Object} SecretResponse + * @property {TRequest} request + * @property {string} value + * @property {boolean} cachedResponse + */ + + /** + * @template TRequest + * @param {Array} secretRequests + * @param {import('got').Got} client + * @return {Promise[]>} + */ +async function getSecrets(secretRequests, client) { + const responseCache = new Map(); + const results = []; + for (const secretRequest of secretRequests) { + const { path, selector } = secretRequest; + + const requestPath = `v1${path}`; + let body; + let cachedResponse = false; + if (responseCache.has(requestPath)) { + body = responseCache.get(requestPath); + cachedResponse = true; + } else { + const result = await client.get(requestPath); + body = result.body; + responseCache.set(requestPath, body); + } + + const value = selectData(JSON.parse(body), selector); + results.push({ + request: secretRequest, + value, + cachedResponse + }); + } + return results; +} + +/** + * Uses a Jsonata selector retrieve a bit of data from the result + * @param {object} data + * @param {string} selector + */ +function selectData(data, selector) { + const ata = jsonata(selector); + let result = JSON.stringify(ata.evaluate(data)); + // Compat for custom engines + if (!result && ata.ast().type === "path" && ata.ast()['steps'].length === 1 && selector !== 'data' && 'data' in data) { + result = JSON.stringify(jsonata(`data.${selector}`).evaluate(data)); + } else if (!result) { + throw Error(`Unable to retrieve result for ${selector}. No match data was found. Double check your Key or Selector.`); + } + + if (result.startsWith(`"`)) { + result = result.substring(1, result.length - 1); + } + return result; +} + +module.exports = { + getSecrets, + selectData +} + /***/ }), /***/ 470: @@ -3949,6 +11451,22 @@ module.exports = timer; module.exports.default = timer; +/***/ }), + +/***/ 492: +/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { + +const core = __webpack_require__(470); +const { exportSecrets } = __webpack_require__(928); + +(async () => { + try { + await core.group('Get Vault Secrets', exportSecrets); + } catch (error) { + core.setFailed(error.message); + } +})(); + /***/ }), /***/ 534: @@ -4720,18 +12238,15 @@ module.exports = require("util"); /***/ }), /***/ 676: -/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { +/***/ (function(module, __unusedexports, __webpack_require__) { -const core = __webpack_require__(470); -const { exportSecrets } = __webpack_require__(928); +const auth = __webpack_require__(151); +const secrets = __webpack_require__(461); -(async () => { - try { - await core.group('Get Vault Secrets', exportSecrets); - } catch (error) { - core.setFailed(error.message); - } -})(); +module.exports = { + auth, + secrets +}; /***/ }), @@ -5736,7 +13251,8 @@ module.exports = require("dns"); const core = __webpack_require__(470); const command = __webpack_require__(431); const got = __webpack_require__(77).default; -const { retrieveToken } = __webpack_require__(151); +const jsonata = __webpack_require__(350); +const { auth: { retrieveToken }, secrets: { getSecrets } } = __webpack_require__(676); const AUTH_METHODS = ['approle', 'token', 'github']; const VALID_KV_VERSION = [-1, 1, 2]; @@ -5773,8 +13289,9 @@ async function exportSecrets() { defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace; } + const vaultToken = await retrieveToken(vaultMethod, got.extend(defaultOptions)); + defaultOptions.headers['X-Vault-Token'] = vaultToken; const client = got.extend(defaultOptions); - const vaultToken = await retrieveToken(vaultMethod, client); if (!enginePath) { enginePath = 'secret'; @@ -5789,62 +13306,42 @@ async function exportSecrets() { throw Error(`You must provide a valid K/V version (${VALID_KV_VERSION.slice(1).join(', ')}). Input: "${kvVersion}"`); } - const responseCache = new Map(); - for (const secretRequest of secretRequests) { - const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest; - const requestOptions = { - headers: { - 'X-Vault-Token': vaultToken - }, - }; + const requests = secretRequests.map(request => { + const { path, selector } = request; - for (const [headerName, headerValue] of extraHeaders) { - requestOptions.headers[headerName] = headerValue; - } - - if (vaultNamespace != null) { - requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; + if (path.startsWith('/')) { + return request; } + const kvPath = (kvVersion === 2) + ? `/${enginePath}/data/${path}` + : `/${enginePath}/${path}`; + const kvSelector = (kvVersion === 2) + ? `data.data.${selector}` + : `data.${selector}`; + return { ...request, path: kvPath, selector: kvSelector }; + }); - let requestPath = `v1`; - const kvRequest = !secretPath.startsWith('/') - if (!kvRequest) { - requestPath += secretPath; - } else { - requestPath += (kvVersion === 2) - ? `/${enginePath}/data/${secretPath}` - : `/${enginePath}/${secretPath}`; - } + const results = await getSecrets(requests, client); - let body; - if (responseCache.has(requestPath)) { - body = responseCache.get(requestPath); + for (const result of results) { + const { value, request, cachedResponse } = result; + if (cachedResponse) { core.debug('ℹ using cached response'); - } else { - const result = await client.get(requestPath, requestOptions); - body = result.body; - responseCache.set(requestPath, body); - } - - let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion; - - const secretData = getResponseData(body, dataDepth); - const value = selectData(secretData, secretSelector, isJSONPath); + } command.issue('add-mask', value); if (exportEnv) { - core.exportVariable(envVarName, `${value}`); + core.exportVariable(request.envVarName, `${value}`); } - core.setOutput(outputVarName, `${value}`); - core.debug(`✔ ${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`); + core.setOutput(request.outputVarName, `${value}`); + core.debug(`✔ ${request.path} => outputs.${request.outputVarName}${exportEnv ? ` | env.${request.envVarName}` : ''}`); } }; /** @typedef {Object} SecretRequest - * @property {string} secretPath + * @property {string} path * @property {string} envVarName * @property {string} outputVarName - * @property {string} secretSelector - * @property {boolean} isJSONPath + * @property {string} selector */ /** @@ -5861,12 +13358,12 @@ function parseSecretsInput(secretsInput) { /** @type {SecretRequest[]} */ const output = []; for (const secret of secrets) { - let path = secret; + let pathSpec = secret; let outputVarName = null; const renameSigilIndex = secret.lastIndexOf('|'); if (renameSigilIndex > -1) { - path = secret.substring(0, renameSigilIndex).trim(); + pathSpec = secret.substring(0, renameSigilIndex).trim(); outputVarName = secret.substring(renameSigilIndex + 1).trim(); if (outputVarName.length < 1) { @@ -5874,7 +13371,7 @@ function parseSecretsInput(secretsInput) { } } - const pathParts = path + const pathParts = pathSpec .split(/\s+/) .map(part => part.trim()) .filter(part => part.length !== 0); @@ -5883,64 +13380,43 @@ function parseSecretsInput(secretsInput) { throw Error(`You must provide a valid path and key. Input: "${secret}"`); } - const [secretPath, secretSelector] = pathParts; + const [path, selector] = pathParts; - const isJSONPath = secretSelector.includes('.'); + /** @type {any} */ + const selectorAst = jsonata(selector).ast(); - if (isJSONPath && !outputVarName) { + if ((selectorAst.type !== "path" || selectorAst.steps[0].stages) && !outputVarName) { throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); } let envVarName = outputVarName; if (!outputVarName) { - outputVarName = secretSelector; - envVarName = normalizeOutputKey(outputVarName); + outputVarName = normalizeOutputKey(selector); + envVarName = normalizeOutputKey(selector, true); } output.push({ - secretPath, + path, envVarName, outputVarName, - secretSelector, - isJSONPath + selector }); } return output; } /** - * Parses a JSON response and returns the secret data - * @param {string} responseBody - * @param {number} dataLevel - */ -function getResponseData(responseBody, dataLevel) { - let secretData = JSON.parse(responseBody); - - for (let i = 0; i < dataLevel; i++) { - secretData = secretData['data']; - } - return secretData; -} - -/** - * Parses a JSON response and returns the secret data - * @param {Object} data - * @param {string} selector - */ -function selectData(data, selector, isJSONPath) { - if (!isJSONPath) { - return data[selector]; - } - - // TODO: JSONPath -} - -/** - * Replaces any forward-slash characters to + * Replaces any dot chars to __ and removes non-ascii charts * @param {string} dataKey + * @param {boolean=} isEnvVar */ -function normalizeOutputKey(dataKey) { - return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase(); +function normalizeOutputKey(dataKey, isEnvVar = false) { + let outputKey = dataKey + .replace('.', '__').replace(/[^\p{L}\p{N}_-]/gu, ''); + if (isEnvVar) { + outputKey = outputKey.toUpperCase(); + } + return outputKey; } /** @@ -5971,12 +13447,10 @@ function parseHeadersInput(inputKey, inputOptions) { module.exports = { exportSecrets, parseSecretsInput, - parseResponse: getResponseData, normalizeOutputKey, parseHeadersInput }; - /***/ }), /***/ 946: diff --git a/package-lock.json b/package-lock.json index 12c7c4ab..a4680898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7693,6 +7693,11 @@ "minimist": "^1.2.5" } }, + "jsonata": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-1.8.2.tgz", + "integrity": "sha512-ma5F/Bs47dZfJfDZ0Dt37eIbzVBVKZIDqsZSqdCCAPNHxKn+s3+CfMA6ahVVlf8Y1hyIjXkVLFU7yv4XxRfihA==" + }, "jsonfile": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", diff --git a/package.json b/package.json index c9de6e2b..106ece89 100644 --- a/package.json +++ b/package.json @@ -4,18 +4,23 @@ "description": "A Github Action that allows you to consume vault secrets as secure environment variables.", "main": "dist/index.js", "scripts": { - "build": "ncc build src/index.js -o dist", + "build": "ncc build src/entry.js -o dist", "test": "jest", "test:integration:basic": "jest -c integrationTests/basic/jest.config.js", "test:integration:enterprise": "jest -c integrationTests/enterprise/jest.config.js", "test:e2e": "jest -c integrationTests/e2e/jest.config.js" }, + "files": [ + "src/**/*", + "dist/**/*" + ], "release": { "branch": "master", "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", - "@semantic-release/github" + "@semantic-release/github", + "@semantic-release/npm" ], "ci": false }, @@ -38,8 +43,9 @@ }, "homepage": "https://github.com/RichiCoder1/vault-action#readme", "dependencies": { + "@actions/core": "^1.2.3", "got": "^10.2.2", - "@actions/core": "^1.2.3" + "jsonata": "^1.8.2" }, "devDependencies": { "@types/got": "^9.6.9", diff --git a/src/action.js b/src/action.js index 9bfea2c5..ab8e1a59 100644 --- a/src/action.js +++ b/src/action.js @@ -2,7 +2,8 @@ const core = require('@actions/core'); const command = require('@actions/core/lib/command'); const got = require('got').default; -const { retrieveToken } = require('./auth'); +const jsonata = require('jsonata'); +const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index'); const AUTH_METHODS = ['approle', 'token', 'github']; const VALID_KV_VERSION = [-1, 1, 2]; @@ -39,8 +40,9 @@ async function exportSecrets() { defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace; } + const vaultToken = await retrieveToken(vaultMethod, got.extend(defaultOptions)); + defaultOptions.headers['X-Vault-Token'] = vaultToken; const client = got.extend(defaultOptions); - const vaultToken = await retrieveToken(vaultMethod, client); if (!enginePath) { enginePath = 'secret'; @@ -55,62 +57,42 @@ async function exportSecrets() { throw Error(`You must provide a valid K/V version (${VALID_KV_VERSION.slice(1).join(', ')}). Input: "${kvVersion}"`); } - const responseCache = new Map(); - for (const secretRequest of secretRequests) { - const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest; - const requestOptions = { - headers: { - 'X-Vault-Token': vaultToken - }, - }; - - for (const [headerName, headerValue] of extraHeaders) { - requestOptions.headers[headerName] = headerValue; - } - - if (vaultNamespace != null) { - requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; - } + const requests = secretRequests.map(request => { + const { path, selector } = request; - let requestPath = `v1`; - const kvRequest = !secretPath.startsWith('/') - if (!kvRequest) { - requestPath += secretPath; - } else { - requestPath += (kvVersion === 2) - ? `/${enginePath}/data/${secretPath}` - : `/${enginePath}/${secretPath}`; + if (path.startsWith('/')) { + return request; } - - let body; - if (responseCache.has(requestPath)) { - body = responseCache.get(requestPath); + const kvPath = (kvVersion === 2) + ? `/${enginePath}/data/${path}` + : `/${enginePath}/${path}`; + const kvSelector = (kvVersion === 2) + ? `data.data.${selector}` + : `data.${selector}`; + return { ...request, path: kvPath, selector: kvSelector }; + }); + + const results = await getSecrets(requests, client); + + for (const result of results) { + const { value, request, cachedResponse } = result; + if (cachedResponse) { core.debug('ℹ using cached response'); - } else { - const result = await client.get(requestPath, requestOptions); - body = result.body; - responseCache.set(requestPath, body); - } - - let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion; - - const secretData = getResponseData(body, dataDepth); - const value = selectData(secretData, secretSelector, isJSONPath); + } command.issue('add-mask', value); if (exportEnv) { - core.exportVariable(envVarName, `${value}`); + core.exportVariable(request.envVarName, `${value}`); } - core.setOutput(outputVarName, `${value}`); - core.debug(`✔ ${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`); + core.setOutput(request.outputVarName, `${value}`); + core.debug(`✔ ${request.path} => outputs.${request.outputVarName}${exportEnv ? ` | env.${request.envVarName}` : ''}`); } }; /** @typedef {Object} SecretRequest - * @property {string} secretPath + * @property {string} path * @property {string} envVarName * @property {string} outputVarName - * @property {string} secretSelector - * @property {boolean} isJSONPath + * @property {string} selector */ /** @@ -127,12 +109,12 @@ function parseSecretsInput(secretsInput) { /** @type {SecretRequest[]} */ const output = []; for (const secret of secrets) { - let path = secret; + let pathSpec = secret; let outputVarName = null; const renameSigilIndex = secret.lastIndexOf('|'); if (renameSigilIndex > -1) { - path = secret.substring(0, renameSigilIndex).trim(); + pathSpec = secret.substring(0, renameSigilIndex).trim(); outputVarName = secret.substring(renameSigilIndex + 1).trim(); if (outputVarName.length < 1) { @@ -140,7 +122,7 @@ function parseSecretsInput(secretsInput) { } } - const pathParts = path + const pathParts = pathSpec .split(/\s+/) .map(part => part.trim()) .filter(part => part.length !== 0); @@ -149,64 +131,43 @@ function parseSecretsInput(secretsInput) { throw Error(`You must provide a valid path and key. Input: "${secret}"`); } - const [secretPath, secretSelector] = pathParts; + const [path, selector] = pathParts; - const isJSONPath = secretSelector.includes('.'); + /** @type {any} */ + const selectorAst = jsonata(selector).ast(); - if (isJSONPath && !outputVarName) { + if ((selectorAst.type !== "path" || selectorAst.steps[0].stages) && !outputVarName) { throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); } let envVarName = outputVarName; if (!outputVarName) { - outputVarName = secretSelector; - envVarName = normalizeOutputKey(outputVarName); + outputVarName = normalizeOutputKey(selector); + envVarName = normalizeOutputKey(selector, true); } output.push({ - secretPath, + path, envVarName, outputVarName, - secretSelector, - isJSONPath + selector }); } return output; } /** - * Parses a JSON response and returns the secret data - * @param {string} responseBody - * @param {number} dataLevel - */ -function getResponseData(responseBody, dataLevel) { - let secretData = JSON.parse(responseBody); - - for (let i = 0; i < dataLevel; i++) { - secretData = secretData['data']; - } - return secretData; -} - -/** - * Parses a JSON response and returns the secret data - * @param {Object} data - * @param {string} selector - */ -function selectData(data, selector, isJSONPath) { - if (!isJSONPath) { - return data[selector]; - } - - // TODO: JSONPath -} - -/** - * Replaces any forward-slash characters to + * Replaces any dot chars to __ and removes non-ascii charts * @param {string} dataKey + * @param {boolean=} isEnvVar */ -function normalizeOutputKey(dataKey) { - return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase(); +function normalizeOutputKey(dataKey, isEnvVar = false) { + let outputKey = dataKey + .replace('.', '__').replace(/[^\p{L}\p{N}_-]/gu, ''); + if (isEnvVar) { + outputKey = outputKey.toUpperCase(); + } + return outputKey; } /** @@ -237,7 +198,6 @@ function parseHeadersInput(inputKey, inputOptions) { module.exports = { exportSecrets, parseSecretsInput, - parseResponse: getResponseData, normalizeOutputKey, parseHeadersInput -}; +}; \ No newline at end of file diff --git a/src/action.test.js b/src/action.test.js index e400f0c4..c2738ac8 100644 --- a/src/action.test.js +++ b/src/action.test.js @@ -17,11 +17,10 @@ describe('parseSecretsInput', () => { it('parses simple secret', () => { const output = parseSecretsInput('test key'); expect(output).toContainEqual({ - secretPath: 'test', - secretSelector: 'key', + path: 'test', + selector: 'key', outputVarName: 'key', - envVarName: 'KEY', - isJSONPath: false + envVarName: 'KEY' }); }); @@ -49,10 +48,10 @@ describe('parseSecretsInput', () => { expect(output).toHaveLength(2); expect(output[0]).toMatchObject({ - secretPath: 'first', + path: 'first', }); expect(output[1]).toMatchObject({ - secretPath: 'second', + path: 'second', }); }); @@ -78,7 +77,7 @@ describe('parseSecretsInput', () => { expect(output).toHaveLength(3); expect(output[0]).toMatchObject({ - secretPath: 'first', + path: 'first', }); expect(output[1]).toMatchObject({ outputVarName: 'b', @@ -88,7 +87,7 @@ describe('parseSecretsInput', () => { outputVarName: 'SOME_C', envVarName: 'SOME_C', }); - }) + }); }); describe('parseHeaders', () => { @@ -129,41 +128,8 @@ describe('parseHeaders', () => { const result = parseHeadersInput('extraHeaders'); expect(Array.from(result)).toHaveLength(0); }); -}) - -describe('parseResponse', () => { - // https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response - it('parses K/V version 1 response', () => { - const response = JSON.stringify({ - data: { - foo: 'bar' - } - }) - const output = parseResponse(response, 1); - - expect(output).toEqual({ - foo: 'bar' - }); - }); - - // https://www.vaultproject.io/api/secret/kv/kv-v2.html#read-secret-version - it('parses K/V version 2 response', () => { - const response = JSON.stringify({ - data: { - data: { - foo: 'bar' - } - } - }) - const output = parseResponse(response, 2); - - expect(output).toEqual({ - foo: 'bar' - }); - }); }); - describe('exportSecrets', () => { beforeEach(() => { jest.resetAllMocks(); @@ -224,6 +190,18 @@ describe('exportSecrets', () => { expect(core.setOutput).toBeCalledWith('key', '1'); }); + it('intl secret retrieval', async () => { + mockInput('测试 测试'); + mockVaultData({ + 测试: 1 + }); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('测试', '1'); + expect(core.setOutput).toBeCalledWith('测试', '1'); + }); + it('mapped secret retrieval', async () => { mockInput('test key|TEST_NAME'); mockVaultData({ @@ -267,4 +245,16 @@ describe('exportSecrets', () => { expect(core.exportVariable).toBeCalledWith('KEY', '1'); expect(core.setOutput).toBeCalledWith('key', '1'); }); + + it('nested secret retrieval', async () => { + mockInput('test key.value'); + mockVaultData({ + key: { value: 1 } + }); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('KEY__VALUE', '1'); + expect(core.setOutput).toBeCalledWith('key__value', '1'); + }); }); diff --git a/src/entry.js b/src/entry.js new file mode 100644 index 00000000..44b0e047 --- /dev/null +++ b/src/entry.js @@ -0,0 +1,10 @@ +const core = require('@actions/core'); +const { exportSecrets } = require('./action'); + +(async () => { + try { + await core.group('Get Vault Secrets', exportSecrets); + } catch (error) { + core.setFailed(error.message); + } +})(); \ No newline at end of file diff --git a/src/index.js b/src/index.js index 44b0e047..d1d673b3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,7 @@ -const core = require('@actions/core'); -const { exportSecrets } = require('./action'); +const auth = require('./auth'); +const secrets = require('./secrets'); -(async () => { - try { - await core.group('Get Vault Secrets', exportSecrets); - } catch (error) { - core.setFailed(error.message); - } -})(); \ No newline at end of file +module.exports = { + auth, + secrets +}; \ No newline at end of file diff --git a/src/secrets.js b/src/secrets.js new file mode 100644 index 00000000..a259293d --- /dev/null +++ b/src/secrets.js @@ -0,0 +1,76 @@ +const jsonata = require("jsonata"); + + +/** + * @typedef {Object} SecretRequest + * @property {string} path + * @property {string} selector + */ + +/** + * @template {SecretRequest} TRequest + * @typedef {Object} SecretResponse + * @property {TRequest} request + * @property {string} value + * @property {boolean} cachedResponse + */ + + /** + * @template TRequest + * @param {Array} secretRequests + * @param {import('got').Got} client + * @return {Promise[]>} + */ +async function getSecrets(secretRequests, client) { + const responseCache = new Map(); + const results = []; + for (const secretRequest of secretRequests) { + const { path, selector } = secretRequest; + + const requestPath = `v1${path}`; + let body; + let cachedResponse = false; + if (responseCache.has(requestPath)) { + body = responseCache.get(requestPath); + cachedResponse = true; + } else { + const result = await client.get(requestPath); + body = result.body; + responseCache.set(requestPath, body); + } + + const value = selectData(JSON.parse(body), selector); + results.push({ + request: secretRequest, + value, + cachedResponse + }); + } + return results; +} + +/** + * Uses a Jsonata selector retrieve a bit of data from the result + * @param {object} data + * @param {string} selector + */ +function selectData(data, selector) { + const ata = jsonata(selector); + let result = JSON.stringify(ata.evaluate(data)); + // Compat for custom engines + if (!result && ata.ast().type === "path" && ata.ast()['steps'].length === 1 && selector !== 'data' && 'data' in data) { + result = JSON.stringify(jsonata(`data.${selector}`).evaluate(data)); + } else if (!result) { + throw Error(`Unable to retrieve result for ${selector}. No match data was found. Double check your Key or Selector.`); + } + + if (result.startsWith(`"`)) { + result = result.substring(1, result.length - 1); + } + return result; +} + +module.exports = { + getSecrets, + selectData +} \ No newline at end of file