diff --git a/packages/concerto-analysis/src/compare-config.ts b/packages/concerto-analysis/src/compare-config.ts index cb36687e6..75c725e91 100644 --- a/packages/concerto-analysis/src/compare-config.ts +++ b/packages/concerto-analysis/src/compare-config.ts @@ -16,11 +16,11 @@ import { ComparerFactory } from './comparer'; import { comparerFactories } from './comparers'; export enum CompareResult { - NONE, - PATCH, - MINOR, - MAJOR, - ERROR, + NONE, + PATCH, + MINOR, + MAJOR, + ERROR, } export function compareResultToString(result: CompareResult) { @@ -39,9 +39,9 @@ export function compareResultToString(result: CompareResult) { } export type CompareConfig = { - comparerFactories: ComparerFactory[]; - rules: Record; -} + comparerFactories: ComparerFactory[]; + rules: Record; +}; export const defaultCompareConfig: CompareConfig = { comparerFactories, @@ -63,8 +63,111 @@ export const defaultCompareConfig: CompareConfig = { 'map-key-type-changed': CompareResult.MAJOR, 'map-value-type-changed': CompareResult.MAJOR, 'scalar-extends-changed': CompareResult.MAJOR, - 'scalar-validator-added' : CompareResult.MAJOR, - 'scalar-validator-removed' : CompareResult.PATCH, - 'scalar-validator-changed' : CompareResult.MAJOR, - } + 'scalar-validator-added': CompareResult.MAJOR, + 'scalar-validator-removed': CompareResult.PATCH, + 'scalar-validator-changed': CompareResult.MAJOR, + }, }; + +const EmptyConfig: CompareConfig = { + comparerFactories: [], + rules: {}, +}; + +export class CompareConfigBuilder { + /** + * A utility to build {@link CompareConfig} to be used in {@link Compare} class. + * A new compare config can be edited with provided functions and finally + * resulting config can be used by calling `build`. + * + * By default, it starts with an empty configuration. + */ + + private _config: CompareConfig = EmptyConfig; + + /** + * Final step of the builder + * + * @returns {CompareConfig} Resulting CompareConfig object. + */ + public build(): CompareConfig { + return { + comparerFactories: [...this._config.comparerFactories], + rules: { ...this._config.rules }, + }; + } + + /** + * Adds default comparer configuration onto the configuration + * being built. + * + * @returns {CompareConfigBuilder} A reference to the builder object to chain + */ + public default(): CompareConfigBuilder { + this._config = { + comparerFactories: [...this._config.comparerFactories, ...defaultCompareConfig.comparerFactories], + rules: { ...this._config.rules, ...defaultCompareConfig.rules }, + }; + + return this; + } + + /** + * Extends existing configuration that's built upto this point + * with the provided config. + * + * @param {CompareConfig} config - The configuration to extend with + * @returns {CompareConfigBuilder} A reference to the builder object to chain + */ + public extend(config: CompareConfig): CompareConfigBuilder { + this._config = { + comparerFactories: [...this._config.comparerFactories, ...config.comparerFactories], + rules: { ...this._config.rules, ...config.rules }, + }; + + return this; + } + + /** + * Adds a comparison outcome rule to the configuration + * + * @param {string} ruleKey - A key that is referenced from one of the comparer factories + * @param {CompareResult} result - A version diff outcome based on this rule + * @returns {CompareConfigBuilder} A reference to the builder object to chain + */ + public addRule(ruleKey: string, result: CompareResult): CompareConfigBuilder { + this._config.rules[ruleKey] = result; + + return this; + } + + /** + * Removes a comparison outcome rule from the configuration + * + * @param {string} ruleKey - A key that is referenced from one of the comparer factories + * @returns {CompareConfigBuilder} A reference to the builder object to chain + * @throws {ReferenceError} + * Thrown if the `ruleKey` does not exist in the configuration + */ + public removeRule(ruleKey: string): CompareConfigBuilder { + if (!this._config.rules[ruleKey]) { + throw new ReferenceError(`ruleKey '${ruleKey}' does not exist`); + } + + delete this._config.rules[ruleKey]; + + return this; + } + + /** + * Add a {@link ComparerFactory} to the configuration. + * + * @param {ComparerFactory} f - A {@link ComparerFactory} that should reference the rules in the configuration + * @returns {CompareConfigBuilder} A reference to the builder object to chain + */ + public addComparerFactory(f: ComparerFactory): CompareConfigBuilder { + this._config.comparerFactories = [...this._config.comparerFactories, f]; + + return this; + } +} diff --git a/packages/concerto-analysis/src/index.ts b/packages/concerto-analysis/src/index.ts index 9555d6d14..2f8bf3324 100644 --- a/packages/concerto-analysis/src/index.ts +++ b/packages/concerto-analysis/src/index.ts @@ -13,6 +13,6 @@ */ export { Compare } from './compare'; -export { CompareConfig, CompareResult, compareResultToString } from './compare-config'; +export { CompareConfig, CompareResult, CompareConfigBuilder, compareResultToString } from './compare-config'; export { CompareFinding, CompareResults } from './compare-results'; diff --git a/packages/concerto-analysis/test/unit/compare-config.test.ts b/packages/concerto-analysis/test/unit/compare-config.test.ts new file mode 100644 index 000000000..d37fcba63 --- /dev/null +++ b/packages/concerto-analysis/test/unit/compare-config.test.ts @@ -0,0 +1,77 @@ +import { CompareConfig, CompareConfigBuilder, CompareResult } from '../../src/compare-config'; + +describe('CompareConfigBuilder', () => { + it('Should start with empty config', () => { + const builder = new CompareConfigBuilder(); + + const actual = builder.build(); + + expect(actual.comparerFactories.length).toEqual(0); + expect(Object.keys(actual.rules).length).toEqual(0); + }); + + it('Should add default config with `default`', () => { + const builder = new CompareConfigBuilder(); + + const actual = builder.default().build(); + + expect(actual.comparerFactories.length).toEqual(11); + expect(Object.keys(actual.rules).length).toEqual(20); + expect(actual.rules['class-declaration-added']).toEqual(CompareResult.MINOR); + expect(actual.rules['optional-property-added']).toEqual(CompareResult.PATCH); + expect(actual.rules['map-value-type-changed']).toEqual(CompareResult.MAJOR); + }); + + it('Should extend config', () => { + const newRules = { + 'a-new-rule': CompareResult.MAJOR + }; + const toExtend: CompareConfig = { + comparerFactories: [ + () => ({}), + ], + rules: newRules + }; + const builder = new CompareConfigBuilder(); + + const actual = builder.default().extend(toExtend).build(); + + expect(actual.comparerFactories.length).toEqual(12); + expect(Object.keys(actual.rules).length).toEqual(21); + expect(actual.rules['a-new-rule']).toEqual(CompareResult.MAJOR); + }); + + it('Should add a new comparer factory', () => { + const builder = new CompareConfigBuilder(); + + const actual = builder.default().addComparerFactory(() => ({})).build(); + + expect(actual.comparerFactories.length).toEqual(12); + expect(Object.keys(actual.rules).length).toEqual(20); + }); + + it('Should add a new rule', () => { + const builder = new CompareConfigBuilder(); + + const actual = builder.default().addRule('a-new-rule', CompareResult.MAJOR).build(); + + expect(actual.comparerFactories.length).toEqual(11); + expect(Object.keys(actual.rules).length).toEqual(21); + expect(actual.rules['a-new-rule']).toEqual(CompareResult.MAJOR); + }); + + it('Should remove an existing rule', () => { + const builder = new CompareConfigBuilder(); + + const actual = builder.default().removeRule('optional-property-added').build(); + + expect(actual.comparerFactories.length).toEqual(11); + expect(Object.keys(actual.rules).length).toEqual(19); + expect(actual.rules['optional-property-added']).toBeFalsy(); + }); + + it('Should throw while removing a rule that does not exist', () => { + const builder = new CompareConfigBuilder(); + expect(() => builder.default().removeRule('does-not-exist')).toThrow('ruleKey \'does-not-exist\' does not exist'); + }); +});