From a8a60689c0f32bd14ca2e1d9fe96fbcfaa00e149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Colladon?= Date: Sun, 1 Dec 2024 19:46:10 +0100 Subject: [PATCH] refactor: simplify implementation and improve performance --- src/utils/fxpHelper.ts | 3 +- src/utils/metadataDiff.ts | 288 ++++++++++++++++++++------------------ 2 files changed, 157 insertions(+), 134 deletions(-) diff --git a/src/utils/fxpHelper.ts b/src/utils/fxpHelper.ts index 8bd504f8..7375f80f 100644 --- a/src/utils/fxpHelper.ts +++ b/src/utils/fxpHelper.ts @@ -25,7 +25,8 @@ const JSON_PARSER_OPTION = { suppressEmptyNode: false, } -export const asArray = (node: string[] | string) => { +// biome-ignore lint/suspicious/noExplicitAny: +export const asArray = (node: any[] | any) => { return Array.isArray(node) ? node : [node] } diff --git a/src/utils/metadataDiff.ts b/src/utils/metadataDiff.ts index 3b5b7c53..aba2a55c 100644 --- a/src/utils/metadataDiff.ts +++ b/src/utils/metadataDiff.ts @@ -1,11 +1,9 @@ 'use strict' import { differenceWith, isEqual, isUndefined } from 'lodash' - import type { Config } from '../types/config' import type { SharedFileMetadata } from '../types/metadata' import type { Manifest } from '../types/work' - import { ATTRIBUTE_PREFIX, XML_HEADER_ATTRIBUTE_KEY, @@ -15,77 +13,159 @@ import { } from './fxpHelper' import { fillPackageWithParameter } from './packageHelper' -type DiffResult = { +// biome-ignore lint/suspicious/noExplicitAny: +type XmlContent = Record +// biome-ignore lint/suspicious/noExplicitAny: +type XmlElement = Record + +type KeySelectorFn = (elem: XmlElement) => string | undefined + +interface DiffResult { added: Manifest deleted: Manifest } -type PrunedContent = { +interface PrunedContent { xmlContent: string isEmpty: boolean } -class MetadataExtractor { - constructor(private attributes: Map) {} +export default class MetadataDiff { + private toContent!: XmlContent + private fromContent!: XmlContent + private extractor: MetadataExtractor - // biome-ignore lint/suspicious/noExplicitAny: - public getRoot(fileContent: any): any { - return ( - fileContent[ - Object.keys(fileContent).find( - attr => attr !== XML_HEADER_ATTRIBUTE_KEY - )! - ] ?? {} + constructor( + private config: Config, + attributes: Map + ) { + this.extractor = new MetadataExtractor(attributes) + } + + async compare(path: string): Promise { + const [toContent, fromContent] = await Promise.all([ + parseXmlFileToJson({ path, oid: this.config.to }, this.config), + parseXmlFileToJson({ path, oid: this.config.from }, this.config), + ]) + + this.toContent = toContent + this.fromContent = fromContent + + const comparator = new MetadataComparator(this.extractor) + + const added = comparator.compare( + this.toContent, + this.fromContent, + this.compareAdded() ) + const deleted = comparator.compare( + this.fromContent, + this.toContent, + this.compareDeleted() + ) + + return { added, deleted } } - isTypePackageable(subType: string): unknown { - return this.attributes.get(subType)?.excluded !== true + prune(): PrunedContent { + const transformer = new JsonTransformer(this.extractor) + const prunedContent = transformer.generatePartialJson( + this.fromContent, + this.toContent + ) + + return { + xmlContent: convertJsonToXml(prunedContent), + isEmpty: this.extractor.isContentEmpty(prunedContent), + } } - // biome-ignore lint/suspicious/noExplicitAny: - public getSubTypes(fileContent: any): string[] { - const root = this.getRoot(fileContent) + private compareAdded() { + return ( + meta: XmlElement[], + keySelector: KeySelectorFn, + elem: XmlElement + ) => { + const elemKey = keySelector(elem) + const match = meta.find(el => keySelector(el) === elemKey) + return !match || !isEqual(match, elem) + } + } + + private compareDeleted() { + return ( + meta: XmlElement[], + keySelector: KeySelectorFn, + elem: XmlElement + ) => { + const elemKey = keySelector(elem) + return !meta.some(el => keySelector(el) === elemKey) + } + } +} + +class MetadataExtractor { + constructor(readonly attributes: Map) {} + + getSubTypes(fileContent: XmlContent): string[] { + const root = this.extractRootElement(fileContent) return Object.keys(root).filter(tag => this.attributes.has(tag)) } - getXmlName(subType: string) { + isTypePackageable(subType: string): boolean { + return !this.attributes.get(subType)?.excluded + } + + getXmlName(subType: string): string { return this.attributes.get(subType)?.xmlName! } - public getKeySelector(subType: string) { - // biome-ignore lint/suspicious/noExplicitAny: - return (elem: any) => elem[this.attributes.get(subType)?.key!] + getKeyValueSelector(subType: string): KeySelectorFn { + const metadataKey = this.getKeyFieldDefinition(subType) + return elem => elem[metadataKey!] as string } - // biome-ignore lint/suspicious/noExplicitAny: - public extractForSubType(fileContent: any, subType: string): any[] { - return asArray(this.getRoot(fileContent)?.[subType] ?? []) + getKeyFieldDefinition(subType: string): string | undefined { + return this.attributes.get(subType)?.key } - // biome-ignore lint/suspicious/noExplicitAny: - public isContentEmpty(fileContent: any): boolean { - const root = this.getRoot(fileContent) + extractForSubType(fileContent: XmlContent, subType: string): XmlElement[] { + const root = this.extractRootElement(fileContent) + const content = root[subType] + return content ? asArray(content) : [] + } + + isContentEmpty(fileContent: XmlContent): boolean { + const root = this.extractRootElement(fileContent) return Object.entries(root) .filter(([key]) => !key.startsWith(ATTRIBUTE_PREFIX)) - .every(([, value]) => Array.isArray(value) && value.length === 0) + .every( + ([, value]) => !value || (Array.isArray(value) && value.length === 0) + ) + } + + extractRootElement(fileContent: XmlContent): XmlElement { + const rootKey = + Object.keys(fileContent).find(key => key !== XML_HEADER_ATTRIBUTE_KEY) ?? + '' + return (fileContent[rootKey] as XmlElement) ?? {} } } class MetadataComparator { constructor(private extractor: MetadataExtractor) {} - public compare( - // biome-ignore lint/suspicious/noExplicitAny: - baseContent: any, - // biome-ignore lint/suspicious/noExplicitAny: - targetContent: any, - // biome-ignore lint/suspicious/noExplicitAny: - predicate: (base: any[], type: string, key: string) => boolean + compare( + baseContent: XmlContent, + targetContent: XmlContent, + elementMatcher: ( + meta: XmlElement[], + keySelector: KeySelectorFn, + elem: XmlElement + ) => boolean ): Manifest { - const subTypes = this.extractor.getSubTypes(baseContent) - - return subTypes + return this.extractor + .getSubTypes(baseContent) .filter(subType => this.extractor.isTypePackageable(subType)) .reduce((manifest, subType) => { const baseMeta = this.extractor.extractForSubType(baseContent, subType) @@ -93,118 +173,60 @@ class MetadataComparator { targetContent, subType ) - - const keySelector = this.extractor.getKeySelector(subType) + const keySelector = this.extractor.getKeyValueSelector(subType) const xmlName = this.extractor.getXmlName(subType) - for (const elem of baseMeta) { - if (predicate(targetMeta, subType, elem)) { + + baseMeta + .filter(elem => elementMatcher(targetMeta, keySelector, elem)) + .forEach(elem => { fillPackageWithParameter({ store: manifest, type: xmlName, - member: keySelector(elem), + member: keySelector(elem)!, }) - } - } + }) + return manifest }, new Map()) } } class JsonTransformer { - constructor(private attributes: Map) {} + constructor(private extractor: MetadataExtractor) {} - // biome-ignore lint/suspicious/noExplicitAny: - public generatePartialJson(fromContent: any, toContent: any): any { - const metadataExtractor = new MetadataExtractor(this.attributes) - const subTypes = metadataExtractor.getSubTypes(toContent) - return subTypes.reduce((acc, subType) => { - const fromMeta = metadataExtractor.extractForSubType(fromContent, subType) - const toMeta = metadataExtractor.extractForSubType(toContent, subType) + generatePartialJson( + fromContent: XmlContent, + toContent: XmlContent + ): XmlContent { + return this.extractor.getSubTypes(toContent).reduce((acc, subType) => { + const fromMeta = this.extractor.extractForSubType(fromContent, subType) + const toMeta = this.extractor.extractForSubType(toContent, subType) + const keyField = this.extractor.getKeyFieldDefinition(subType) - const rootMetadata = metadataExtractor.getRoot(acc) + const partialContentBuilder = isUndefined(keyField) + ? this.getPartialContentWithoutKey + : this.getPartialContentWithKey + + this.extractor.extractRootElement(acc)[subType] = partialContentBuilder( + fromMeta, + toMeta + ) - rootMetadata[subType] = this.getPartialContent(fromMeta, toMeta, subType) return acc }, structuredClone(toContent)) } - private getPartialContent( - fromMeta: string[], - toMeta: string[], - subType: string - ): string[] { - const keyField = this.attributes.get(subType)?.key - if (isUndefined(keyField)) { - return isEqual(fromMeta, toMeta) ? [] : toMeta - } - return differenceWith(toMeta, fromMeta, isEqual) - } -} - -export default class MetadataDiff { - // biome-ignore lint/suspicious/noExplicitAny: - private toContent: any - // biome-ignore lint/suspicious/noExplicitAny: - private fromContent: any - private added!: Manifest - private metadataExtractor!: MetadataExtractor - - constructor( - private config: Config, - private attributes: Map - ) { - this.metadataExtractor = new MetadataExtractor(this.attributes) + private getPartialContentWithoutKey( + fromMeta: XmlElement[], + toMeta: XmlElement[] + ): XmlElement[] { + return isEqual(fromMeta, toMeta) ? [] : toMeta } - public async compare(path: string): Promise { - this.toContent = await parseXmlFileToJson( - { path, oid: this.config.to }, - this.config - ) - this.fromContent = await parseXmlFileToJson( - { path, oid: this.config.from }, - this.config - ) - - const comparator = new MetadataComparator(this.metadataExtractor) - this.added = comparator.compare( - this.toContent, - this.fromContent, - // biome-ignore lint/suspicious/noExplicitAny: - (meta, type, elem: any) => { - const keySelector = this.metadataExtractor.getKeySelector(type) - const elemKey = keySelector(elem) - // biome-ignore lint/suspicious/noExplicitAny: - const match = meta.find((el: any) => keySelector(el) === elemKey) - return !match || !isEqual(match, elem) - } - ) - - const deleted = comparator.compare( - this.fromContent, - this.toContent, - // biome-ignore lint/suspicious/noExplicitAny: - (meta, type, elem: any) => { - const keySelector = this.metadataExtractor.getKeySelector(type) - const elemKey = keySelector(elem) - // biome-ignore lint/suspicious/noExplicitAny: - return !meta.some((el: any) => keySelector(el) === elemKey) - } - ) - - return { added: this.added, deleted } - } - - public prune(): PrunedContent { - const transformer = new JsonTransformer(this.attributes) - const prunedContent = transformer.generatePartialJson( - this.fromContent, - this.toContent - ) - - return { - xmlContent: convertJsonToXml(prunedContent), - isEmpty: this.metadataExtractor.isContentEmpty(prunedContent), - } + private getPartialContentWithKey( + fromMeta: XmlElement[], + toMeta: XmlElement[] + ): XmlElement[] { + return differenceWith(toMeta, fromMeta, isEqual) } }