Skip to content

Commit 00c70af

Browse files
committed
Refactored entities
1 parent 0b94a8d commit 00c70af

21 files changed

Lines changed: 615 additions & 439 deletions

codify-core/src/commands/plan/orchestrator.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ import { PluginCollection } from '../../plugins/plugin-collection';
33

44
export const PlanOrchestrator = {
55
async run(rootDirectory: string): Promise<string> {
6-
const project = await ConfigCompiler.parseProject(rootDirectory);
6+
const parsedProject = await ConfigCompiler.parseProject(rootDirectory);
77

8-
const pluginCollection = await PluginCollection.create(project);
9-
const resourceDefinitions = await pluginCollection.getAllResourceDefinitions();
8+
const pluginCollection = await PluginCollection.create(parsedProject);
9+
const definitions = await pluginCollection.getResourceDefinitions();
1010

11-
await ConfigCompiler.analyzeProject(project, resourceDefinitions);
12-
const dependencyList = ConfigCompiler.buildDependencyList(project);
13-
console.log(dependencyList);
11+
const compiledProject = await ConfigCompiler.compileProject(parsedProject, definitions);
12+
console.log(compiledProject.getApplySequence());
1413

15-
const plan = await pluginCollection.getPlan(project);
14+
const plan = await pluginCollection.getPlan(parsedProject);
1615

1716
await pluginCollection.destroy();
1817
return JSON.stringify(plan, null, 2);

codify-core/src/config-compiler/index.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { ResourceDefinitions } from '../plugins/entities/definitions/resource';
22
import { InternalError } from '../utils/errors';
33
import { ConfigClass } from './language-definition';
44
import { ConfigLoader } from './loader';
5-
import { ConfigSemanticAnalyzer } from './output/config-semantic-analyzer';
6-
import { DependencyBuilder } from './output/dependency-builder';
5+
import { DependencyGraphBuilder } from './output-generation/dependency-graph-builder';
6+
import { CompiledProject } from './output-generation/entities/compiled-project';
7+
import { CompiledProjectTransformer } from './output-generation/transformer';
78
import { FileParser } from './parser';
89
import { ProjectConfig } from './parser/entities/configs/project';
9-
import { ResourceConfig } from './parser/entities/configs/resource';
1010
import { ParsedModule } from './parser/entities/parsed-module';
1111
import { ParsedProject } from './parser/entities/parsed-project';
1212
import { JsonFileParser } from './parser/json/file-parser';
@@ -44,14 +44,9 @@ export class ConfigCompiler {
4444
})
4545
}
4646

