Skip to content

Commit a15250e

Browse files
authored
chore(cfnspec): CloudFormation documentation in L1 classes, again (#18190)
Re-draft of #18101. The previous implementation rendered invalid escape sequences which would break the Java rendering. Fixed in this iteration. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 8832df1 commit a15250e

File tree

6 files changed

+116
-11
lines changed

6 files changed

+116
-11
lines changed

packages/@aws-cdk/cfnspec/build-tools/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ async function main() {
1717

1818
await generateResourceSpecification(inputDir, path.join(outDir, 'specification.json'));
1919
await mergeSpecificationFromDirs(path.join(inputDir, 'cfn-lint'), path.join(outDir, 'cfn-lint.json'));
20+
await fs.copyFile(path.join(inputDir, 'cfn-docs', 'cfn-docs.json'), path.join(outDir, 'cfn-docs.json'));
2021
}
2122

2223
/**

packages/@aws-cdk/cfnspec/lib/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ export function specification(): schema.Specification {
1212
return require('../spec/specification.json');
1313
}
1414

15+
/**
16+
* The complete AWS CloudFormation Resource specification, having any CDK patches and enhancements included in it.
17+
*/
18+
export function docs(): schema.CloudFormationDocsFile {
19+
// eslint-disable-next-line @typescript-eslint/no-require-imports
20+
return require('../spec/cfn-docs.json');
21+
}
22+
23+
1524
/**
1625
* Return the resource specification for the given typename
1726
*
@@ -26,6 +35,21 @@ export function resourceSpecification(typeName: string): schema.ResourceType {
2635
return ret;
2736
}
2837

38+
/**
39+
* Return documentation for the given type
40+
*/
41+
export function typeDocs(resourceName: string, propertyTypeName?: string): schema.CloudFormationTypeDocs {
42+
const key = propertyTypeName ? `${resourceName}.${propertyTypeName}` : resourceName;
43+
const ret = docs().Types[key];
44+
if (!ret) {
45+
return {
46+
description: '',
47+
properties: {},
48+
};
49+
}
50+
return ret;
51+
}
52+
2953
/**
3054
* Get the resource augmentations for a given type
3155
*/
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Docs for a CloudFormation resource or property type
3+
*/
4+
export interface CloudFormationTypeDocs {
5+
/**
6+
* Description for this type
7+
*/
8+
readonly description: string;
9+
10+
/**
11+
* Descriptions for each of the type's properties
12+
*/
13+
readonly properties: Record<string, string>;
14+
15+
/**
16+
* Descriptions for each of the resource's attributes
17+
*/
18+
readonly attributes?: Record<string, string>;
19+
}
20+
21+
export interface CloudFormationDocsFile {
22+
readonly Types: Record<string, CloudFormationTypeDocs>;
23+
}

packages/@aws-cdk/cfnspec/lib/schema/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './resource-type';
44
export * from './specification';
55
export * from './augmentation';
66
export * from './cfn-lint';
7+
export * from './docs';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as cfnspec from '../lib';
2+
3+
test('spot-check resource docs', () => {
4+
const bucketDocs = cfnspec.typeDocs('AWS::S3::Bucket');
5+
6+
expect(bucketDocs.description).toBeTruthy();
7+
expect(bucketDocs.properties.BucketName).toBeTruthy();
8+
});
9+
10+
test('spot-check property type docs', () => {
11+
const destDocs = cfnspec.typeDocs('AWS::S3::Bucket.Destination');
12+
13+
expect(destDocs.description).toBeTruthy();
14+
expect(destDocs.properties.Format).toBeTruthy();
15+
});

tools/@aws-cdk/cfn2ts/lib/codegen.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { schema, cfnLintAnnotations } from '@aws-cdk/cfnspec';
1+
import { schema, cfnLintAnnotations, typeDocs } from '@aws-cdk/cfnspec';
22
import { CodeMaker } from 'codemaker';
33
import * as genspec from './genspec';
44
import { itemTypeNames, PropertyAttributeName, scalarTypeNames, SpecName } from './spec-utils';
@@ -115,7 +115,7 @@ export default class CodeGenerator {
115115
const name = genspec.CodeName.forResourceProperties(resourceContext);
116116

117117
this.docLink(spec.Documentation,
118-
`Properties for defining a \`${resourceContext.specName!.fqn}\``,
118+
`Properties for defining a \`${resourceContext.className}\``,
119119
'',
120120
'@stability external');
121121
this.code.openBlock(`export interface ${name.className}`);
@@ -144,15 +144,17 @@ export default class CodeGenerator {
144144
container: Container): Dictionary<string> {
145145
const propertyMap: Dictionary<string> = {};
146146

147+
const docs = typeDocs(resource.specName?.fqn ?? '');
148+
147149
Object.keys(propertiesSpec).sort(propertyComparator).forEach(propName => {
148150
this.code.line();
149151
const propSpec = propertiesSpec[propName];
150-
const additionalDocs = resource.specName!.relativeName(propName).fqn;
152+
const additionalDocs = docs.properties[propName] || quoteCode(resource.specName!.relativeName(propName).fqn);
151153
const newName = this.emitProperty({
152154
context: resource,
153155
propName,
154156
spec: propSpec,
155-
additionalDocs: quoteCode(additionalDocs),
157+
additionalDocs,
156158
},
157159
container,
158160
);
@@ -192,15 +194,20 @@ export default class CodeGenerator {
192194
this.code.line();
193195
}
194196

197+
const docs = typeDocs(cfnName);
198+
195199
//
196200
// The class declaration representing this Resource
197201
//
198202

199-
this.docLink(spec.Documentation,
203+
this.docLink(spec.Documentation, ...[
200204
`A CloudFormation \`${cfnName}\``,
201205
'',
206+
...docs.description.split('\n'),
207+
'',
202208
`@cloudformationResource ${cfnName}`,
203-
'@stability external');
209+
'@stability external',
210+
]);
204211
this.openClass(resourceName, RESOURCE_BASE_CLASS);
205212

206213
//
@@ -271,7 +278,9 @@ export default class CodeGenerator {
271278

272279
this.code.line();
273280

274-
this.docLink(undefined, `@cloudformationAttribute ${attributeName}`);
281+
this.docLink(undefined,
282+
docs.attributes?.[attributeName] ?? '',
283+
`@cloudformationAttribute ${attributeName}`);
275284
const attr = genspec.attributeDefinition(attributeName, attributeSpec);
276285

277286
this.code.line(`public readonly ${attr.propertyName}: ${attr.attributeType};`);
@@ -847,18 +856,25 @@ export default class CodeGenerator {
847856
this.code.line();
848857
this.beginNamespace(typeName);
849858

850-
this.docLink(propTypeSpec.Documentation, '@stability external');
859+
const docs = typeDocs(resourceContext.specName?.fqn ?? '', (typeName.specName as PropertyAttributeName | undefined)?.propAttrName);
860+
861+
this.docLink(
862+
propTypeSpec.Documentation,
863+
docs.description,
864+
'@stability external',
865+
);
851866
/*
852867
if (!propTypeSpec.Properties || Object.keys(propTypeSpec.Properties).length === 0) {
853868
this.code.line('// eslint-disable-next-line somethingsomething | A genuine empty-object type');
854869
}
855870
*/
856871
this.code.openBlock(`export interface ${typeName.className}`);
857872
const conversionTable: Dictionary<string> = {};
873+
858874
if (propTypeSpec.Properties) {
859875
Object.keys(propTypeSpec.Properties).forEach(propName => {
860876
const propSpec = propTypeSpec.Properties[propName];
861-
const additionalDocs = quoteCode(`${typeName.fqn}.${propName}`);
877+
const additionalDocs = docs.properties[propName] || quoteCode(`${typeName.fqn}.${propName}`);
862878
const newName = this.emitInterfaceProperty({
863879
context: resourceContext,
864880
propName,
@@ -950,12 +966,37 @@ export default class CodeGenerator {
950966
private docLink(link: string | undefined, ...before: string[]): void {
951967
if (!link && before.length === 0) { return; }
952968
this.code.line('/**');
953-
before.forEach(line => this.code.line(` * ${line}`.trimRight()));
969+
before.flatMap(x => x.split('\n')).forEach(line => this.code.line(` * ${escapeDocText(line)}`.trimRight()));
954970
if (link) {
971+
if (before.length > 0) {
972+
this.code.line(' *');
973+
}
955974
this.code.line(` * @link ${link}`);
956975
}
957976
this.code.line(' */');
958-
return;
977+
978+
/**
979+
* Add escapes to the doc text to avoid text that breaks the parsing of the string
980+
*
981+
* We currently escape the following sequences:
982+
*
983+
* - <asterisk><slash> (* /): if this occurs somewhere in the doc text, it
984+
* will end the block comment in the wrong place. Break up those
985+
* characters by inserting a space. Would have loved to use a zero-width space,
986+
* but I'm very very afraid it will break codegen in subtle ways, and just using
987+
* a space feels safer.
988+
* - \u: if this occurs in Java code, the Java compiler will try and parse the
989+
* following 4 characters as a unicode code point, and fail if the 4 characters
990+
* aren't hex digits. This is formally a bug in pacmak (it should do the escaping
991+
* while rendering, https://github.com/aws/jsii/issues/3302), but to
992+
* expedite the build fixing it here as well. Replace with '\ u' (tried using
993+
* `\\u` but for some reason that also doesn't carry over to codegen).
994+
*/
995+
function escapeDocText(x: string) {
996+
x = x.replace(/\*\//g, '* /');
997+
x = x.replace(/\\u/g, '\\ u');
998+
return x;
999+
}
9591000
}
9601001
}
9611002

0 commit comments

Comments
 (0)