diff --git a/packages/bruno-js/src/get.js b/packages/bruno-js/src/get.js new file mode 100644 index 000000000..4a1e490f0 --- /dev/null +++ b/packages/bruno-js/src/get.js @@ -0,0 +1,128 @@ +/** + * Gets property values for all items in source array + */ +const arrayGet = (source, prop) => { + if (!Array.isArray(source)) return []; + + const results = []; + + source.forEach(item => { + const value = item[prop]; + if (value != null) { + results.push(...Array.isArray(value) ? value : [value]); + } + }); + + return results; +}; + +/** + * Recursively collects property values into results + */ +const deepGet = (source, prop, results) => { + if (source == null || typeof source !== 'object') return; + + if (Array.isArray(source)) { + source.forEach(item => deepGet(item, prop, results)); + } else { + for (const key in source) { + if (key === prop) { + const value = source[prop]; + results.push(...Array.isArray(value) ? value : [value]); + } else { + deepGet(source[key], prop, results); + } + } + } +}; + +/** + * Gets property value(s) from source + */ +const baseGet = (source, prop, deep = false) => { + if (source == null || typeof source !== 'object') return; + + if (!deep) { + return Array.isArray(source) ? arrayGet(source, prop) : source[prop]; + } else { + const results = []; + deepGet(source, prop, results); + return results.filter(value => value != null); + } +}; + +/** + * Apply filter on source array or object + */ +const applyFilter = (source, predicate, single = false) => { + const list = Array.isArray(source) ? source : [source]; + const result = list.filter(predicate); + return single ? result[0] : result; +}; + +/** + * Supercharged getter with deep navigation and filter support + * + * 1. Easy array navigation + * ```js + * get(data, 'customer.orders.items.amount') + * ``` + * 2. Deep navigation .. double dots + * ```js + * get(data, '..items.amount') + * ``` + * 3. Array indexing + * ```js + * get(data, '..items[0].amount') + * ``` + * 4. Array filtering [?] with corresponding js filter + * ```js + * get(data, '..items[?].amount', i => i.amount > 20) + * ``` + */ +function get(source, path, ...filters) { + const paths = path + .replace(/\s+/g, '') + .split(/(\.{1,2}|\[\?\]|\[\d+\])/g) // ["..", "items", "[?]", ".", "amount", "[0]" ] + .filter(s => s.length > 0) + .map(str => { + str = str.replace(/\[|\]/g, ''); + const index = parseInt(str); + return isNaN(index) ? str : index; + }); + + let index = 0, lookbehind = '', filterIndex = 0; + + while (source != null && index < paths.length) { + const token = paths[index++]; + + switch (true) { + case token === "..": + case token === ".": + break; + case token === "?": + const filter = filters[filterIndex++]; + if (filter == null) + throw new Error(`missing filter for ${lookbehind}`); + const single = !Array.isArray(source); + source = applyFilter(source, filter, single); + break; + case typeof token === 'number': + source = source[token]; + break; + default: + source = baseGet(source, token, lookbehind === ".."); + if (Array.isArray(source) && !source.length) { + source = undefined; + } + } + + lookbehind = token; + } + + return source; +} + +module.exports = { + get +}; \ No newline at end of file diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 6bc73da83..151bb6014 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -1,4 +1,5 @@ const jsonQuery = require('json-query'); +const { get } = require("./get"); const JS_KEYWORDS = ` break case catch class const continue debugger default delete do @@ -70,6 +71,27 @@ const createResponseParser = (response = {}) => { res.headers = response.headers; res.body = response.data; + /** + * Get supports deep object navigation and filtering + * 1. Easy array navigation + * ```js + * res.get('customer.orders.items.amount') + * ``` + * 2. Deep navigation .. double dots + * ```js + * res.get('..items.amount') + * ``` + * 3. Array indexing + * ```js + * res.get('..items[0].amount') + * ``` + * 4. Array filtering [?] with corresponding js filter + * ```js + * res.get('..items[?].amount', i => i.amount > 20) + * ``` + */ + res.get = (path, ...filters) => get(response.data, path, ...filters); + return res; }; diff --git a/packages/bruno-js/tests/get.spec.js b/packages/bruno-js/tests/get.spec.js new file mode 100644 index 000000000..b5716ccd8 --- /dev/null +++ b/packages/bruno-js/tests/get.spec.js @@ -0,0 +1,46 @@ +const { filter } = require("lodash"); +const { get } = require("../src/get"); + +const data = { + customer: { + address: { + city: "bangalore" + }, + orders: [ + { + id: "order-1", + items: [ + { id: 1, amount: 10 }, + { id: 2, amount: 20 }, + ] + }, + { + id: "order-2", + items: [ + { id: 3, amount: 30, }, + { id: 4, amount: 40 } + ] + } + ] + }, +}; + +describe("get", () => { + it.each([ + ["customer.address.city", "bangalore"], + ["customer.orders.items.amount", [10, 20, 30, 40]], + ["customer.orders.items.amount[0]", 10], + ["..items.amount", [10, 20, 30, 40]], + ["..amount", [10, 20, 30, 40]], + ["..items.amount[0]", 10], + ["..items[0].amount", 10], + ["..items[?].amount", [40], (i) => i.amount > 30], // [?] filter + ["..id", ["order-1", 1, 2, "order-2", 3, 4]], // all ids + ["customer.orders.foo", undefined], + ["..customer.foo", undefined], + ["..address", [{ city: "bangalore" }]], // .. will return array + ["..address[0]", { city: "bangalore" }] + ])("%s should be %j %s", (expr, result, filter = undefined) => { + expect(get(data, expr, filter)).toEqual(result); + }); +});