47-
static async analyzeProject(parsedProject: ParsedProject, resourceDefinitions: ResourceDefinitions): Promise<void> {
48-
ConfigSemanticAnalyzer.validate(parsedProject, resourceDefinitions);
49-
}
50-
51-
static async buildDependencyList(parsedProject: ParsedProject): Promise<ResourceConfig[]> {
52-
const dependencyGraph = DependencyBuilder.buildDependencyGraph(parsedProject.coreModule.configBlocks
53-
.filter((u) => u.configClass === ConfigClass.RESOURCE) as ResourceConfig[]
54-
)
55-
return DependencyBuilder.generateDependencyList(dependencyGraph);
47+
static compileProject(parsedProject: ParsedProject, definitions: ResourceDefinitions): CompiledProject {
48+
const compiledProject = CompiledProjectTransformer.validateAndTransform(parsedProject, definitions);
49+
DependencyGraphBuilder.buildDependencyGraph(compiledProject);
50+
return compiledProject;
5651
}
5752
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { expect } from '@oclif/test';
2+
import { DependencyGraphBuilder } from './dependency-graph-builder';
3+
import { Resource } from './entities/resource';
4+
import { CompiledProject } from './entities/compiled-project';
5+
import { ProjectConfig } from '../parser/entities/configs/project';
6+
7+
describe('Dependency graph tests', () => {
8+
9+
it('parses and replace resource references', () => {
10+
const resource1 = () => new Resource(
11+
'homebrew_installation',
12+
undefined,
13+
new Map([
14+
['directory', '/usr/opt']
15+
]),
16+
'',
17+
);
18+
19+
const resource2 = () => new Resource(
20+
'homebrew_options',
21+
undefined,
22+
new Map([
23+
['directory', '${homebrew_installation.directory}']
24+
]),
25+
'',
26+
);
27+
28+
const graph = new Map([resource1(), resource2()].map((g) => [g.id, g]));
29+
const project = new CompiledProject({ applyableGraph: graph, projectConfig: {} as ProjectConfig });
30+
31+
expect(() => DependencyGraphBuilder.buildDependencyGraph(project)).to.not.throw()
32+
expect(project.applyableGraph.get('homebrew_installation')).to.deep.eq(resource1())
33+
expect(project.applyableGraph.get('homebrew_options')).to.not.deep.eq(resource2())
34+
expect(project.applyableGraph.get('homebrew_options').parameters.get('directory')).to.eq(graph.get('homebrew_installation')!.parameters.get('directory'));
35+
})
36+
37+
it('validates invalid resources', () => {
38+
const resource1 = () => new Resource(
39+
'homebrew_installation',
40+
undefined,
41+
new Map([
42+
['directory', '/usr/opt']
43+
]),
44+
'',
45+
);
46+
47+
const resource2 = () => new Resource(
48+
'homebrew_options',
49+
undefined,
50+
new Map([
51+
['directory', '${homebrew_installation_invalid.directory}']
52+
]),
53+
'',
54+
);
55+
56+
const graph = new Map([resource1(), resource2()].map((g) => [g.id, g]));
57+
const project = new CompiledProject({ applyableGraph: graph, projectConfig: {} as ProjectConfig });
58+
59+
expect(() => DependencyGraphBuilder.buildDependencyGraph(project)).to.throw()
60+
})
61+
62+
it('validates invalid parameters', () => {
63+
const resource1 = () => new Resource(
64+
'homebrew_installation',
65+
undefined,
66+
new Map([
67+
['directory', '/usr/opt']
68+
]),
69+
'',
70+
);
71+
72+
const resource2 = () => new Resource(
73+
'homebrew_options',
74+
undefined,
75+
new Map([
76+
['directory', '${homebrew_installation.directory_invalid}']
77+
]),
78+
'',
79+
);
80+
81+
const graph = new Map([resource1(), resource2()].map((g) => [g.id, g]));
82+
const project = new CompiledProject({ applyableGraph: graph, projectConfig: {} as ProjectConfig });
83+
84+
expect(() => DependencyGraphBuilder.buildDependencyGraph(project)).to.throw()
85+
})
86+
87+
it('handles multiple resource references', () => {
88+
const resource1 = () => new Resource(
89+
'homebrew_installation',
90+
undefined,
91+
new Map([
92+
['directory', '/usr/opt']
93+
]),
94+
'',
95+
);
96+
97+
const resource2 = () => new Resource(
98+
'homebrew_options',
99+
undefined,
100+
new Map([
101+
['directory', '$\{homebrew_installation.directory} and $\{homebrew_installation.directory}']
102+
]),
103+
'',
104+
);
105+
106+
const graph = new Map([resource1(), resource2()].map((g) => [g.id, g]));
107+
const project = new CompiledProject({ applyableGraph: graph, projectConfig: {} as ProjectConfig });
108+
109+
expect(() => DependencyGraphBuilder.buildDependencyGraph(project)).to.not.throw()
110+
expect(project.applyableGraph.get('homebrew_installation')).to.deep.eq(resource1())
111+
expect(project.applyableGraph.get('homebrew_options')).to.not.deep.eq(resource2())
112+
expect(project.applyableGraph.get('homebrew_options').parameters.get('directory')).to.eq(`/usr/opt and /usr/opt`);
113+
})
114+
115+
it('handles named resources', () => {
116+
const resource1 = () => new Resource(
117+
'homebrew_installation',
118+
'first',
119+
new Map([
120+
['directory', '/usr/opt']
121+
]),
122+
'',
123+
);
124+
125+
const resource2 = () => new Resource(
126+
'homebrew_options',
127+
undefined,
128+
new Map([
129+
['directory', '$\{homebrew_installation.first.directory}']
130+
]),
131+
'',
132+
);
133+
134+
const graph = new Map([resource1(), resource2()].map((g) => [g.id, g]));
135+
const project = new CompiledProject({ applyableGraph: graph, projectConfig: {} as ProjectConfig });
136+
137+
expect(() => DependencyGraphBuilder.buildDependencyGraph(project)).to.not.throw()
138+
expect(project.applyableGraph.get('homebrew_installation.first')).to.deep.eq(resource1())
139+
expect(project.applyableGraph.get('homebrew_options')).to.not.deep.eq(resource2())
140+
expect(project.applyableGraph.get('homebrew_options').parameters.get('directory')).to.eq(`/usr/opt`);
141+
})
142+
})
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { CompiledProject } from './entities/compiled-project';
2+
import { ResourceParameter } from './entities/resource-parameter';
3+
4+
export const DependencyGraphBuilder = {
5+
6+
/**
7+
* @return a dependency graph in the form of an adjacency list
8+
*/
9+
buildDependencyGraph(compiledProject: CompiledProject) {
10+
const { applyableGraph } = compiledProject;
11+
12+
const resourceReferenceRegex = /\${([\w.]+)}/g
13+
14+
// TODO: Support named resources in the future
15+
16+
for (const applyable of applyableGraph.values()) {
17+
const referenceParameters = findParametersWithReferences(applyable.parameters)
18+
19+
for (const [name, match] of referenceParameters) {
20+
const parts = match.split('.');
21+
if (parts.length < 2) {
22+
throw new Error(`Only resource parameter references are allowed. ${match}`);
23+
}
24+
25+
const referencedId = findId(parts);
26+
if (!referencedId) {
27+
throw new Error(`Unable to find resource being referenced. ${match}`);
28+
}
29+
30+
const referencedResource = applyableGraph.get(referencedId)
31+
if (!referencedResource) {
32+
throw new Error(`Unable to find resource being referenced. ${match}`);
33+
}
34+
35+
const referencedParameterName = findParameterName(parts, referencedId);
36+
const referencedParameter = referencedResource.parameters.get(referencedParameterName);
37+
if (!referencedParameter) {
38+
throw new Error(`Un-able to find parameter being referenced. ${match}`);
39+
}
40+
41+
// TODO: Add recursive check for parameters of type parameter
42+
43+
applyable.dependencies.push(referencedResource);
44+
45+
// Substitute with actual value
46+
applyable.parameters.set(name,
47+
String(applyable.parameters.get(name)).replace(`\${${match}}`, String(referencedParameter))
48+
);
49+
}
50+
}
51+
52+
function findParametersWithReferences(parameters: Map<string, ResourceParameter>) {
53+
return [...parameters.entries()]
54+
.map(([name, value]) => [name, String(value), String(value).matchAll(resourceReferenceRegex)] as const)
55+
.filter(([, _, match]) => match)
56+
.flatMap(([name, _, matches]) =>
57+
[...matches].map(match => [name, match[1]] as const)
58+
);
59+
}
60+
61+
function findId(parts: string[]): null | string {
62+
if (applyableGraph.has(parts[0])) {
63+
return parts[0];
64+
}
65+
66+
if (applyableGraph.has(parts[0] + '.' + parts[1])) {
67+
return parts[0] + '.' + parts[1];
68+
}
69+
70+
return null;
71+
}
72+
73+
function findParameterName(parts: string[], id: string): string {
74+
return id.split('.').length === 1 ? parts[1] : parts[2];
75+
}
76+
},
77+
}

0 commit comments

Comments
 (0)