From 9ad265e74f02bdeca00e3016adcd926842561b4c Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Sat, 27 Jan 2024 14:07:49 +0530 Subject: [PATCH] feat(#1460): string interpolation --- package-lock.json | 54 ++++++++++++ package.json | 7 +- packages/bruno-common/.gitignore | 22 +++++ packages/bruno-common/jest.config.js | 5 ++ packages/bruno-common/license.md | 21 +++++ packages/bruno-common/package.json | 33 ++++++++ packages/bruno-common/readme.md | 9 ++ packages/bruno-common/rollup.config.js | 40 +++++++++ packages/bruno-common/src/index.ts | 5 ++ .../src/interpolate/String Interpolation.md | 16 ++++ .../src/interpolate/index.spec.ts | 84 +++++++++++++++++++ .../bruno-common/src/interpolate/index.ts | 31 +++++++ packages/bruno-common/src/utils/index.spec.ts | 39 +++++++++ packages/bruno-common/src/utils/index.ts | 11 +++ packages/bruno-common/tsconfig.json | 19 +++++ 15 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-common/.gitignore create mode 100644 packages/bruno-common/jest.config.js create mode 100644 packages/bruno-common/license.md create mode 100644 packages/bruno-common/package.json create mode 100644 packages/bruno-common/readme.md create mode 100644 packages/bruno-common/rollup.config.js create mode 100644 packages/bruno-common/src/index.ts create mode 100644 packages/bruno-common/src/interpolate/String Interpolation.md create mode 100644 packages/bruno-common/src/interpolate/index.spec.ts create mode 100644 packages/bruno-common/src/interpolate/index.ts create mode 100644 packages/bruno-common/src/utils/index.spec.ts create mode 100644 packages/bruno-common/src/utils/index.ts create mode 100644 packages/bruno-common/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 56100f3f..d3ed0bc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "packages/bruno-app", "packages/bruno-electron", "packages/bruno-cli", + "packages/bruno-common", "packages/bruno-schema", "packages/bruno-query", "packages/bruno-js", @@ -21,6 +22,7 @@ "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", "@playwright/test": "^1.27.1", + "@types/jest": "^29.5.11", "fs-extra": "^11.1.1", "husky": "^8.0.3", "jest": "^29.2.0", @@ -5240,6 +5242,16 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "dev": true, @@ -5381,6 +5393,10 @@ "resolved": "packages/bruno-cli", "link": true }, + "node_modules/@usebruno/common": { + "resolved": "packages/bruno-common", + "link": true + }, "node_modules/@usebruno/graphql-docs": { "resolved": "packages/bruno-graphql-docs", "link": true @@ -18144,6 +18160,21 @@ "node": ">= 14" } }, + "packages/bruno-common": { + "name": "@usebruno/common", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^9.0.2", + "rollup": "3.2.5", + "rollup-plugin-dts": "^5.0.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-terser": "^7.0.2", + "typescript": "^4.8.4" + } + }, "packages/bruno-electron": { "name": "bruno", "version": "v1.6.1", @@ -22287,6 +22318,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "@types/json-schema": { "version": "7.0.11", "dev": true @@ -22628,6 +22669,19 @@ } } }, + "@usebruno/common": { + "version": "file:packages/bruno-common", + "requires": { + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^9.0.2", + "rollup": "3.2.5", + "rollup-plugin-dts": "^5.0.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-terser": "^7.0.2", + "typescript": "^4.8.4" + } + }, "@usebruno/graphql-docs": { "version": "file:packages/bruno-graphql-docs", "requires": { diff --git a/package.json b/package.json index 7ba991b5..a623adc6 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "packages/bruno-app", "packages/bruno-electron", "packages/bruno-cli", + "packages/bruno-common", "packages/bruno-schema", "packages/bruno-query", "packages/bruno-js", @@ -18,18 +19,20 @@ "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", "@playwright/test": "^1.27.1", + "@types/jest": "^29.5.11", + "fs-extra": "^11.1.1", "husky": "^8.0.3", "jest": "^29.2.0", "pretty-quick": "^3.1.3", "randomstring": "^1.2.2", - "ts-jest": "^29.0.5", - "fs-extra": "^11.1.1" + "ts-jest": "^29.0.5" }, "scripts": { "dev:web": "npm run dev --workspace=packages/bruno-app", "build:web": "npm run build --workspace=packages/bruno-app", "prettier:web": "npm run prettier --workspace=packages/bruno-app", "dev:electron": "npm run dev --workspace=packages/bruno-electron", + "build:bruno-common": "npm run build --workspace=packages/bruno-common", "build:bruno-query": "npm run build --workspace=packages/bruno-query", "build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs", "build:electron": "node ./scripts/build-electron.js", diff --git a/packages/bruno-common/.gitignore b/packages/bruno-common/.gitignore new file mode 100644 index 00000000..f6eabff3 --- /dev/null +++ b/packages/bruno-common/.gitignore @@ -0,0 +1,22 @@ +# dependencies +node_modules +yarn.lock +pnpm-lock.yaml +package-lock.json +.pnp +.pnp.js + +# testing +coverage + +# production +dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/packages/bruno-common/jest.config.js b/packages/bruno-common/jest.config.js new file mode 100644 index 00000000..a58c252f --- /dev/null +++ b/packages/bruno-common/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node' +}; diff --git a/packages/bruno-common/license.md b/packages/bruno-common/license.md new file mode 100644 index 00000000..95ff90b1 --- /dev/null +++ b/packages/bruno-common/license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Anoop M D, Anusree P S and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/bruno-common/package.json b/packages/bruno-common/package.json new file mode 100644 index 00000000..d8e59842 --- /dev/null +++ b/packages/bruno-common/package.json @@ -0,0 +1,33 @@ +{ + "name": "@usebruno/common", + "version": "0.1.0", + "license": "MIT", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "src", + "package.json" + ], + "scripts": { + "clean": "rimraf dist", + "test": "jest", + "prebuild": "npm run clean", + "build": "rollup -c", + "prepack": "npm run test && npm run build" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^9.0.2", + "rollup": "3.2.5", + "rollup-plugin-dts": "^5.0.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-terser": "^7.0.2", + "typescript": "^4.8.4" + }, + "overrides": { + "rollup": "3.2.5" + } +} diff --git a/packages/bruno-common/readme.md b/packages/bruno-common/readme.md new file mode 100644 index 00000000..dd7caf77 --- /dev/null +++ b/packages/bruno-common/readme.md @@ -0,0 +1,9 @@ +# bruno-common + +A collection of common utilities used across Bruno App, Electron and CLI packages. + +### Publish to Npm Registry + +```bash +npm publish --access=public +``` diff --git a/packages/bruno-common/rollup.config.js b/packages/bruno-common/rollup.config.js new file mode 100644 index 00000000..51aedecb --- /dev/null +++ b/packages/bruno-common/rollup.config.js @@ -0,0 +1,40 @@ +const { nodeResolve } = require('@rollup/plugin-node-resolve'); +const commonjs = require('@rollup/plugin-commonjs'); +const typescript = require('@rollup/plugin-typescript'); +const dts = require('rollup-plugin-dts'); +const { terser } = require('rollup-plugin-terser'); +const peerDepsExternal = require('rollup-plugin-peer-deps-external'); + +const packageJson = require('./package.json'); + +module.exports = [ + { + input: 'src/index.ts', + output: [ + { + file: packageJson.main, + format: 'cjs', + sourcemap: true + }, + { + file: packageJson.module, + format: 'esm', + sourcemap: true + } + ], + plugins: [ + peerDepsExternal(), + nodeResolve({ + extensions: ['.css'] + }), + commonjs(), + typescript({ tsconfig: './tsconfig.json' }), + terser() + ] + }, + { + input: 'dist/esm/index.d.ts', + output: [{ file: 'dist/index.d.ts', format: 'esm' }], + plugins: [dts.default()] + } +]; diff --git a/packages/bruno-common/src/index.ts b/packages/bruno-common/src/index.ts new file mode 100644 index 00000000..04a709c5 --- /dev/null +++ b/packages/bruno-common/src/index.ts @@ -0,0 +1,5 @@ +import interpolate from './interpolate'; + +export default { + interpolate +}; diff --git a/packages/bruno-common/src/interpolate/String Interpolation.md b/packages/bruno-common/src/interpolate/String Interpolation.md new file mode 100644 index 00000000..6ff510e6 --- /dev/null +++ b/packages/bruno-common/src/interpolate/String Interpolation.md @@ -0,0 +1,16 @@ +String Interpolation + +### Goal + +Today our interlation logic is duplicated across multiple packages. +The goal is to centralize a single source of truth for all interpolation logic. + +### Considerations + +- We want to be flexible in terms of key naming conventions. +- We plan to support Nested environments in the future. + +### Moving away from handlebars + +I think its time to move away from handlebars. +We don't need the full power of handlebars and write a custom interpolation function that meets our needs. diff --git a/packages/bruno-common/src/interpolate/index.spec.ts b/packages/bruno-common/src/interpolate/index.spec.ts new file mode 100644 index 00000000..ca2384ba --- /dev/null +++ b/packages/bruno-common/src/interpolate/index.spec.ts @@ -0,0 +1,84 @@ +import interpolate from './index'; + +describe('interpolate', () => { + it('should replace placeholders with values from the object', () => { + const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old'; + const inputObject = { + 'user.name': 'Bruno', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Bruno and I am 4 years old'); + }); + + it('should handle missing values by leaving the placeholders unchanged using {{}} as delimiters', () => { + const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old'; + const inputObject = { + user: { + name: 'Bruno' + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Bruno and I am {{user.age}} years old'); + }); + + it('should handle all valid keys', () => { + const inputObject = { + user: { + full_name: 'Bruno', + age: 4, + 'fav-food': ['egg', 'meat'], + 'want.attention': true + } + }; + const inputStr = ` + Hi, I am {{user.full_name}}, + I am {{user.age}} years old. + My favorite food is {{user.fav-food[0]}} and {{user.fav-food[1]}}. + I like attention: {{user.want.attention}} +`; + const expectedStr = ` + Hi, I am Bruno, + I am 4 years old. + My favorite food is egg and meat. + I like attention: true +`; + const result = interpolate(inputStr, inputObject); + expect(result).toBe(expectedStr); + }); + + it('should strictly match the keys (whitespace matters)', () => { + const inputString = 'Hello, my name is {{ user.name }} and I am {{user.age}} years old'; + const inputObject = { + 'user.name': 'Bruno', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is {{ user.name }} and I am 4 years old'); + }); + + it('should give precedence to the last key in case of duplicates', () => { + const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old'; + const inputObject = { + 'user.name': 'Bruno', + user: { + name: 'Not Bruno', + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Not Bruno and I am 4 years old'); + }); +}); diff --git a/packages/bruno-common/src/interpolate/index.ts b/packages/bruno-common/src/interpolate/index.ts new file mode 100644 index 00000000..8ad86c5b --- /dev/null +++ b/packages/bruno-common/src/interpolate/index.ts @@ -0,0 +1,31 @@ +/** + * The interpolation function expects a string with placeholders and an object with the values to replace the placeholders. + * The keys passed can have dot notation too. + * + * Ex: interpolate('Hello, my name is ${user.name} and I am ${user.age} years old', { + * "user.name": "Bruno", + * "user": { + * "age": 4 + * } + * }); + * Output: Hello, my name is Bruno and I am 4 years old + */ + +import { flattenObject } from '../utils'; + +const interpolate = (str: string, obj: Record): string => { + if (!str || typeof str !== 'string' || !obj || typeof obj !== 'object') { + return str; + } + + const patternRegex = /\{\{([^}]+)\}\}/g; + const flattenedObj = flattenObject(obj); + const result = str.replace(patternRegex, (match, placeholder) => { + const replacement = flattenedObj[placeholder]; + return replacement !== undefined ? replacement : match; + }); + + return result; +}; + +export default interpolate; diff --git a/packages/bruno-common/src/utils/index.spec.ts b/packages/bruno-common/src/utils/index.spec.ts new file mode 100644 index 00000000..ced32532 --- /dev/null +++ b/packages/bruno-common/src/utils/index.spec.ts @@ -0,0 +1,39 @@ +import { flattenObject } from './index'; + +describe('flattenObject', () => { + it('should flatten a simple object', () => { + const input = { a: 1, b: { c: 2, d: { e: 3 } } }; + const output = flattenObject(input); + expect(output).toEqual({ a: 1, 'b.c': 2, 'b.d.e': 3 }); + }); + + it('should flatten an object with arrays', () => { + const input = { a: 1, b: { c: [2, 3, 4], d: { e: 5 } } }; + const output = flattenObject(input); + expect(output).toEqual({ a: 1, 'b.c[0]': 2, 'b.c[1]': 3, 'b.c[2]': 4, 'b.d.e': 5 }); + }); + + it('should flatten an object with arrays having objects', () => { + const input = { a: 1, b: { c: [{ d: 2 }, { e: 3 }], f: { g: 4 } } }; + const output = flattenObject(input); + expect(output).toEqual({ a: 1, 'b.c[0].d': 2, 'b.c[1].e': 3, 'b.f.g': 4 }); + }); + + it('should handle null values', () => { + const input = { a: 1, b: { c: null, d: { e: 3 } } }; + const output = flattenObject(input); + expect(output).toEqual({ a: 1, 'b.c': null, 'b.d.e': 3 }); + }); + + it('should handle an empty object', () => { + const input = {}; + const output = flattenObject(input); + expect(output).toEqual({}); + }); + + it('should handle an object with nested empty objects', () => { + const input = { a: { b: {}, c: { d: {} } } }; + const output = flattenObject(input); + expect(output).toEqual({}); + }); +}); diff --git a/packages/bruno-common/src/utils/index.ts b/packages/bruno-common/src/utils/index.ts new file mode 100644 index 00000000..bba8f131 --- /dev/null +++ b/packages/bruno-common/src/utils/index.ts @@ -0,0 +1,11 @@ +export const flattenObject = (obj: Record, parentKey: string = ''): Record => { + return Object.entries(obj).reduce((acc: Record, [key, value]: [string, any]) => { + const newKey = parentKey ? (Array.isArray(obj) ? `${parentKey}[${key}]` : `${parentKey}.${key}`) : key; + if (typeof value === 'object' && value !== null) { + Object.assign(acc, flattenObject(value, newKey)); + } else { + acc[newKey] = value; + } + return acc; + }, {}); +}; diff --git a/packages/bruno-common/tsconfig.json b/packages/bruno-common/tsconfig.json new file mode 100644 index 00000000..57a8bcc7 --- /dev/null +++ b/packages/bruno-common/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES6", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "jsx": "react", + "module": "ESNext", + "declaration": true, + "declarationDir": "types", + "sourceMap": true, + "outDir": "dist", + "moduleResolution": "node", + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["dist", "node_modules", "tests"] +}