Skip to content

Commit f1cbda2

Browse files
committed
feat: WIP added a file modification calculator to calculate imports
1 parent 0b919e7 commit f1cbda2

File tree

5 files changed

+210
-4
lines changed

5 files changed

+210
-4
lines changed

package-lock.json

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
"@oclif/core": "^4.0.8",
1111
"@oclif/plugin-help": "^6.2.4",
1212
"@oclif/plugin-update": "^4.6.13",
13+
"@types/diff": "^7.0.1",
1314
"ajv": "^8.12.0",
1415
"ajv-formats": "^3.0.1",
1516
"chalk": "^5.3.0",
1617
"codify-schemas": "^1.0.63",
1718
"debug": "^4.3.4",
19+
"diff": "^7.0.0",
1820
"ink": "^5",
1921
"jotai": "^2.11.1",
2022
"js-yaml": "^4.1.0",

src/entities/resource-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ export class ResourceConfig implements ConfigBlock {
113113
this.raw.name = name;
114114
}
115115

116+
setParameter(name: string, value: unknown) {
117+
this.parameters[name] = value;
118+
this.raw[name] = value;
119+
}
120+
116121
addDependenciesFromDependsOn(resourceExists: (id: string) => boolean) {
117122
for (const id of this.dependsOn) {
118123
if (!resourceExists(id)) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, it, vi, afterEach } from 'vitest';
2+
import { FileModificationCalculator, ModificationType } from './file-modification-calculator';
3+
import { ResourceConfig } from '../entities/resource-config';
4+
import { ResourceInfo } from '../entities/resource-info';
5+
import { FileType, InMemoryFile } from '../parser/entities';
6+
7+
vi.mock('node:fs', async () => {
8+
const { fs } = await import('memfs');
9+
return fs
10+
})
11+
12+
vi.mock('node:fs/promises', async () => {
13+
const { fs } = await import('memfs');
14+
return fs.promises;
15+
})
16+
17+
18+
describe('File modification calculator tests', () => {
19+
20+
it('Can generate a diff and a new file', async () => {
21+
const existingResource = new ResourceConfig({
22+
type: 'resource1'
23+
});
24+
existingResource.attachResourceInfo(generateResourceInfo('resource1'))
25+
26+
const existingFileContents =
27+
`[
28+
{
29+
"type": "project",
30+
"plugins": {
31+
"default": "latest",
32+
}
33+
},
34+
{ "type": "resource1" }
35+
]`
36+
const existingFile = <InMemoryFile> { filePath: '/path/to/file.json', fileType: FileType.JSON, contents: existingFileContents };
37+
38+
const modifiedResource = new ResourceConfig({
39+
type: 'resource1',
40+
parameter1: 'abc'
41+
})
42+
modifiedResource.attachResourceInfo(generateResourceInfo('resource1'))
43+
44+
const calculator = new FileModificationCalculator([existingResource], existingFile)
45+
const result = await calculator.calculate([{
46+
modification: ModificationType.INSERT_OR_UPDATE,
47+
resource: modifiedResource,
48+
}])
49+
50+
console.log(result)
51+
console.log(result.diff)
52+
})
53+
54+
afterEach(() => {
55+
vi.resetAllMocks();
56+
})
57+
})
58+
59+
function generateResourceInfo(type: string, requiredParameters?: string[]): ResourceInfo {
60+
return ResourceInfo.fromResponseData({
61+
plugin: 'plugin',
62+
type,
63+
import: { requiredParameters }
64+
})
65+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import chalk from 'chalk';
2+
import { ResourceConfig } from '../entities/resource-config.js';
3+
import * as Diff from 'diff'
4+
import { FileType, InMemoryFile } from '../parser/entities.js';
5+
6+
export enum ModificationType {
7+
INSERT_OR_UPDATE,
8+
DELETE
9+
}
10+
11+
export interface ModifiedResource {
12+
resource: ResourceConfig;
13+
modification: ModificationType
14+
}
15+
16+
export interface FileModificationResult {
17+
newFile: string;
18+
diff: string;
19+
}
20+
21+
export class FileModificationCalculator {
22+
private existingFile?: InMemoryFile;
23+
private existingResources: ResourceConfig[];
24+
25+
constructor(existingResources: ResourceConfig[], existingFile: InMemoryFile) {
26+
this.existingFile = existingFile;
27+
this.existingResources = existingResources;
28+
}
29+
30+
async calculate(modifications: ModifiedResource[]): Promise<FileModificationResult> {
31+
const resultResources = [...this.existingResources]
32+
33+
if (this.existingResources.length === 0 || !this.existingFile) {
34+
const newFile = JSON.stringify(
35+
modifications
36+
.filter((r) => r.modification === ModificationType.INSERT_OR_UPDATE)
37+
.map((r) => r.resource.raw),
38+
null, 2)
39+
40+
return {
41+
newFile,
42+
diff: this.diff('', newFile),
43+
}
44+
}
45+
46+
this.validate(modifications);
47+
48+
for (const modified of modifications) {
49+
const duplicateIndex = this.existingResources.findIndex((existing) => existing.isSameOnSystem(modified.resource))
50+
51+
if (duplicateIndex === -1) {
52+
if (modified.modification === ModificationType.INSERT_OR_UPDATE) {
53+
resultResources.push(modified.resource);
54+
}
55+
56+
continue;
57+
}
58+
59+
if (modified.modification === ModificationType.DELETE) {
60+
resultResources.splice(duplicateIndex, 1);
61+
continue;
62+
}
63+
64+
const duplicate = resultResources[duplicateIndex];
65+
for (const [key, newValue] of Object.entries(modified.resource.parameters)) {
66+
duplicate.setParameter(key, newValue);
67+
}
68+
}
69+
70+
const newFile = JSON.stringify(
71+
resultResources.map((r) => r.raw),
72+
null, 2
73+
);
74+
75+
return {
76+
newFile,
77+
diff: this.diff(this.existingFile.contents, newFile),
78+
}
79+
}
80+
81+
validate(modifiedResources: ModifiedResource[]): void {
82+
if (!this.existingFile) {
83+
return;
84+
}
85+
86+
if (this.existingFile?.fileType !== FileType.JSON) {
87+
throw new Error(`Only updating .json files are currently supported. Found ${this.existingFile?.filePath}`);
88+
}
89+
90+
if (this.existingResources.some((r) => !r.resourceInfo)) {
91+
const badResources = this.existingResources
92+
.filter((r) => !r.resourceInfo)
93+
.map((r) => r.id);
94+
95+
throw new Error(`All resources must have resource info attached to generate diff. Found bad resources: ${badResources}`);
96+
}
97+
98+
if (modifiedResources.some((r) => !r.resource.resourceInfo)) {
99+
const badResources = modifiedResources
100+
.filter((r) => !r.resource.resourceInfo)
101+
.map((r) => r.resource.id);
102+
103+
throw new Error(`All resources must have resource info attached to generate diff. Found bad resources: ${badResources}`);
104+
}
105+
}
106+
107+
diff(a: string, b: string): string {
108+
const diff = Diff.diffLines(a, b);
109+
110+
let result = '';
111+
diff.forEach((part) => {
112+
result += part.added ? chalk.green(part.value) :
113+
part.removed ? chalk.red(part.value) :
114+
part.value;
115+
});
116+
117+
return result;
118+
}
119+
}

0 commit comments

Comments
 (0)