@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.
Files changed (94) hide show
  1. package/README.md +438 -0
  2. package/package.json +30 -0
  3. package/src/codegen/__snapshots__/snapshot.test.ts.snap +197 -0
  4. package/src/codegen/docs-cli.ts +3 -0
  5. package/src/codegen/docs.ts +1206 -0
  6. package/src/codegen/extensions.ts +171 -0
  7. package/src/codegen/fallback.ts +33 -0
  8. package/src/codegen/generate-cli.ts +17 -0
  9. package/src/codegen/generate-lexicon.ts +98 -0
  10. package/src/codegen/generate-typescript.ts +257 -0
  11. package/src/codegen/generate.test.ts +125 -0
  12. package/src/codegen/generate.ts +226 -0
  13. package/src/codegen/idempotency.test.ts +28 -0
  14. package/src/codegen/naming.ts +120 -0
  15. package/src/codegen/package.test.ts +60 -0
  16. package/src/codegen/package.ts +84 -0
  17. package/src/codegen/patches.ts +98 -0
  18. package/src/codegen/rollback.test.ts +80 -0
  19. package/src/codegen/rollback.ts +20 -0
  20. package/src/codegen/sam.ts +387 -0
  21. package/src/codegen/snapshot.test.ts +84 -0
  22. package/src/codegen/typecheck.test.ts +50 -0
  23. package/src/codegen/typecheck.ts +4 -0
  24. package/src/codegen/versions.ts +37 -0
  25. package/src/coverage.ts +14 -0
  26. package/src/generated/index.d.ts +160753 -0
  27. package/src/generated/index.ts +14396 -0
  28. package/src/generated/lexicon-aws.json +114563 -0
  29. package/src/generated/runtime.ts +4 -0
  30. package/src/import/generator.test.ts +181 -0
  31. package/src/import/generator.ts +349 -0
  32. package/src/import/parser.test.ts +200 -0
  33. package/src/import/parser.ts +350 -0
  34. package/src/import/roundtrip-fixtures.test.ts +78 -0
  35. package/src/import/roundtrip.test.ts +195 -0
  36. package/src/index.ts +63 -0
  37. package/src/integration.test.ts +129 -0
  38. package/src/intrinsics.test.ts +167 -0
  39. package/src/intrinsics.ts +223 -0
  40. package/src/lint/post-synth/cf-refs.ts +91 -0
  41. package/src/lint/post-synth/cor020.ts +72 -0
  42. package/src/lint/post-synth/ext001.test.ts +68 -0
  43. package/src/lint/post-synth/ext001.ts +222 -0
  44. package/src/lint/post-synth/post-synth.test.ts +280 -0
  45. package/src/lint/post-synth/waw010.ts +49 -0
  46. package/src/lint/post-synth/waw011.ts +49 -0
  47. package/src/lint/post-synth/waw013.ts +45 -0
  48. package/src/lint/post-synth/waw014.ts +50 -0
  49. package/src/lint/post-synth/waw015.ts +100 -0
  50. package/src/lint/rules/hardcoded-region.ts +43 -0
  51. package/src/lint/rules/iam-wildcard.ts +66 -0
  52. package/src/lint/rules/index.ts +7 -0
  53. package/src/lint/rules/rules.test.ts +175 -0
  54. package/src/lint/rules/s3-encryption.ts +69 -0
  55. package/src/lsp/completions.test.ts +72 -0
  56. package/src/lsp/completions.ts +18 -0
  57. package/src/lsp/hover.test.ts +53 -0
  58. package/src/lsp/hover.ts +53 -0
  59. package/src/nested-stack.test.ts +83 -0
  60. package/src/nested-stack.ts +125 -0
  61. package/src/plugin.test.ts +316 -0
  62. package/src/plugin.ts +514 -0
  63. package/src/pseudo.test.ts +55 -0
  64. package/src/pseudo.ts +29 -0
  65. package/src/serializer.test.ts +507 -0
  66. package/src/serializer.ts +333 -0
  67. package/src/spec/fetch.test.ts +27 -0
  68. package/src/spec/fetch.ts +107 -0
  69. package/src/spec/parse.test.ts +153 -0
  70. package/src/spec/parse.ts +202 -0
  71. package/src/testdata/load-fixtures.ts +17 -0
  72. package/src/testdata/roundtrip/conditions.json +21 -0
  73. package/src/testdata/roundtrip/intrinsic-calls.json +31 -0
  74. package/src/testdata/roundtrip/intrinsics.json +18 -0
  75. package/src/testdata/roundtrip/multi-resource.json +37 -0
  76. package/src/testdata/roundtrip/parameters.json +23 -0
  77. package/src/testdata/roundtrip/simple.json +12 -0
  78. package/src/testdata/sam-fixtures/api.yaml +14 -0
  79. package/src/testdata/sam-fixtures/application.yaml +13 -0
  80. package/src/testdata/sam-fixtures/function.yaml +22 -0
  81. package/src/testdata/sam-fixtures/graphql-api.yaml +13 -0
  82. package/src/testdata/sam-fixtures/http-api.yaml +15 -0
  83. package/src/testdata/sam-fixtures/layer-version.yaml +15 -0
  84. package/src/testdata/sam-fixtures/multi-type-a.yaml +23 -0
  85. package/src/testdata/sam-fixtures/multi-type-b.yaml +29 -0
  86. package/src/testdata/sam-fixtures/simple-table.yaml +12 -0
  87. package/src/testdata/sam-fixtures/state-machine.yaml +14 -0
  88. package/src/testdata/schemas/aws-dynamodb-table.json +126 -0
  89. package/src/testdata/schemas/aws-iam-role.json +85 -0
  90. package/src/testdata/schemas/aws-lambda-function.json +90 -0
  91. package/src/testdata/schemas/aws-s3-bucket.json +83 -0
  92. package/src/testdata/schemas/aws-sns-topic.json +71 -0
  93. package/src/validate-cli.ts +19 -0
  94. package/src/validate.ts +34 -0
