mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-25 01:14:23 +01:00
feat(#1460): string interpolation
This commit is contained in:
parent
aed1b41da6
commit
9ad265e74f
54
package-lock.json
generated
54
package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
22
packages/bruno-common/.gitignore
vendored
Normal file
22
packages/bruno-common/.gitignore
vendored
Normal file
@ -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*
|
5
packages/bruno-common/jest.config.js
Normal file
5
packages/bruno-common/jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node'
|
||||
};
|
21
packages/bruno-common/license.md
Normal file
21
packages/bruno-common/license.md
Normal file
@ -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.
|
33
packages/bruno-common/package.json
Normal file
33
packages/bruno-common/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
9
packages/bruno-common/readme.md
Normal file
9
packages/bruno-common/readme.md
Normal file
@ -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
|
||||
```
|
40
packages/bruno-common/rollup.config.js
Normal file
40
packages/bruno-common/rollup.config.js
Normal file
@ -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()]
|
||||
}
|
||||
];
|
5
packages/bruno-common/src/index.ts
Normal file
5
packages/bruno-common/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import interpolate from './interpolate';
|
||||
|
||||
export default {
|
||||
interpolate
|
||||
};
|
@ -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.
|
84
packages/bruno-common/src/interpolate/index.spec.ts
Normal file
84
packages/bruno-common/src/interpolate/index.spec.ts
Normal file
@ -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');
|
||||
});
|
||||
});
|
31
packages/bruno-common/src/interpolate/index.ts
Normal file
31
packages/bruno-common/src/interpolate/index.ts
Normal file
@ -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, any>): 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;
|
39
packages/bruno-common/src/utils/index.spec.ts
Normal file
39
packages/bruno-common/src/utils/index.spec.ts
Normal file
@ -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({});
|
||||
});
|
||||
});
|
11
packages/bruno-common/src/utils/index.ts
Normal file
11
packages/bruno-common/src/utils/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const flattenObject = (obj: Record<string, any>, parentKey: string = ''): Record<string, any> => {
|
||||
return Object.entries(obj).reduce((acc: Record<string, any>, [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;
|
||||
}, {});
|
||||
};
|
19
packages/bruno-common/tsconfig.json
Normal file
19
packages/bruno-common/tsconfig.json
Normal file
@ -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"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user