Skip to content

Commit 51156d2

Browse files
committed
Add dependency parser + tests
1 parent 99d04c8 commit 51156d2

7 files changed

Lines changed: 234 additions & 4 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { ConfigCompiler } from '../../config-compiler';
2-
import { ConfigSemanticAnalyzer } from '../../config-compiler/semantic-analysis/config-semantic-analyzer';
32
import { PluginCollection } from '../../plugins/plugin-collection';
43

54
export const PlanOrchestrator = {
@@ -9,7 +8,9 @@ export const PlanOrchestrator = {
98
const pluginCollection = await PluginCollection.create(project);
109
const resourceDefinitions = await pluginCollection.getAllResourceDefinitions();
1110

12-
await ConfigSemanticAnalyzer.validate(project, resourceDefinitions);
11+
await ConfigCompiler.analyzeProject(project, resourceDefinitions);
12+
13+
console.log(project.coreModule.configBlocks);
1314

1415
await pluginCollection.destroy();
1516
},

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ConfigLoader } from './loader';
55
import { FileParser } from './parser';
66
import { ParsedModule, ParsedProject } from './parser/entities';
77
import { ProjectConfig } from './parser/entities/project';
8+
import { ResourceConfig } from './parser/entities/resource';
89
import { JsonFileParser } from './parser/json/file-parser';
910
import { ConfigSemanticAnalyzer } from './semantic-analysis/config-semantic-analyzer';
1011

@@ -43,5 +44,8 @@ export class ConfigCompiler {
4344

4445
static async analyzeProject(parsedProject: ParsedProject, resourceDefinitions: ResourceDefinitions): Promise<void> {
4546
ConfigSemanticAnalyzer.validate(parsedProject, resourceDefinitions);
47+
ConfigSemanticAnalyzer.parseResourceDependencies(parsedProject.coreModule.configBlocks
48+
.filter((u) => u.configType === ConfigBlockType.RESOURCE) as ResourceConfig[]
49+
)
4650
}
4751
}