@@ -0,0 +1,333 @@
1
+ import type { Declarable, CoreParameter } from "@intentius/chant/declarable";
2
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
3
+ import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
4
+ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
5
+ import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
6
+ import { toPascalCase } from "@intentius/chant/codegen/case";
7
+ import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
8
+ import { isStackOutput, type StackOutput } from "@intentius/chant/stack-output";
9
+
10
+ /**
11
+ * Check if a declarable is a CoreParameter
12
+ */
13
+ function isCoreParameter(entity: Declarable): entity is CoreParameter {
14
+ return "parameterType" in entity;
15
+ }
16
+
17
+ /**
18
+ * CloudFormation template structure
19
+ */
20
+ interface CFTemplate {
21
+ AWSTemplateFormatVersion: "2010-09-09";
22
+ Description?: string;
23
+ Parameters?: Record<string, CFParameter>;
24
+ Resources: Record<string, CFResource>;
25
+ Outputs?: Record<string, CFOutput>;
26
+ }
27
+
28
+ /**
29
+ * CloudFormation parameter
30
+ */
31
+ interface CFParameter {
32
+ Type: string;
33
+ Description?: string;
34
+ Default?: unknown;
35
+ AllowedValues?: unknown[];
36
+ AllowedPattern?: string;
37
+ ConstraintDescription?: string;
38
+ MaxLength?: number;
39
+ MaxValue?: number;
40
+ MinLength?: number;
41
+ MinValue?: number;
42
+ NoEcho?: boolean;
43
+ }
44
+
45
+ /**
46
+ * CloudFormation resource
47
+ */
48
+ interface CFResource {
49
+ Type: string;
50
+ Properties?: Record<string, unknown>;
51
+ DependsOn?: string | string[];
52
+ Condition?: string;
53
+ Metadata?: Record<string, unknown>;
54
+ }
55
+
56
+ /**
57
+ * CloudFormation output
58
+ */
59
+ interface CFOutput {
60
+ Value: unknown;
61
+ Description?: string;
62
+ Export?: { Name: unknown };
63
+ Condition?: string;
64
+ }
65
+
66
+ /**
67
+ * CloudFormation-specific visitor for the generic serializer walker.
68
+ */
69
+ function cfnVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
70
+ return {
71
+ attrRef: (name, attr) => ({ "Fn::GetAttr": [name, attr] }),
72
+ resourceRef: (name) => ({ Ref: name }),
73
+ propertyDeclarable: (entity, walk) => {
74
+ if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
75
+ return undefined;
76
+ }
77
+ const props = entity.props as Record<string, unknown>;
78
+ const cfProps: Record<string, unknown> = {};
79
+ for (const [key, value] of Object.entries(props)) {
80
+ if (value !== undefined) {
81
+ const cfKey = toPascalCase(key);
82
+ cfProps[cfKey] = walk(value);
83
+ }
84
+ }
85
+ return Object.keys(cfProps).length > 0 ? cfProps : undefined;
86
+ },
87
+ transformKey: toPascalCase,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Convert a value to CF-compatible JSON using the generic walker.
93
+ */
94
+ function toCFValue(value: unknown, entityNames: Map<Declarable, string>, convertKeys = false): unknown {
95
+ const visitor = cfnVisitor(entityNames);
96
+ if (!convertKeys) {
97
+ // When not converting keys, use a visitor without transformKey
98
+ return walkValue(value, entityNames, { ...visitor, transformKey: undefined });
99
+ }
100
+ return walkValue(value, entityNames, visitor);
101
+ }
102
+
103
+ /**
104
+ * Convert entity props to CF properties
105
+ */
106
+ function toProperties(
107
+ entity: Declarable,
108
+ entityNames: Map<Declarable, string>
109
+ ): Record<string, unknown> | undefined {
110
+ if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
111
+ return undefined;
112
+ }
113
+
114
+ const props = entity.props as Record<string, unknown>;
115
+ const cfProps: Record<string, unknown> = {};
116
+
117
+ for (const [key, value] of Object.entries(props)) {
118
+ if (value !== undefined) {
119
+ const cfKey = toPascalCase(key);
120
+ cfProps[cfKey] = toCFValue(value, entityNames, true);
121
+ }
122
+ }
123
+
124
+ return Object.keys(cfProps).length > 0 ? cfProps : undefined;
125
+ }
126
+
127
+
128
+ /**
129
+ * Serialize a set of entities into a CFTemplate object (without JSON.stringify).
130
+ */
131
+ function serializeToTemplate(
132
+ entities: Map<string, Declarable>,
133
+ outputs?: LexiconOutput[],
134
+ extraParameters?: Record<string, CFParameter>,
135
+ extraOutputs?: Record<string, CFOutput>,
136
+ ): CFTemplate {
137
+ const template: CFTemplate = {
138
+ AWSTemplateFormatVersion: "2010-09-09",
139
+ Resources: {},
140
+ };
141
+
142
+ // Add extra parameters (e.g. TemplateBasePath)
143
+ if (extraParameters && Object.keys(extraParameters).length > 0) {
144
+ template.Parameters = { ...extraParameters };
145
+ }
146
+
147
+ // Build reverse map: entity -> name
148
+ const entityNames = new Map<Declarable, string>();
149
+ for (const [name, entity] of entities) {
150
+ entityNames.set(entity, name);
151
+ }
152
+
153
+ // Process entities
154
+ for (const [name, entity] of entities) {
155
+ // Skip StackOutput entities — they go in the Outputs section
156
+ if (isStackOutput(entity)) {
157
+ continue;
158
+ }
159
+
160
+ if (isCoreParameter(entity)) {
161
+ if (!template.Parameters) {
162
+ template.Parameters = {};
163
+ }
164
+
165
+ const param: CFParameter = {
166
+ Type: entity.parameterType,
167
+ };
168
+
169
+ if ("description" in entity && typeof entity.description === "string") {
170
+ param.Description = entity.description;
171
+ }
172
+
173
+ if ("defaultValue" in entity && entity.defaultValue !== undefined) {
174
+ param.Default = entity.defaultValue;
175
+ }
176
+
177
+ template.Parameters[name] = param;
178
+ } else if (isChildProject(entity)) {
179
+ // ChildProjectInstance → AWS::CloudFormation::Stack resource
180
+ const childProject = entity as ChildProjectInstance;
181
+ const childName = childProject.logicalName;
182
+ const filename = `${childName}.template.json`;
183
+
184
+ const properties: Record<string, unknown> = {
185
+ TemplateURL: {
186
+ "Fn::Sub": `\${TemplateBasePath}/${filename}`,
187
+ },
188
+ };
189
+
190
+ // Build parameters: always pass TemplateBasePath down
191
+ const parameters: Record<string, unknown> = {
192
+ TemplateBasePath: { Ref: "TemplateBasePath" },
193
+ };
194
+
195
+ // Add user-specified parameters
196
+ const opts = childProject.options as { parameters?: Record<string, unknown> };
197
+ if (opts.parameters) {
198
+ for (const [key, value] of Object.entries(opts.parameters)) {
199
+ parameters[key] = value;
200
+ }
201
+ }
202
+
203
+ properties.Parameters = parameters;
204
+
205
+ template.Resources[name] = {
206
+ Type: "AWS::CloudFormation::Stack",
207
+ Properties: properties,
208
+ };
209
+ } else if (!isPropertyDeclarable(entity)) {
210
+ const resource: CFResource = {
211
+ Type: entity.entityType,
212
+ };
213
+
214
+ const properties = toProperties(entity, entityNames);
215
+ if (properties) {
216
+ resource.Properties = properties;
217
+ }
218
+
219
+ template.Resources[name] = resource;
220
+ }
221
+ }
222
+
223
+ // Emit StackOutput entities as CF Outputs
224
+ for (const [name, entity] of entities) {
225
+ if (isStackOutput(entity)) {
226
+ if (!template.Outputs) {
227
+ template.Outputs = {};
228
+ }
229
+ const stackOutput = entity as StackOutput;
230
+ const ref = stackOutput.sourceRef;
231
+ const logicalName = ref.getLogicalName();
232
+ if (logicalName) {
233
+ const output: CFOutput = {
234
+ Value: { "Fn::GetAttr": [logicalName, ref.attribute] },
235
+ };
236
+ if (stackOutput.description) {
237
+ output.Description = stackOutput.description;
238
+ }
239
+ template.Outputs[name] = output;
240
+ }
241
+ }
242
+ }
243
+
244
+ // Add CF Outputs for LexiconOutputs produced by this lexicon
245
+ if (outputs && outputs.length > 0) {
246
+ template.Outputs = template.Outputs ?? {};
247
+ for (const output of outputs) {
248
+ template.Outputs[output.outputName] = {
249
+ Value: {
250
+ "Fn::GetAttr": [output.sourceEntity, output.sourceAttribute],
251
+ },
252
+ };
253
+ }
254
+ }
255
+
256
+ // Add extra outputs (e.g. auto-wired cross-stack refs)
257
+ if (extraOutputs && Object.keys(extraOutputs).length > 0) {
258
+ template.Outputs = { ...template.Outputs, ...extraOutputs };
259
+ }
260
+
261
+ return template;
262
+ }
263
+
264
+ /**
265
+ * AWS CloudFormation serializer implementation
266
+ */
267
+ export const awsSerializer: Serializer = {
268
+ name: "aws",
269
+ rulePrefix: "WAW",
270
+
271
+ serialize(entities: Map<string, Declarable>, outputs?: LexiconOutput[]): string | SerializerResult {
272
+ // Check if any entities are child projects (nested stacks)
273
+ const childProjects = new Map<string, ChildProjectInstance>();
274
+ let hasChildProjects = false;
275
+
276
+ for (const [name, entity] of entities) {
277
+ if (isChildProject(entity)) {
278
+ childProjects.set(name, entity as ChildProjectInstance);
279
+ hasChildProjects = true;
280
+ }
281
+ }
282
+
283
+ // No nested stacks — use the simple path
284
+ if (!hasChildProjects) {
285
+ const template = serializeToTemplate(entities, outputs);
286
+ return JSON.stringify(template, null, 2);
287
+ }
288
+
289
+ // Has child projects — produce multi-file output
290
+ const allFiles: Record<string, string> = {};
291
+
292
+ // Add TemplateBasePath parameter to the parent template
293
+ const parentParams: Record<string, CFParameter> = {
294
+ TemplateBasePath: {
295
+ Type: "String",
296
+ Default: ".",
297
+ Description: "Base URL/path for nested stack templates",
298
+ },
299
+ };
300
+
301
+ // Collect child template files from build results
302
+ for (const [, childProject] of childProjects) {
303
+ if (childProject.buildResult) {
304
+ const childOutput = childProject.buildResult.outputs.get("aws");
305
+ if (childOutput) {
306
+ const childName = childProject.logicalName;
307
+ const filename = `${childName}.template.json`;
308
+
309
+ if (typeof childOutput === "string") {
310
+ allFiles[filename] = childOutput;
311
+ } else {
312
+ // SerializerResult — the child itself has child templates
313
+ allFiles[filename] = childOutput.primary;
314
+ if (childOutput.files) {
315
+ for (const [childFile, content] of Object.entries(childOutput.files)) {
316
+ allFiles[childFile] = content;
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ // Serialize the parent template (ChildProjectInstance entities become CF::Stack resources)
325
+ const parentTemplate = serializeToTemplate(entities, outputs, parentParams);
326
+ const primary = JSON.stringify(parentTemplate, null, 2);
327
+
328
+ return {
329
+ primary,
330
+ files: allFiles,
331
+ };
332
+ },
333
+ };
@@ -0,0 +1,27 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { fetchSchemaZip } from "./fetch";
3
+
4
+ describe("fetchSchemaZip", () => {
5
+ test("exports fetchSchemaZip function", () => {
6
+ expect(typeof fetchSchemaZip).toBe("function");
7
+ });
8
+
9
+ // Integration test - requires network, skip by default
10
+ test.skip("fetches schema zip from AWS (integration)", async () => {
11
+ const schemas = await fetchSchemaZip();
12
+
13
+ // Should have many resource schemas
14
+ expect(schemas.size).toBeGreaterThan(100);
15
+
16
+ // Should contain common resource types
17
+ expect(schemas.has("aws-s3-bucket.json")).toBe(true);
18
+ expect(schemas.has("aws-lambda-function.json")).toBe(true);
19
+
20
+ // Each schema should be valid JSON
21
+ for (const [name, buffer] of schemas) {
22
+ const text = new TextDecoder().decode(buffer);
23
+ const parsed = JSON.parse(text);
24
+ expect(parsed.typeName).toBeDefined();
25
+ }
26
+ });
27
+ });
@@ -0,0 +1,107 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { fetchWithCache, extractFromZip, clearCacheFile } from "@intentius/chant/codegen/fetch";
4
+
5
+ /**
6
+ * Top-level CloudFormation Registry JSON Schema for a single resource type.
7
+ */
8
+ export interface CFNSchema {
9
+ typeName: string;
10
+ description?: string;
11
+ properties?: Record<string, SchemaProperty>;
12
+ definitions?: Record<string, SchemaDefinition>;
13
+ required?: string[];
14
+ readOnlyProperties?: string[];
15
+ createOnlyProperties?: string[];
16
+ writeOnlyProperties?: string[];
17
+ primaryIdentifier?: string[];
18
+ additionalProperties?: boolean;
19
+ }
20
+
21
+ /**
22
+ * A single property in a CloudFormation Registry schema.
23
+ */
24
+ export interface SchemaProperty {
25
+ type?: string | string[];
26
+ description?: string;
27
+ enum?: string[];
28
+ $ref?: string;
29
+ items?: SchemaProperty;
30
+ properties?: Record<string, SchemaProperty>;
31
+ oneOf?: unknown[];
32
+ anyOf?: unknown[];
33
+ required?: string[];
34
+ pattern?: string;
35
+ minLength?: number;
36
+ maxLength?: number;
37
+ minimum?: number;
38
+ maximum?: number;
39
+ format?: string;
40
+ const?: unknown;
41
+ default?: unknown;
42
+ }
43
+
44
+ /**
45
+ * A named type within the definitions section.
46
+ */
47
+ export interface SchemaDefinition {
48
+ type?: string | string[];
49
+ description?: string;
50
+ enum?: string[];
51
+ properties?: Record<string, SchemaProperty>;
52
+ required?: string[];
53
+ items?: SchemaProperty;
54
+ }
55
+
56
+ const SCHEMA_ZIP_URL = "https://schema.cloudformation.us-east-1.amazonaws.com/CloudformationSchema.zip";
57
+ const CACHE_DIR = join(homedir(), ".chant");
58
+ const CACHE_FILE = join(CACHE_DIR, "CloudformationSchema.zip");
59
+
60
+ /**
61
+ * Fetch the CloudFormation Registry schema zip and extract per-resource JSON schemas.
62
+ * Returns a Map keyed by typeName (e.g. "AWS::S3::Bucket") to raw JSON bytes.
63
+ *
64
+ * Uses a local cache with 24h TTL.
65
+ */
66
+ export async function fetchSchemaZip(force = false): Promise<Map<string, Buffer>> {
67
+ const zipData = await fetchWithCache(
68
+ { url: SCHEMA_ZIP_URL, cacheFile: CACHE_FILE },
69
+ force,
70
+ );
71
+ return extractRawSchemas(zipData);
72
+ }
73
+
74
+ /**
75
+ * Extract raw JSON schema bytes from the zip, keyed by typeName.
76
+ */
77
+ async function extractRawSchemas(zipData: Buffer): Promise<Map<string, Buffer>> {
78
+ const files = await extractFromZip(zipData, (name) => name.endsWith(".json"));
79
+
80
+ const schemas = new Map<string, Buffer>();
81
+ for (const [_name, data] of files) {
82
+ try {
83
+ const text = data.toString("utf-8");
84
+ const partial = JSON.parse(text) as { typeName?: string };
85
+ if (!partial.typeName) continue;
86
+ schemas.set(partial.typeName, data);
87
+ } catch {
88
+ // Skip files that can't be parsed
89
+ }
90
+ }
91
+
92
+ return schemas;
93
+ }
94
+
95
+ /**
96
+ * Get the cache file path (for testing)
97
+ */
98
+ export function getCachePath(): string {
99
+ return CACHE_FILE;
100
+ }
101
+
102
+ /**
103
+ * Clear the cache (for testing)
104
+ */
105
+ export function clearCache(): void {
106
+ clearCacheFile(CACHE_FILE);
107
+ }
@@ -0,0 +1,153 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ parseCFNSchema,
4
+ cfnShortName,
5
+ cfnServiceName,
6
+ } from "./parse";
7
+
8
+ // Sample Registry JSON Schema for testing
9
+ const sampleBucketSchema = JSON.stringify({
10
+ typeName: "AWS::S3::Bucket",
11
+ description: "Creates an S3 bucket",
12
+ properties: {
13
+ BucketName: { type: "string", description: "Name of the bucket" },
14
+ Tags: {
15
+ type: "array",
16
+ items: { $ref: "#/definitions/Tag" },
17
+ },
18
+ VersioningConfiguration: {
19
+ $ref: "#/definitions/VersioningConfiguration",
20
+ },
21
+ AccessControl: {
22
+ type: "string",
23
+ enum: ["Private", "PublicRead", "PublicReadWrite"],
24
+ },
25
+ },
26
+ definitions: {
27
+ Tag: {
28
+ type: "object",
29
+ properties: {
30
+ Key: { type: "string" },
31
+ Value: { type: "string" },
32
+ },
33
+ required: ["Key", "Value"],
34
+ additionalProperties: false,
35
+ },
36
+ VersioningConfiguration: {
37
+ type: "object",
38
+ properties: {
39
+ Status: { type: "string", enum: ["Enabled", "Suspended"] },
40
+ },
41
+ required: ["Status"],
42
+ additionalProperties: false,
43
+ },
44
+ },
45
+ readOnlyProperties: [
46
+ "/properties/Arn",
47
+ "/properties/DomainName",
48
+ "/properties/RegionalDomainName",
49
+ "/properties/WebsiteURL",
50
+ ],
51
+ required: [],
52
+ primaryIdentifier: ["/properties/BucketName"],
53
+ additionalProperties: false,
54
+ });
55
+
56
+ describe("parseCFNSchema", () => {
57
+ test("parses resource type", () => {
58
+ const result = parseCFNSchema(sampleBucketSchema);
59
+
60
+ expect(result.resource.typeName).toBe("AWS::S3::Bucket");
61
+ });
62
+
63
+ test("parses properties", () => {
64
+ const result = parseCFNSchema(sampleBucketSchema);
65
+
66
+ expect(result.resource.properties.length).toBeGreaterThan(0);
67
+
68
+ const bucketName = result.resource.properties.find((p) => p.name === "BucketName");
69
+ expect(bucketName).toBeDefined();
70
+ expect(bucketName!.tsType).toBe("string");
71
+ expect(bucketName!.required).toBe(false);
72
+ });
73
+
74
+ test("parses attributes from readOnlyProperties", () => {
75
+ const result = parseCFNSchema(sampleBucketSchema);
76
+
77
+ expect(result.resource.attributes.length).toBe(4);
78
+ const arn = result.resource.attributes.find((a) => a.name === "Arn");
79
+ expect(arn).toBeDefined();
80
+ });
81
+
82
+ test("parses property types from definitions", () => {
83
+ const result = parseCFNSchema(sampleBucketSchema);
84
+
85
+ expect(result.propertyTypes.length).toBeGreaterThan(0);
86
+
87
+ const tag = result.propertyTypes.find((pt) => pt.name === "Bucket_Tag");
88
+ expect(tag).toBeDefined();
89
+ expect(tag!.properties.length).toBe(2);
90
+ });
91
+
92
+ test("parses enum values", () => {
93
+ const result = parseCFNSchema(sampleBucketSchema);
94
+
95
+ // AccessControl enum should be extracted from the property
96
+ const accessControl = result.resource.properties.find((p) => p.name === "AccessControl");
97
+ expect(accessControl).toBeDefined();
98
+ expect(accessControl!.enum).toContain("Private");
99
+ expect(accessControl!.enum).toContain("PublicRead");
100
+ });
101
+
102
+ test("parses array types", () => {
103
+ const result = parseCFNSchema(sampleBucketSchema);
104
+
105
+ const tags = result.resource.properties.find((p) => p.name === "Tags");
106
+ expect(tags).toBeDefined();
107
+ expect(tags!.tsType).toContain("[]");
108
+ });
109
+
110
+ test("parses required properties", () => {
111
+ const result = parseCFNSchema(JSON.stringify({
112
+ typeName: "AWS::Test::Resource",
113
+ properties: {
114
+ Name: { type: "string" },
115
+ Optional: { type: "string" },
116
+ },
117
+ required: ["Name"],
118
+ additionalProperties: false,
119
+ }));
120
+
121
+ const name = result.resource.properties.find((p) => p.name === "Name");
122
+ const optional = result.resource.properties.find((p) => p.name === "Optional");
123
+ expect(name!.required).toBe(true);
124
+ expect(optional!.required).toBe(false);
125
+ });
126
+
127
+ test("handles schema without properties", () => {
128
+ const result = parseCFNSchema(JSON.stringify({
129
+ typeName: "AWS::Test::Empty",
130
+ additionalProperties: false,
131
+ }));
132
+
133
+ expect(result.resource.typeName).toBe("AWS::Test::Empty");
134
+ expect(result.resource.properties).toEqual([]);
135
+ expect(result.resource.attributes).toEqual([]);
136
+ });
137
+ });
138
+
139
+ describe("cfnShortName", () => {
140
+ test("extracts short name from full type", () => {
141
+ expect(cfnShortName("AWS::S3::Bucket")).toBe("Bucket");
142
+ expect(cfnShortName("AWS::Lambda::Function")).toBe("Function");
143
+ expect(cfnShortName("AWS::IAM::Role")).toBe("Role");
144
+ });
145
+ });
146
+
147
+ describe("cfnServiceName", () => {
148
+ test("extracts service name from full type", () => {
149
+ expect(cfnServiceName("AWS::S3::Bucket")).toBe("S3");
150
+ expect(cfnServiceName("AWS::Lambda::Function")).toBe("Lambda");
151
+ expect(cfnServiceName("AWS::IAM::Role")).toBe("IAM");
152
+ });
153
+ });