@intentius/chant-lexicon-aws 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +438 -0
- package/package.json +30 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +197 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +1206 -0
- package/src/codegen/extensions.ts +171 -0
- package/src/codegen/fallback.ts +33 -0
- package/src/codegen/generate-cli.ts +17 -0
- package/src/codegen/generate-lexicon.ts +98 -0
- package/src/codegen/generate-typescript.ts +257 -0
- package/src/codegen/generate.test.ts +125 -0
- package/src/codegen/generate.ts +226 -0
- package/src/codegen/idempotency.test.ts +28 -0
- package/src/codegen/naming.ts +120 -0
- package/src/codegen/package.test.ts +60 -0
- package/src/codegen/package.ts +84 -0
- package/src/codegen/patches.ts +98 -0
- package/src/codegen/rollback.test.ts +80 -0
- package/src/codegen/rollback.ts +20 -0
- package/src/codegen/sam.ts +387 -0
- package/src/codegen/snapshot.test.ts +84 -0
- package/src/codegen/typecheck.test.ts +50 -0
- package/src/codegen/typecheck.ts +4 -0
- package/src/codegen/versions.ts +37 -0
- package/src/coverage.ts +14 -0
- package/src/generated/index.d.ts +160753 -0
- package/src/generated/index.ts +14396 -0
- package/src/generated/lexicon-aws.json +114563 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +181 -0
- package/src/import/generator.ts +349 -0
- package/src/import/parser.test.ts +200 -0
- package/src/import/parser.ts +350 -0
- package/src/import/roundtrip-fixtures.test.ts +78 -0
- package/src/import/roundtrip.test.ts +195 -0
- package/src/index.ts +63 -0
- package/src/integration.test.ts +129 -0
- package/src/intrinsics.test.ts +167 -0
- package/src/intrinsics.ts +223 -0
- package/src/lint/post-synth/cf-refs.ts +91 -0
- package/src/lint/post-synth/cor020.ts +72 -0
- package/src/lint/post-synth/ext001.test.ts +68 -0
- package/src/lint/post-synth/ext001.ts +222 -0
- package/src/lint/post-synth/post-synth.test.ts +280 -0
- package/src/lint/post-synth/waw010.ts +49 -0
- package/src/lint/post-synth/waw011.ts +49 -0
- package/src/lint/post-synth/waw013.ts +45 -0
- package/src/lint/post-synth/waw014.ts +50 -0
- package/src/lint/post-synth/waw015.ts +100 -0
- package/src/lint/rules/hardcoded-region.ts +43 -0
- package/src/lint/rules/iam-wildcard.ts +66 -0
- package/src/lint/rules/index.ts +7 -0
- package/src/lint/rules/rules.test.ts +175 -0
- package/src/lint/rules/s3-encryption.ts +69 -0
- package/src/lsp/completions.test.ts +72 -0
- package/src/lsp/completions.ts +18 -0
- package/src/lsp/hover.test.ts +53 -0
- package/src/lsp/hover.ts +53 -0
- package/src/nested-stack.test.ts +83 -0
- package/src/nested-stack.ts +125 -0
- package/src/plugin.test.ts +316 -0
- package/src/plugin.ts +514 -0
- package/src/pseudo.test.ts +55 -0
- package/src/pseudo.ts +29 -0
- package/src/serializer.test.ts +507 -0
- package/src/serializer.ts +333 -0
- package/src/spec/fetch.test.ts +27 -0
- package/src/spec/fetch.ts +107 -0
- package/src/spec/parse.test.ts +153 -0
- package/src/spec/parse.ts +202 -0
- package/src/testdata/load-fixtures.ts +17 -0
- package/src/testdata/roundtrip/conditions.json +21 -0
- package/src/testdata/roundtrip/intrinsic-calls.json +31 -0
- package/src/testdata/roundtrip/intrinsics.json +18 -0
- package/src/testdata/roundtrip/multi-resource.json +37 -0
- package/src/testdata/roundtrip/parameters.json +23 -0
- package/src/testdata/roundtrip/simple.json +12 -0
- package/src/testdata/sam-fixtures/api.yaml +14 -0
- package/src/testdata/sam-fixtures/application.yaml +13 -0
- package/src/testdata/sam-fixtures/function.yaml +22 -0
- package/src/testdata/sam-fixtures/graphql-api.yaml +13 -0
- package/src/testdata/sam-fixtures/http-api.yaml +15 -0
- package/src/testdata/sam-fixtures/layer-version.yaml +15 -0
- package/src/testdata/sam-fixtures/multi-type-a.yaml +23 -0
- package/src/testdata/sam-fixtures/multi-type-b.yaml +29 -0
- package/src/testdata/sam-fixtures/simple-table.yaml +12 -0
- package/src/testdata/sam-fixtures/state-machine.yaml +14 -0
- package/src/testdata/schemas/aws-dynamodb-table.json +126 -0
- package/src/testdata/schemas/aws-iam-role.json +85 -0
- package/src/testdata/schemas/aws-lambda-function.json +90 -0
- package/src/testdata/schemas/aws-s3-bucket.json +83 -0
- package/src/testdata/schemas/aws-sns-topic.json +71 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.ts +34 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { CFParser } from "./parser";
|
|
3
|
+
|
|
4
|
+
describe("CFParser", () => {
|
|
5
|
+
const parser = new CFParser();
|
|
6
|
+
|
|
7
|
+
test("parses empty template", () => {
|
|
8
|
+
const content = JSON.stringify({
|
|
9
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
10
|
+
Resources: {},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const ir = parser.parse(content);
|
|
14
|
+
|
|
15
|
+
expect(ir.parameters).toHaveLength(0);
|
|
16
|
+
expect(ir.resources).toHaveLength(0);
|
|
17
|
+
expect(ir.metadata?.version).toBe("2010-09-09");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("parses parameters", () => {
|
|
21
|
+
const content = JSON.stringify({
|
|
22
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
23
|
+
Parameters: {
|
|
24
|
+
Environment: {
|
|
25
|
+
Type: "String",
|
|
26
|
+
Description: "Environment name",
|
|
27
|
+
Default: "dev",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
Resources: {},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ir = parser.parse(content);
|
|
34
|
+
|
|
35
|
+
expect(ir.parameters).toHaveLength(1);
|
|
36
|
+
expect(ir.parameters[0].name).toBe("Environment");
|
|
37
|
+
expect(ir.parameters[0].type).toBe("String");
|
|
38
|
+
expect(ir.parameters[0].description).toBe("Environment name");
|
|
39
|
+
expect(ir.parameters[0].defaultValue).toBe("dev");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("parses S3 bucket resource", () => {
|
|
43
|
+
const content = JSON.stringify({
|
|
44
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
45
|
+
Resources: {
|
|
46
|
+
MyBucket: {
|
|
47
|
+
Type: "AWS::S3::Bucket",
|
|
48
|
+
Properties: {
|
|
49
|
+
BucketName: "my-bucket",
|
|
50
|
+
VersioningConfiguration: {
|
|
51
|
+
Status: "Enabled",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const ir = parser.parse(content);
|
|
59
|
+
|
|
60
|
+
expect(ir.resources).toHaveLength(1);
|
|
61
|
+
expect(ir.resources[0].logicalId).toBe("MyBucket");
|
|
62
|
+
expect(ir.resources[0].type).toBe("AWS::S3::Bucket");
|
|
63
|
+
expect(ir.resources[0].properties.BucketName).toBe("my-bucket");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("parses Ref intrinsic", () => {
|
|
67
|
+
const content = JSON.stringify({
|
|
68
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
69
|
+
Parameters: {
|
|
70
|
+
BucketName: { Type: "String" },
|
|
71
|
+
},
|
|
72
|
+
Resources: {
|
|
73
|
+
MyBucket: {
|
|
74
|
+
Type: "AWS::S3::Bucket",
|
|
75
|
+
Properties: {
|
|
76
|
+
BucketName: { Ref: "BucketName" },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const ir = parser.parse(content);
|
|
83
|
+
|
|
84
|
+
const nameProp = ir.resources[0].properties.BucketName as Record<string, unknown>;
|
|
85
|
+
expect(nameProp.__intrinsic).toBe("Ref");
|
|
86
|
+
expect(nameProp.name).toBe("BucketName");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("parses Fn::GetAtt array form", () => {
|
|
90
|
+
const content = JSON.stringify({
|
|
91
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
92
|
+
Resources: {
|
|
93
|
+
Source: { Type: "AWS::S3::Bucket", Properties: {} },
|
|
94
|
+
Dest: {
|
|
95
|
+
Type: "AWS::S3::Bucket",
|
|
96
|
+
Properties: {
|
|
97
|
+
SourceArn: { "Fn::GetAtt": ["Source", "Arn"] },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const ir = parser.parse(content);
|
|
104
|
+
|
|
105
|
+
const dest = ir.resources.find((r) => r.logicalId === "Dest");
|
|
106
|
+
const prop = dest?.properties.SourceArn as Record<string, unknown>;
|
|
107
|
+
expect(prop.__intrinsic).toBe("GetAtt");
|
|
108
|
+
expect(prop.logicalId).toBe("Source");
|
|
109
|
+
expect(prop.attribute).toBe("Arn");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("parses Fn::Sub string form", () => {
|
|
113
|
+
const content = JSON.stringify({
|
|
114
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
115
|
+
Resources: {
|
|
116
|
+
MyBucket: {
|
|
117
|
+
Type: "AWS::S3::Bucket",
|
|
118
|
+
Properties: {
|
|
119
|
+
BucketName: { "Fn::Sub": "${AWS::StackName}-bucket" },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const ir = parser.parse(content);
|
|
126
|
+
|
|
127
|
+
const prop = ir.resources[0].properties.BucketName as Record<string, unknown>;
|
|
128
|
+
expect(prop.__intrinsic).toBe("Sub");
|
|
129
|
+
expect(prop.template).toBe("${AWS::StackName}-bucket");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("parses Fn::If", () => {
|
|
133
|
+
const content = JSON.stringify({
|
|
134
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
135
|
+
Resources: {
|
|
136
|
+
MyBucket: {
|
|
137
|
+
Type: "AWS::S3::Bucket",
|
|
138
|
+
Properties: {
|
|
139
|
+
BucketName: {
|
|
140
|
+
"Fn::If": ["CreateProd", "prod-bucket", "dev-bucket"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const ir = parser.parse(content);
|
|
148
|
+
|
|
149
|
+
const prop = ir.resources[0].properties.BucketName as Record<string, unknown>;
|
|
150
|
+
expect(prop.__intrinsic).toBe("If");
|
|
151
|
+
expect(prop.condition).toBe("CreateProd");
|
|
152
|
+
expect(prop.valueIfTrue).toBe("prod-bucket");
|
|
153
|
+
expect(prop.valueIfFalse).toBe("dev-bucket");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("parses Fn::Join", () => {
|
|
157
|
+
const content = JSON.stringify({
|
|
158
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
159
|
+
Resources: {
|
|
160
|
+
MyBucket: {
|
|
161
|
+
Type: "AWS::S3::Bucket",
|
|
162
|
+
Properties: {
|
|
163
|
+
BucketName: { "Fn::Join": ["-", ["my", "bucket"]] },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const ir = parser.parse(content);
|
|
170
|
+
|
|
171
|
+
const prop = ir.resources[0].properties.BucketName as Record<string, unknown>;
|
|
172
|
+
expect(prop.__intrinsic).toBe("Join");
|
|
173
|
+
expect(prop.delimiter).toBe("-");
|
|
174
|
+
expect(prop.values).toEqual(["my", "bucket"]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("parses nested properties", () => {
|
|
178
|
+
const content = JSON.stringify({
|
|
179
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
180
|
+
Resources: {
|
|
181
|
+
MyFunction: {
|
|
182
|
+
Type: "AWS::Lambda::Function",
|
|
183
|
+
Properties: {
|
|
184
|
+
Environment: {
|
|
185
|
+
Variables: {
|
|
186
|
+
KEY: "value",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const ir = parser.parse(content);
|
|
195
|
+
|
|
196
|
+
const env = ir.resources[0].properties.Environment as Record<string, unknown>;
|
|
197
|
+
const vars = env.Variables as Record<string, unknown>;
|
|
198
|
+
expect(vars.KEY).toBe("value");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TemplateParser,
|
|
3
|
+
TemplateIR,
|
|
4
|
+
ResourceIR,
|
|
5
|
+
ParameterIR,
|
|
6
|
+
} from "@intentius/chant/import/parser";
|
|
7
|
+
import { BaseValueParser } from "@intentius/chant/import/base-parser";
|
|
8
|
+
import yaml from "js-yaml";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Custom YAML schema for CloudFormation shorthand tags (!Ref, !Sub, !GetAtt, etc.)
|
|
12
|
+
*/
|
|
13
|
+
const cfnYamlTypes = [
|
|
14
|
+
new yaml.Type("!Ref", {
|
|
15
|
+
kind: "scalar",
|
|
16
|
+
construct: (data: string) => ({ Ref: data }),
|
|
17
|
+
}),
|
|
18
|
+
new yaml.Type("!Sub", {
|
|
19
|
+
kind: "scalar",
|
|
20
|
+
construct: (data: string) => ({ "Fn::Sub": data }),
|
|
21
|
+
}),
|
|
22
|
+
new yaml.Type("!Sub", {
|
|
23
|
+
kind: "sequence",
|
|
24
|
+
construct: (data: unknown[]) => ({ "Fn::Sub": data }),
|
|
25
|
+
}),
|
|
26
|
+
new yaml.Type("!GetAtt", {
|
|
27
|
+
kind: "scalar",
|
|
28
|
+
construct: (data: string) => ({ "Fn::GetAtt": data.split(".") }),
|
|
29
|
+
}),
|
|
30
|
+
new yaml.Type("!GetAtt", {
|
|
31
|
+
kind: "sequence",
|
|
32
|
+
construct: (data: unknown[]) => ({ "Fn::GetAtt": data }),
|
|
33
|
+
}),
|
|
34
|
+
new yaml.Type("!Join", {
|
|
35
|
+
kind: "sequence",
|
|
36
|
+
construct: (data: unknown[]) => ({ "Fn::Join": data }),
|
|
37
|
+
}),
|
|
38
|
+
new yaml.Type("!Select", {
|
|
39
|
+
kind: "sequence",
|
|
40
|
+
construct: (data: unknown[]) => ({ "Fn::Select": data }),
|
|
41
|
+
}),
|
|
42
|
+
new yaml.Type("!Split", {
|
|
43
|
+
kind: "sequence",
|
|
44
|
+
construct: (data: unknown[]) => ({ "Fn::Split": data }),
|
|
45
|
+
}),
|
|
46
|
+
new yaml.Type("!If", {
|
|
47
|
+
kind: "sequence",
|
|
48
|
+
construct: (data: unknown[]) => ({ "Fn::If": data }),
|
|
49
|
+
}),
|
|
50
|
+
new yaml.Type("!Equals", {
|
|
51
|
+
kind: "sequence",
|
|
52
|
+
construct: (data: unknown[]) => ({ "Fn::Equals": data }),
|
|
53
|
+
}),
|
|
54
|
+
new yaml.Type("!Not", {
|
|
55
|
+
kind: "sequence",
|
|
56
|
+
construct: (data: unknown[]) => ({ "Fn::Not": data }),
|
|
57
|
+
}),
|
|
58
|
+
new yaml.Type("!And", {
|
|
59
|
+
kind: "sequence",
|
|
60
|
+
construct: (data: unknown[]) => ({ "Fn::And": data }),
|
|
61
|
+
}),
|
|
62
|
+
new yaml.Type("!Or", {
|
|
63
|
+
kind: "sequence",
|
|
64
|
+
construct: (data: unknown[]) => ({ "Fn::Or": data }),
|
|
65
|
+
}),
|
|
66
|
+
new yaml.Type("!FindInMap", {
|
|
67
|
+
kind: "sequence",
|
|
68
|
+
construct: (data: unknown[]) => ({ "Fn::FindInMap": data }),
|
|
69
|
+
}),
|
|
70
|
+
new yaml.Type("!Base64", {
|
|
71
|
+
kind: "scalar",
|
|
72
|
+
construct: (data: string) => ({ "Fn::Base64": data }),
|
|
73
|
+
}),
|
|
74
|
+
new yaml.Type("!Base64", {
|
|
75
|
+
kind: "mapping",
|
|
76
|
+
construct: (data: unknown) => ({ "Fn::Base64": data }),
|
|
77
|
+
}),
|
|
78
|
+
new yaml.Type("!Cidr", {
|
|
79
|
+
kind: "sequence",
|
|
80
|
+
construct: (data: unknown[]) => ({ "Fn::Cidr": data }),
|
|
81
|
+
}),
|
|
82
|
+
new yaml.Type("!ImportValue", {
|
|
83
|
+
kind: "scalar",
|
|
84
|
+
construct: (data: string) => ({ "Fn::ImportValue": data }),
|
|
85
|
+
}),
|
|
86
|
+
new yaml.Type("!ImportValue", {
|
|
87
|
+
kind: "mapping",
|
|
88
|
+
construct: (data: unknown) => ({ "Fn::ImportValue": data }),
|
|
89
|
+
}),
|
|
90
|
+
new yaml.Type("!GetAZs", {
|
|
91
|
+
kind: "scalar",
|
|
92
|
+
construct: (data: string) => ({ "Fn::GetAZs": data }),
|
|
93
|
+
}),
|
|
94
|
+
new yaml.Type("!GetAZs", {
|
|
95
|
+
kind: "mapping",
|
|
96
|
+
construct: (data: unknown) => ({ "Fn::GetAZs": data }),
|
|
97
|
+
}),
|
|
98
|
+
new yaml.Type("!Transform", {
|
|
99
|
+
kind: "mapping",
|
|
100
|
+
construct: (data: unknown) => ({ "Fn::Transform": data }),
|
|
101
|
+
}),
|
|
102
|
+
new yaml.Type("!Condition", {
|
|
103
|
+
kind: "scalar",
|
|
104
|
+
construct: (data: string) => ({ Condition: data }),
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const CF_SCHEMA = yaml.DEFAULT_SCHEMA.extend(cfnYamlTypes);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* CloudFormation template structure
|
|
112
|
+
*/
|
|
113
|
+
interface CFTemplate {
|
|
114
|
+
AWSTemplateFormatVersion?: string;
|
|
115
|
+
Description?: string;
|
|
116
|
+
Parameters?: Record<string, CFParameter>;
|
|
117
|
+
Resources?: Record<string, CFResource>;
|
|
118
|
+
Outputs?: Record<string, unknown>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* CloudFormation parameter
|
|
123
|
+
*/
|
|
124
|
+
interface CFParameter {
|
|
125
|
+
Type: string;
|
|
126
|
+
Description?: string;
|
|
127
|
+
Default?: unknown;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* CloudFormation resource
|
|
132
|
+
*/
|
|
133
|
+
interface CFResource {
|
|
134
|
+
Type: string;
|
|
135
|
+
Properties?: Record<string, unknown>;
|
|
136
|
+
Metadata?: Record<string, unknown>;
|
|
137
|
+
DependsOn?: string | string[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parser for CloudFormation JSON templates.
|
|
142
|
+
* Extends BaseValueParser for generic recursive value walking;
|
|
143
|
+
* overrides dispatchIntrinsic with the CFN-specific dispatch table.
|
|
144
|
+
*/
|
|
145
|
+
export class CFParser extends BaseValueParser implements TemplateParser {
|
|
146
|
+
/**
|
|
147
|
+
* Parse CF JSON content into intermediate representation
|
|
148
|
+
*/
|
|
149
|
+
parse(content: string): TemplateIR {
|
|
150
|
+
let parsed: unknown;
|
|
151
|
+
try {
|
|
152
|
+
parsed = JSON.parse(content);
|
|
153
|
+
} catch {
|
|
154
|
+
parsed = yaml.load(content, { schema: CF_SCHEMA });
|
|
155
|
+
}
|
|
156
|
+
const template = parsed as CFTemplate;
|
|
157
|
+
|
|
158
|
+
const parameters = this.parseParameters(template.Parameters ?? {});
|
|
159
|
+
const resources = this.parseResources(template.Resources ?? {});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
parameters,
|
|
163
|
+
resources,
|
|
164
|
+
metadata: {
|
|
165
|
+
version: template.AWSTemplateFormatVersion ?? "2010-09-09",
|
|
166
|
+
description: template.Description,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse parameters section
|
|
173
|
+
*/
|
|
174
|
+
private parseParameters(params: Record<string, CFParameter>): ParameterIR[] {
|
|
175
|
+
return Object.entries(params).map(([name, param]) => ({
|
|
176
|
+
name,
|
|
177
|
+
type: param.Type,
|
|
178
|
+
description: param.Description,
|
|
179
|
+
defaultValue: param.Default,
|
|
180
|
+
required: param.Default === undefined,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse resources section
|
|
186
|
+
*/
|
|
187
|
+
private parseResources(resources: Record<string, CFResource>): ResourceIR[] {
|
|
188
|
+
return Object.entries(resources)
|
|
189
|
+
.filter(([_, resource]) => typeof resource?.Type === "string")
|
|
190
|
+
.map(([logicalId, resource]) => ({
|
|
191
|
+
logicalId,
|
|
192
|
+
type: resource.Type,
|
|
193
|
+
properties: this.parseProperties(resource.Properties ?? {}),
|
|
194
|
+
metadata: resource.Metadata,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Parse resource properties, handling intrinsic functions
|
|
200
|
+
*/
|
|
201
|
+
private parseProperties(props: Record<string, unknown>): Record<string, unknown> {
|
|
202
|
+
const result: Record<string, unknown> = {};
|
|
203
|
+
|
|
204
|
+
for (const [key, value] of Object.entries(props)) {
|
|
205
|
+
result[key] = this.parseValue(value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* CFN-specific intrinsic dispatch table.
|
|
213
|
+
*/
|
|
214
|
+
protected dispatchIntrinsic(key: string, value: unknown, _obj: Record<string, unknown>): unknown | null {
|
|
215
|
+
if (key === "Ref") {
|
|
216
|
+
return { __intrinsic: "Ref", name: value };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (key === "Fn::GetAtt") {
|
|
220
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
221
|
+
return { __intrinsic: "GetAtt", logicalId: value[0], attribute: value[1] };
|
|
222
|
+
}
|
|
223
|
+
if (typeof value === "string") {
|
|
224
|
+
const [logicalId, attribute] = value.split(".");
|
|
225
|
+
return { __intrinsic: "GetAtt", logicalId, attribute };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (key === "Fn::Sub") {
|
|
230
|
+
if (typeof value === "string") {
|
|
231
|
+
return { __intrinsic: "Sub", template: value };
|
|
232
|
+
}
|
|
233
|
+
if (Array.isArray(value) && value.length >= 1) {
|
|
234
|
+
return { __intrinsic: "Sub", template: value[0], variables: value[1] };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (key === "Fn::If") {
|
|
239
|
+
const ifValue = value as unknown[];
|
|
240
|
+
return {
|
|
241
|
+
__intrinsic: "If",
|
|
242
|
+
condition: ifValue[0],
|
|
243
|
+
valueIfTrue: this.parseValue(ifValue[1]),
|
|
244
|
+
valueIfFalse: this.parseValue(ifValue[2]),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (key === "Fn::Join") {
|
|
249
|
+
const joinValue = value as [string, unknown];
|
|
250
|
+
const delimiter = joinValue[0];
|
|
251
|
+
const source = joinValue[1];
|
|
252
|
+
return {
|
|
253
|
+
__intrinsic: "Join",
|
|
254
|
+
delimiter,
|
|
255
|
+
values: Array.isArray(source)
|
|
256
|
+
? source.map((v) => this.parseValue(v))
|
|
257
|
+
: [this.parseValue(source)],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (key === "Fn::Select") {
|
|
262
|
+
const selectValue = value as [string | number, unknown];
|
|
263
|
+
const index = Number(selectValue[0]);
|
|
264
|
+
const source = selectValue[1];
|
|
265
|
+
if (Array.isArray(source)) {
|
|
266
|
+
return {
|
|
267
|
+
__intrinsic: "Select",
|
|
268
|
+
index,
|
|
269
|
+
values: source.map((v) => this.parseValue(v)),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
__intrinsic: "Select",
|
|
274
|
+
index,
|
|
275
|
+
values: [this.parseValue(source)],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (key === "Fn::Split") {
|
|
280
|
+
const splitValue = value as [string, unknown];
|
|
281
|
+
return {
|
|
282
|
+
__intrinsic: "Split",
|
|
283
|
+
delimiter: splitValue[0],
|
|
284
|
+
source: this.parseValue(splitValue[1]),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (key === "Fn::Base64") {
|
|
289
|
+
return { __intrinsic: "Base64", value: this.parseValue(value) };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (key === "Fn::FindInMap") {
|
|
293
|
+
const mapValue = value as unknown[];
|
|
294
|
+
return {
|
|
295
|
+
__intrinsic: "FindInMap",
|
|
296
|
+
mapName: mapValue[0],
|
|
297
|
+
firstKey: this.parseValue(mapValue[1]),
|
|
298
|
+
secondKey: this.parseValue(mapValue[2]),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (key === "Fn::GetAZs") {
|
|
303
|
+
return { __intrinsic: "GetAZs", region: this.parseValue(value) };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (key === "Fn::ImportValue") {
|
|
307
|
+
return { __intrinsic: "ImportValue", value: this.parseValue(value) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (key === "Fn::Cidr") {
|
|
311
|
+
const cidrValue = value as unknown[];
|
|
312
|
+
return {
|
|
313
|
+
__intrinsic: "Cidr",
|
|
314
|
+
ipBlock: this.parseValue(cidrValue[0]),
|
|
315
|
+
count: this.parseValue(cidrValue[1]),
|
|
316
|
+
cidrBits: this.parseValue(cidrValue[2]),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (key === "Fn::Transform") {
|
|
321
|
+
return { __intrinsic: "Transform", value };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (key === "Fn::Equals") {
|
|
325
|
+
const eqValue = value as unknown[];
|
|
326
|
+
return {
|
|
327
|
+
__intrinsic: "Equals",
|
|
328
|
+
left: this.parseValue(eqValue[0]),
|
|
329
|
+
right: this.parseValue(eqValue[1]),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (key === "Fn::Not") {
|
|
334
|
+
const notValue = value as unknown[];
|
|
335
|
+
return { __intrinsic: "Not", condition: this.parseValue(notValue[0]) };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (key === "Fn::And") {
|
|
339
|
+
const andValue = value as unknown[];
|
|
340
|
+
return { __intrinsic: "And", conditions: andValue.map((v) => this.parseValue(v)) };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (key === "Fn::Or") {
|
|
344
|
+
const orValue = value as unknown[];
|
|
345
|
+
return { __intrinsic: "Or", conditions: orValue.map((v) => this.parseValue(v)) };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { CFParser } from "./parser";
|
|
5
|
+
import { CFGenerator } from "./generator";
|
|
6
|
+
|
|
7
|
+
const parser = new CFParser();
|
|
8
|
+
const generator = new CFGenerator();
|
|
9
|
+
|
|
10
|
+
const roundtripDir = join(import.meta.dir, "../testdata/roundtrip");
|
|
11
|
+
const samDir = join(import.meta.dir, "../testdata/sam-fixtures");
|
|
12
|
+
|
|
13
|
+
describe("CF roundtrip fixtures", () => {
|
|
14
|
+
const fixtures = readdirSync(roundtripDir).filter((f) => f.endsWith(".json"));
|
|
15
|
+
|
|
16
|
+
for (const fixture of fixtures) {
|
|
17
|
+
test(`roundtrip: ${fixture}`, () => {
|
|
18
|
+
const content = readFileSync(join(roundtripDir, fixture), "utf-8");
|
|
19
|
+
const template = JSON.parse(content);
|
|
20
|
+
|
|
21
|
+
const ir = parser.parse(content);
|
|
22
|
+
const files = generator.generate(ir);
|
|
23
|
+
|
|
24
|
+
// Verify at least one file was generated
|
|
25
|
+
expect(files.length).toBeGreaterThanOrEqual(1);
|
|
26
|
+
|
|
27
|
+
// Verify barrel/main file exists
|
|
28
|
+
const hasBarrel = files.some(
|
|
29
|
+
(f) => f.path === "main.ts" || f.path === "_.ts",
|
|
30
|
+
);
|
|
31
|
+
expect(hasBarrel).toBe(true);
|
|
32
|
+
|
|
33
|
+
// Verify resource count: each resource should be represented in generated output
|
|
34
|
+
const resourceNames = Object.keys(template.Resources ?? {});
|
|
35
|
+
const mainFile = files.find((f) => f.path === "main.ts" || f.path === "_.ts");
|
|
36
|
+
expect(mainFile).toBeDefined();
|
|
37
|
+
|
|
38
|
+
for (const name of resourceNames) {
|
|
39
|
+
const varName = name.charAt(0).toLowerCase() + name.slice(1);
|
|
40
|
+
expect(mainFile!.content).toContain(varName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If parameters exist, verify they appear in the output
|
|
44
|
+
const paramNames = Object.keys(template.Parameters ?? {});
|
|
45
|
+
for (const name of paramNames) {
|
|
46
|
+
const varName = name.charAt(0).toLowerCase() + name.slice(1);
|
|
47
|
+
expect(mainFile!.content).toContain(varName);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("SAM roundtrip fixtures", () => {
|
|
54
|
+
const fixtures = readdirSync(samDir).filter(
|
|
55
|
+
(f) => f.endsWith(".yaml") || f.endsWith(".yml"),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
for (const fixture of fixtures) {
|
|
59
|
+
test(`SAM roundtrip: ${fixture}`, () => {
|
|
60
|
+
const content = readFileSync(join(samDir, fixture), "utf-8");
|
|
61
|
+
|
|
62
|
+
// Parse should not throw (YAML support)
|
|
63
|
+
const ir = parser.parse(content);
|
|
64
|
+
|
|
65
|
+
// Generate should produce output
|
|
66
|
+
const files = generator.generate(ir);
|
|
67
|
+
|
|
68
|
+
// Verify at least one .ts file was generated
|
|
69
|
+
expect(files.length).toBeGreaterThanOrEqual(1);
|
|
70
|
+
expect(files.some((f) => f.path.endsWith(".ts"))).toBe(true);
|
|
71
|
+
|
|
72
|
+
// Verify no empty content
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
expect(file.content.length).toBeGreaterThan(0);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|