diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index fa730923..777a9175 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -1,5 +1,5 @@ import { ThemeInterface } from '../theme'; -import { isNumeric } from '../utils/helpers'; +import { isNumeric, mergeObjects } from '../utils/helpers'; import defaultTheme from '../theme'; export interface RedocRawOptions { @@ -33,7 +33,7 @@ export class RedocNormalizedOptions { untrustedSpec: boolean; constructor(raw: RedocRawOptions) { - this.theme = { ...(raw.theme || {}), ...defaultTheme }; // todo: merge deep + this.theme = mergeObjects({} as any, raw.theme || {}, defaultTheme); this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset); this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname); this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses); diff --git a/src/utils/__tests__/helpers.test.ts b/src/utils/__tests__/helpers.test.ts index 8a18b84a..17428ca5 100644 --- a/src/utils/__tests__/helpers.test.ts +++ b/src/utils/__tests__/helpers.test.ts @@ -1,4 +1,4 @@ -import { mapWithLast, appendToMdHeading } from '../helpers'; +import { mapWithLast, appendToMdHeading, mergeObjects } from '../helpers'; describe('Utils', () => { describe('helpers', () => { @@ -38,5 +38,24 @@ describe('Utils', () => { const val = appendToMdHeading('', 'Authentication', ''); expect(val).toEqual('# Authentication\n\n'); }); + + describe('mergeObjects', () => { + test('should merge Objects and all nested Ones', () => { + const obj1 = { a: { a1: 'A1' }, c: 'C', d: {} }; + const obj2 = { a: { a2: 'A2' }, b: { b1: 'B1' }, d: null }; + const obj3 = { a: { a1: 'A1', a2: 'A2' }, b: { b1: 'B1' }, c: 'C', d: null }; + expect(mergeObjects({}, obj1, obj2)).toEqual(obj3); + }); + test('should behave like Object.assign on the top level', () => { + const obj1 = { a: { a1: 'A1' }, c: 'C' }; + const obj2 = { a: undefined, b: { b1: 'B1' } }; + expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2)); + }); + test('should not merge array values, just override', () => { + const obj1 = { a: ['A', 'B'] }; + const obj2 = { a: ['C'], b: ['D'] }; + expect(mergeObjects({}, obj1, obj2)).toEqual({ a: ['C'], b: ['D'] }); + }); + }); }); }); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 8452669f..7b4407a2 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -80,3 +80,37 @@ export function appendToMdHeading(md: string, heading: string, content: string) return `${md}${br}# ${heading}\n\n${content}`; } } + +// credits https://stackoverflow.com/a/46973278/1749888 +export const mergeObjects = (target: T, ...sources: T[]): T => { + if (!sources.length) { + return target; + } + const source = sources.shift(); + if (source === undefined) { + return target; + } + + if (isMergebleObject(target) && isMergebleObject(source)) { + Object.keys(source).forEach(function(key: string) { + if (isMergebleObject(source[key])) { + if (!target[key]) { + target[key] = {}; + } + mergeObjects(target[key], source[key]); + } else { + target[key] = source[key]; + } + }); + } + + return mergeObjects(target, ...sources); +}; + +const isObject = (item: any): boolean => { + return item !== null && typeof item === 'object'; +}; + +const isMergebleObject = (item): boolean => { + return isObject(item) && !Array.isArray(item); +};