codify-core/src/config-compiler/parser/entities/resource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class ResourceConfig implements ConfigBlock {
2424
type: string;
2525
name?: string;
2626
parameters: Record<string, unknown>;
27-
dependencies?: Node[] = [];
27+
dependencies: { match: string; parameterName: string; parameterValue: string; resource: ResourceConfig }[] = [];
2828

2929
constructor(config: unknown) {
3030
if (this.validate(config)) {
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { ResourceConfig } from '../parser/entities/resource';
2+
import { ResourceDefinition } from '../../entities/resource-definition';
3+
import { ResourceParameterDefinition } from '../../entities/resource-parameter';
4+
import { ResourceParameterType } from '../language-definition';
5+
import { ConfigSemanticAnalyzer } from './config-semantic-analyzer';
6+
import { expect } from '@oclif/test';
7+
8+
describe('Config semantic analyzer tests', () => {
9+
10+
const createResource1 = () => {
11+
return new ResourceConfig({
12+
type: 'homebrew_installation',
13+
directory: '/usr/opt'
14+
})
15+
}
16+
17+
const createResource2 = () => {
18+
return new ResourceConfig({
19+
type: 'homebrew_installation',
20+
invalidParameter: 'something',
21+
})
22+
}
23+
24+
const createResource3 = () => {
25+
return new ResourceConfig({
26+
type: 'homebrew_installation_wrong',
27+
directory: '/usr/opt'
28+
})
29+
}
30+
31+
const createResource4 = () => {
32+
return new ResourceConfig({
33+
type: 'homebrew_installation',
34+
directory: 123
35+
})
36+
}
37+
38+
const createResource5 = () => {
39+
return new ResourceConfig({
40+
type: 'homebrew_options',
41+
directory: '${homebrew_installation.directory}'
42+
})
43+
}
44+
45+
const createResource6 = () => {
46+
return new ResourceConfig({
47+
type: 'homebrew_options',
48+
directory: '${homebrew_installation_invalid.directory}'
49+
})
50+
}
51+
52+
const createResource7 = () => {
53+
return new ResourceConfig({
54+
type: 'homebrew_options',
55+
directory: '${homebrew_installation.directory_invalid}'
56+
})
57+
}
58+
59+
const createResource8 = () => {
60+
return new ResourceConfig({
61+
type: 'homebrew_options',
62+
directory: `$\{homebrew_installation.directory} and $\{homebrew_installation.directory}`
63+
})
64+
}
65+
66+
const createResourceDefinition1 = () => {
67+
return new ResourceDefinition({
68+
name: 'homebrew_installation',
69+
parameters: new Map(Object.entries({
70+
directory: new ResourceParameterDefinition({
71+
name: 'directory',
72+
type: ResourceParameterType.STRING,
73+
})
74+
}))
75+
})
76+
}
77+
78+
79+
it('validates resources based on the definition', () => {
80+
const resource = createResource1();
81+
const definition = createResourceDefinition1();
82+
83+
expect(() => ConfigSemanticAnalyzer.validateResourceConfigs([resource], new Map([[definition.name, definition] as const]))).to.not.throw();
84+
});
85+
86+
it('validates invalid parameter names', () => {
87+
const resource = createResource2();
88+
const definition = createResourceDefinition1();
89+
90+
expect(() => ConfigSemanticAnalyzer.validateResourceConfigs([resource], new Map([[definition.name, definition] as const]))).to.throw();
91+
})
92+
93+
it('validates invalid resource names', () => {
94+
const resource = createResource3();
95+
const definition = createResourceDefinition1();
96+
97+
98+
expect(() => ConfigSemanticAnalyzer.validateResourceConfigs([resource], new Map([[definition.name, definition] as const]))).to.throw();
99+
})
100+
101+
it('validates invalid parameter type', () => {
102+
const resource = createResource4();
103+
const definition = createResourceDefinition1();
104+
105+
expect(() => ConfigSemanticAnalyzer.validateResourceConfigs([resource], new Map([[definition.name, definition] as const]))).to.throw();
106+
})
107+
108+
it('parses and replace resource references', () => {
109+
const resource1 = createResource1();
110+
const resource2 = createResource5();
111+
112+
expect(() => ConfigSemanticAnalyzer.parseResourceDependencies([resource1, resource2])).to.not.throw()
113+
expect(resource1).to.deep.eq(createResource1())
114+
expect(resource2.parameters['directory']).to.eq(resource1.parameters.directory);
115+
})
116+
117+
it('validates invalid resources', () => {
118+
const resource1 = createResource1();
119+
const resource2 = createResource6();
120+
121+
expect(() => ConfigSemanticAnalyzer.parseResourceDependencies([resource1, resource2])).to.throw()
122+
})
123+
124+
it('validates invalid parameters', () => {
125+
const resource1 = createResource1();
126+
const resource2 = createResource7();
127+
128+
expect(() => ConfigSemanticAnalyzer.parseResourceDependencies([resource1, resource2])).to.throw()
129+
})
130+
131+
it('handles multiple resource references', () => {
132+
const resource1 = createResource1();
133+
const resource2 = createResource8();
134+
135+
expect(() => ConfigSemanticAnalyzer.parseResourceDependencies([resource1, resource2])).to.not.throw()
136+
expect(resource1).to.deep.eq(createResource1())
137+
expect(resource2.parameters['directory']).to.eq(`${resource1.parameters.directory} and ${resource1.parameters.directory}`);
138+
})
139+
});

codify-core/src/config-compiler/semantic-analysis/config-semantic-analyzer.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { ResourceDefinitions } from '../../entities/resource-definition';
2+
import { validateResourceParameterType } from '../../utils/validator';
23
import { ConfigBlockType } from '../language-definition';
34
import { ParsedProject } from '../parser/entities';
45
import { ResourceConfig } from '../parser/entities/resource';
56

7+
/* eslint-disable perfectionist/sort-objects */
68
export const ConfigSemanticAnalyzer = {
79

8-
async validate(project: ParsedProject, resourceDefinitions: ResourceDefinitions): Promise<void> {
10+
validate(project: ParsedProject, resourceDefinitions: ResourceDefinitions): void {
911
const resourceConfigs = project
1012
.coreModule
1113
.configBlocks
@@ -30,14 +32,68 @@ export const ConfigSemanticAnalyzer = {
3032
throw new Error(`Invalid resource parameter: ${key}. Resource: ${type}`)
3133
}
3234

35+
if (!validateResourceParameterType(value, parameterDefinitions.get(key)!.type)) {
36+
throw new Error(`Invalid parameter type: ${key}:${value} is not of type ${parameterDefinitions.get(key)!.type}`)
37+
}
38+
3339
const parameter = parameterDefinitions.get(key)!;
3440
// validate value here
3541
if (parameter.allowedValues && !(parameter.allowedValues as Array<unknown>).includes(value)) {
3642
throw new Error(`Invalid resource config ${type}. Allowed values are ${parameter.allowedValues} but ${value} was provided`)
3743
}
3844
}
3945
}
46+
},
47+
48+
49+
parseResourceDependencies(blocks: ResourceConfig[]) {
50+
const resourceReferenceRegex = /\${([\w.]+)}/g
51+
52+
// TODO: Support named resources in the future
53+
const resourceMap = new Map(blocks.map((resource) => [resource.type, resource]));
54+
55+
for (const configBlock of blocks) {
56+
57+
const referenceParameters = Object.entries(configBlock.parameters)
58+
.map(([name, value]) => [name, String(value), String(value).matchAll(resourceReferenceRegex)] as const)
59+
.filter(([, _, match]) => match)
60+
.flatMap(([name, value, matches]) =>
61+
[...matches].map(match => [name, value, match[1]] as const
62+
));
63+
64+
for (const [name, value, match] of referenceParameters) {
65+
const parts = match.split('.');
66+
if (parts.length < 2) {
67+
throw new Error(`Only resource parameter references are allowed. ${match}`);
68+
}
4069

70+
if (!resourceMap.has(parts[0])) {
71+
throw new Error(`Un-able to find resource being referenced. ${match}`);
72+
}
73+
74+
// TODO: Check for circular dependencies
75+
76+
// TODO: Support named resources in the future
77+
const referencedResource = resourceMap.get(parts[0])!;
78+
const referencedParameter = referencedResource.parameters[parts[1]];
79+
80+
if (!referencedParameter) {
81+
throw new Error(`Un-able to find parameter being referenced. ${match}`);
82+
}
83+
84+
// TODO: Add recursive check for parameters of type parameter
85+
86+
configBlock.dependencies.push({
87+
match,
88+
parameterName: name,
89+
parameterValue: value,
90+
resource: referencedResource
91+
})
92+
93+
// Substitute with actual value
94+
configBlock.parameters[name] = String(configBlock.parameters[name]).replace(`\${${match}}`, String(referencedParameter));
95+
}
96+
}
4197
}
4298

4399
};

codify-core/src/utils/validator.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export function validateTypeString(actual: unknown): actual is string {
1717
return typeof actual === 'string';
1818
}
1919

20+
export function validateTypeBoolean(actual: unknown): actual is boolean {
21+
return typeof actual === 'boolean';
22+
}
23+
2024
export function validateTypeArray(actual: unknown): actual is [] {
2125
return Array.isArray(actual);
2226
}
@@ -69,4 +73,24 @@ export function validateAllowedObjectKeys(actual: unknown, allowedKeys: string[]
6973
return validateTypeRecordStringUnknown(actual) && Object.keys(actual).every((k) => keySet.has(k));
7074
}
7175

76+
export function validateResourceParameterType(actual: unknown, type: ResourceParameterType): boolean {
77+
78+
/* eslint-disable unicorn/switch-case-braces */
79+
switch (type) {
80+
case ResourceParameterType.ARRAY:
81+
return validateTypeArray(actual);
82+
case ResourceParameterType.STRING:
83+
return validateTypeString(actual);
84+
case ResourceParameterType.BOOLEAN:
85+
return validateTypeBoolean(actual);
86+
case ResourceParameterType.NUMBER:
87+
return validateTypeNumber(actual);
88+
case ResourceParameterType.OBJECT:
89+
return validateTypeRecordStringUnknown(actual);
90+
default:
91+
return false;
92+
}
93+
/* eslint-enable unicorn/switch-case-braces */
94+
}
95+
7296

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "options",
3+
"parameters": {
4+
"directory": "string"
5+
}
6+
}

0 commit comments

Comments
 (0)