Skip to content

Commit

Permalink
refactor: simplify implementation and improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
scolladon committed Dec 1, 2024
1 parent 60b1c88 commit ccea764
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 135 deletions.
3 changes: 2 additions & 1 deletion src/utils/fxpHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const JSON_PARSER_OPTION = {
suppressEmptyNode: false,
}

export const asArray = (node: string[] | string) => {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export const asArray = (node: any[] | any) => {
return Array.isArray(node) ? node : [node]
}

Expand Down
290 changes: 156 additions & 134 deletions src/utils/metadataDiff.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,196 +13,220 @@ import {
} from './fxpHelper'
import { fillPackageWithParameter } from './packageHelper'

type DiffResult = {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type XmlContent = Record<string, any>
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type XmlElement = Record<string, any>

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<string, SharedFileMetadata>) {}
export default class MetadataDiff {
private toContent!: XmlContent
private fromContent!: XmlContent
private extractor: MetadataExtractor

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
public getRoot(fileContent: any): any {
return (
fileContent[
Object.keys(fileContent).find(
attr => attr !== XML_HEADER_ATTRIBUTE_KEY
)!
] ?? {}
constructor(
private config: Config,
attributes: Map<string, SharedFileMetadata>
) {
this.extractor = new MetadataExtractor(attributes)
}

async compare(path: string): Promise<DiffResult> {
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: <explanation>
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<string, SharedFileMetadata>) {}

getSubTypes(fileContent: XmlContent): string[] {
const root = this.extractRootElement(fileContent)
return Object.keys(root).filter(tag => this.attributes.has(tag))
}

getXmlName(subType: string) {
return this.attributes.get(subType)?.xmlName!
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: <explanation>
return (elem: any) => elem[this.attributes.get(subType)?.key!]
getKeyValueSelector(subType: string): KeySelectorFn {
const metadataKey = this.getKeyFieldDefinition(subType)
return elem => (metadataKey ? (elem[metadataKey] as string) : undefined)
}

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
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: <explanation>
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: <explanation>
baseContent: any,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
targetContent: any,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
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)
const targetMeta = this.extractor.extractForSubType(
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<string, SharedFileMetadata>) {}
constructor(private extractor: MetadataExtractor) {}

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
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: <explanation>
private toContent: any
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
private fromContent: any
private added!: Manifest
private metadataExtractor!: MetadataExtractor

constructor(
private config: Config,
private attributes: Map<string, SharedFileMetadata>
) {
this.metadataExtractor = new MetadataExtractor(this.attributes)
private getPartialContentWithoutKey(
fromMeta: XmlElement[],
toMeta: XmlElement[]
): XmlElement[] {
return isEqual(fromMeta, toMeta) ? [] : toMeta
}

public async compare(path: string): Promise<DiffResult> {
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: <explanation>
(meta, type, elem: any) => {
const keySelector = this.metadataExtractor.getKeySelector(type)
const elemKey = keySelector(elem)
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
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: <explanation>
(meta, type, elem: any) => {
const keySelector = this.metadataExtractor.getKeySelector(type)
const elemKey = keySelector(elem)
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
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)
}
}

0 comments on commit ccea764

Please sign in to comment.