feat: Add cleanAndMerge and tests

This commit is contained in:
Sidharth Vinod 2023-08-11 02:47:46 +05:30
parent cc70d37166
commit 396bda8d95
No known key found for this signature in database
GPG Key ID: FB5CCD378D3907CD
3 changed files with 85 additions and 43 deletions

View File

@ -20,7 +20,7 @@
* of src to dst in order. * of src to dst in order.
* @param {any} dst - The destination of the merge * @param {any} dst - The destination of the merge
* @param {any} src - The source object(s) to merge into destination * @param {any} src - The source object(s) to merge into destination
* @param {{ depth: number; clobber: boolean }} [config] - Depth: depth * @param {{ depth: number; clobber?: boolean }} [config] - Depth: depth
* to traverse within src and dst for merging - clobber: should dissimilar types clobber (default: * to traverse within src and dst for merging - clobber: should dissimilar types clobber (default:
* { depth: 2, clobber: false }). Default is `{ depth: 2, clobber: false }` * { depth: 2, clobber: false }). Default is `{ depth: 2, clobber: false }`
* @returns {any} * @returns {any}

View File

@ -1,5 +1,5 @@
import { vi } from 'vitest'; import { vi } from 'vitest';
import utils from './utils.js'; import utils, { cleanAndMerge } from './utils.js';
import assignWithDepth from './assignWithDepth.js'; import assignWithDepth from './assignWithDepth.js';
import { detectType } from './diagram-api/detectType.js'; import { detectType } from './diagram-api/detectType.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js';
@ -10,51 +10,51 @@ addDiagrams();
describe('when assignWithDepth: should merge objects within objects', function () { describe('when assignWithDepth: should merge objects within objects', function () {
it('should handle simple, depth:1 types (identity)', function () { it('should handle simple, depth:1 types (identity)', function () {
let config_0 = { foo: 'bar', bar: 0 }; const config_0 = { foo: 'bar', bar: 0 };
let config_1 = { foo: 'bar', bar: 0 }; const config_1 = { foo: 'bar', bar: 0 };
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1); expect(result).toEqual(config_1);
}); });
it('should handle simple, depth:1 types (dst: undefined)', function () { it('should handle simple, depth:1 types (dst: undefined)', function () {
let config_0 = undefined; const config_0 = undefined;
let config_1 = { foo: 'bar', bar: 0 }; const config_1 = { foo: 'bar', bar: 0 };
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1); expect(result).toEqual(config_1);
}); });
it('should handle simple, depth:1 types (src: undefined)', function () { it('should handle simple, depth:1 types (src: undefined)', function () {
let config_0 = { foo: 'bar', bar: 0 }; const config_0 = { foo: 'bar', bar: 0 };
let config_1 = undefined; const config_1 = undefined;
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_0); expect(result).toEqual(config_0);
}); });
it('should handle simple, depth:1 types (merge)', function () { it('should handle simple, depth:1 types (merge)', function () {
let config_0 = { foo: 'bar', bar: 0 }; const config_0 = { foo: 'bar', bar: 0 };
let config_1 = { foo: 'foo' }; const config_1 = { foo: 'foo' };
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual({ foo: 'foo', bar: 0 }); expect(result).toEqual({ foo: 'foo', bar: 0 });
}); });
it('should handle depth:2 types (dst: orphan)', function () { it('should handle depth:2 types (dst: orphan)', function () {
let config_0 = { foo: 'bar', bar: { foo: 'bar' } }; const config_0 = { foo: 'bar', bar: { foo: 'bar' } };
let config_1 = { foo: 'bar' }; const config_1 = { foo: 'bar' };
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_0); expect(result).toEqual(config_0);
}); });
it('should handle depth:2 types (dst: object, src: simple type)', function () { it('should handle depth:2 types (dst: object, src: simple type)', function () {
let config_0 = { foo: 'bar', bar: { foo: 'bar' } }; const config_0 = { foo: 'bar', bar: { foo: 'bar' } };
let config_1 = { foo: 'foo', bar: 'should NOT clobber' }; const config_1 = { foo: 'foo', bar: 'should NOT clobber' };
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar' } }); expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar' } });
}); });
it('should handle depth:2 types (src: orphan)', function () { it('should handle depth:2 types (src: orphan)', function () {
let config_0 = { foo: 'bar' }; const config_0 = { foo: 'bar' };
let config_1 = { foo: 'bar', bar: { foo: 'bar' } }; const config_1 = { foo: 'bar', bar: { foo: 'bar' } };
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1); expect(result).toEqual(config_1);
}); });
it('should handle depth:2 types (merge)', function () { it('should handle depth:2 types (merge)', function () {
let config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 }; const config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 };
let config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' }; const config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' };
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual({ expect(result).toEqual({
foo: 'foo', foo: 'foo',
bar: { foo: 'bar', bar: 0 }, bar: { foo: 'bar', bar: 0 },
@ -63,17 +63,17 @@ describe('when assignWithDepth: should merge objects within objects', function (
}); });
}); });
it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 2)', function () { it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 2)', function () {
let config_0 = { const config_0 = {
foo: 'bar', foo: 'bar',
bar: { foo: 'bar', bar: { foo: { message: 'this', willbe: 'clobbered' } } }, bar: { foo: 'bar', bar: { foo: { message: 'this', willbe: 'clobbered' } } },
boofar: 1, boofar: 1,
}; };
let config_1 = { const config_1 = {
foo: 'foo', foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } }, bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } },
foobar: 'foobar', foobar: 'foobar',
}; };
let result = assignWithDepth(config_0, config_1); const result = assignWithDepth(config_0, config_1);
expect(result).toEqual({ expect(result).toEqual({
foo: 'foo', foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } }, bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } },
@ -82,7 +82,7 @@ describe('when assignWithDepth: should merge objects within objects', function (
}); });
}); });
it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 1)', function () { it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 1)', function () {
let config_0 = { const config_0 = {
foo: 'bar', foo: 'bar',
bar: { bar: {
foo: 'bar', foo: 'bar',
@ -90,12 +90,12 @@ describe('when assignWithDepth: should merge objects within objects', function (
}, },
boofar: 1, boofar: 1,
}; };
let config_1 = { const config_1 = {
foo: 'foo', foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
foobar: 'foobar', foobar: 'foobar',
}; };
let result = assignWithDepth(config_0, config_1, { depth: 1 }); const result = assignWithDepth(config_0, config_1, { depth: 1 });
expect(result).toEqual({ expect(result).toEqual({
foo: 'foo', foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
@ -104,17 +104,17 @@ describe('when assignWithDepth: should merge objects within objects', function (
}); });
}); });
it('should handle depth:3 types (merge with no clobber because assignWithDepth::depth == 3)', function () { it('should handle depth:3 types (merge with no clobber because assignWithDepth::depth == 3)', function () {
let config_0 = { const config_0 = {
foo: 'bar', foo: 'bar',
bar: { foo: 'bar', bar: { foo: { message: '', willbe: 'present' } } }, bar: { foo: 'bar', bar: { foo: { message: '', willbe: 'present' } } },
boofar: 1, boofar: 1,
}; };
let config_1 = { const config_1 = {
foo: 'foo', foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
foobar: 'foobar', foobar: 'foobar',
}; };
let result = assignWithDepth(config_0, config_1, { depth: 3 }); const result = assignWithDepth(config_0, config_1, { depth: 3 });
expect(result).toEqual({ expect(result).toEqual({
foo: 'foo', foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } }, bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } },
@ -125,8 +125,8 @@ describe('when assignWithDepth: should merge objects within objects', function (
}); });
describe('when memoizing', function () { describe('when memoizing', function () {
it('should return the same value', function () { it('should return the same value', function () {
const fib = memoize( const fib: any = memoize(
function (n, x, canary) { function (n: number, x: string, canary: { flag: boolean }) {
canary.flag = true; canary.flag = true;
if (n < 2) { if (n < 2) {
return 1; return 1;
@ -260,7 +260,7 @@ describe('when formatting urls', function () {
it('should handle links', function () { it('should handle links', function () {
const url = 'https://mermaid-js.github.io/mermaid/#/'; const url = 'https://mermaid-js.github.io/mermaid/#/';
let config = { securityLevel: 'loose' }; const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config); let result = utils.formatUrl(url, config);
expect(result).toEqual(url); expect(result).toEqual(url);
@ -271,7 +271,7 @@ describe('when formatting urls', function () {
it('should handle anchors', function () { it('should handle anchors', function () {
const url = '#interaction'; const url = '#interaction';
let config = { securityLevel: 'loose' }; const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config); let result = utils.formatUrl(url, config);
expect(result).toEqual(url); expect(result).toEqual(url);
@ -282,7 +282,7 @@ describe('when formatting urls', function () {
it('should handle mailto', function () { it('should handle mailto', function () {
const url = 'mailto:user@user.user'; const url = 'mailto:user@user.user';
let config = { securityLevel: 'loose' }; const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config); let result = utils.formatUrl(url, config);
expect(result).toEqual(url); expect(result).toEqual(url);
@ -293,7 +293,7 @@ describe('when formatting urls', function () {
it('should handle other protocols', function () { it('should handle other protocols', function () {
const url = 'notes://do-your-thing/id'; const url = 'notes://do-your-thing/id';
let config = { securityLevel: 'loose' }; const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config); let result = utils.formatUrl(url, config);
expect(result).toEqual(url); expect(result).toEqual(url);
@ -304,7 +304,7 @@ describe('when formatting urls', function () {
it('should handle scripts', function () { it('should handle scripts', function () {
const url = 'javascript:alert("test")'; const url = 'javascript:alert("test")';
let config = { securityLevel: 'loose' }; const config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config); let result = utils.formatUrl(url, config);
expect(result).toEqual(url); expect(result).toEqual(url);
@ -425,6 +425,42 @@ describe('when parsing font sizes', function () {
}); });
it('handles unparseable input', function () { it('handles unparseable input', function () {
// @ts-expect-error Explicitly testing unparsable input
expect(utils.parseFontSize({ fontSize: 14 })).toEqual([undefined, undefined]); expect(utils.parseFontSize({ fontSize: 14 })).toEqual([undefined, undefined]);
}); });
}); });
describe('cleanAndMerge', () => {
test('should merge objects', () => {
expect(cleanAndMerge({ a: 1, b: 2 }, { b: 3 })).toEqual({ a: 1, b: 3 });
expect(cleanAndMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 });
});
test('should remove undefined values', () => {
expect(cleanAndMerge({ a: 1, b: 2 }, { b: undefined })).toEqual({ a: 1, b: 2 });
expect(cleanAndMerge({ a: 1, b: 2 }, { a: 2, b: undefined })).toEqual({ a: 2, b: 2 });
expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: undefined })).toEqual({
a: 2,
b: { c: 2 },
});
// @ts-expect-error Explicitly testing different type
expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: { c: undefined } })).toEqual({
a: 2,
b: { c: 2 },
});
});
test('should create deep copies of object', () => {
const input: { a: number; b?: number } = { a: 1 };
const output = cleanAndMerge(input, { b: 2 });
expect(output).toEqual({ a: 1, b: 2 });
output.b = 3;
expect(input).toEqual({ a: 1 });
const inputDeep = { a: { b: 1 } };
const outputDeep = cleanAndMerge(inputDeep, { a: { b: 2 } });
expect(outputDeep).toEqual({ a: { b: 2 } });
outputDeep.a.b = 3;
expect(inputDeep).toEqual({ a: { b: 1 } });
});
});

View File

@ -31,6 +31,7 @@ import { detectType } from './diagram-api/detectType.js';
import assignWithDepth from './assignWithDepth.js'; import assignWithDepth from './assignWithDepth.js';
import { MermaidConfig } from './config.type.js'; import { MermaidConfig } from './config.type.js';
import memoize from 'lodash-es/memoize.js'; import memoize from 'lodash-es/memoize.js';
import merge from 'lodash-es/merge.js';
export const ZERO_WIDTH_SPACE = '\u200b'; export const ZERO_WIDTH_SPACE = '\u200b';
@ -802,7 +803,7 @@ export const calculateTextDimensions: (
); );
export const initIdGenerator = class iterator { export const initIdGenerator = class iterator {
constructor(deterministic, seed) { constructor(deterministic, seed?: any) {
this.deterministic = deterministic; this.deterministic = deterministic;
// TODO: Seed is only used for length? // TODO: Seed is only used for length?
this.seed = seed; this.seed = seed;
@ -994,12 +995,17 @@ export const parseFontSize = (fontSize: string | number | undefined): [number?,
} }
}; };
export function cleanAndMerge<T>(defaultData: T, data?: Partial<T>): T {
return merge({}, defaultData, data);
}
export default { export default {
assignWithDepth, assignWithDepth,
wrapLabel, wrapLabel,
calculateTextHeight, calculateTextHeight,
calculateTextWidth, calculateTextWidth,
calculateTextDimensions, calculateTextDimensions,
cleanAndMerge,
detectInit, detectInit,
detectDirective, detectDirective,
isSubstringInArray, isSubstringInArray,