packages/ros-cdk-codegen/lib/codegen.ts (650 lines of code) (raw):
import { schema, filteredSpecification } from '@alicloud/ros-cdk-spec';
import { CodeMaker } from 'codemaker';
import * as genspec from './genspec';
import { itemTypeNames, PropertyAttributeName, scalarTypeNames, SpecName } from './spec-utils';
import { upcaseFirst } from './util';
const CORE = genspec.CORE_NAMESPACE;
// base class for all resources
const RESOURCE_BASE_CLASS = `${CORE}.RosResource`;
const CONSTRUCT_CLASS = `${CORE}.Construct`;
const TAG_TYPE = `${CORE}.TagType`;
const TAG_MANAGER = `${CORE}.TagManager`;
interface Dictionary<T> {
[key: string]: T;
}
export interface CodeGeneratorOptions {
/**
* How to import the core library.
*
* @default '@alicloud/ros-cdk-core'
*/
readonly coreImport?: string;
}
/**
* Emits classes for all resource types
*/
export default class CodeGenerator {
public readonly outputFile: string;
private code = new CodeMaker();
/**
* Creates the code generator.
* @param moduleName the name of the module (used to determine the file name).
* @param spec resource specification
*/
constructor(
moduleName: string,
private readonly spec: schema.Specification,
private readonly affix: string,
options: CodeGeneratorOptions = {},
) {
this.outputFile = `${moduleName}`;
this.code.openFile(this.outputFile);
const coreImport = options.coreImport ?? '@alicloud/ros-cdk-core';
this.code.line('// Generated from the AliCloud ROS Resource Specification');
this.code.line();
this.code.line(`import * as ${CORE} from '${coreImport}';`);
}
public async emitCode(resourceTypes: string[]) {
for (const name of resourceTypes.sort()) {
const resourceType = filteredSpecification(name).ResourceTypes[name]
const rosName = SpecName.parse(name);
const resourceName = genspec.CodeName.forRosResource(rosName, this.affix);
this.code.line();
this.emitRosResourceType(resourceName, resourceType);
this.emitPropertyTypes(name, resourceName);
}
}
/**
* Saves the generated file.
*/
public async save(dir: string): Promise<string[]> {
this.code.closeFile(this.outputFile);
return await this.code.save(dir);
}
/**
* Emits classes for all property types
*/
private emitPropertyTypes(resourceName: string, resourceClass: genspec.CodeName): void {
const prefix = `${resourceName}.`;
for (const name of Object.keys(filteredSpecification(resourceName).PropertyTypes).sort()) {
if (!name.startsWith(prefix)) {
continue;
}
const rosTypeName = PropertyAttributeName.parse(name);
const propTypeName = genspec.CodeName.forPropertyType(rosTypeName, resourceClass);
const type = filteredSpecification(resourceName).PropertyTypes[name];
if (schema.isRecordType(type)) {
this.emitPropertyType(resourceClass, propTypeName, type);
}
}
}
private openClass(name: genspec.CodeName, superClasses?: string): string {
const extendsPostfix = superClasses ? ` extends ${superClasses}` : '';
this.code.openBlock(`export class ${name.className}${extendsPostfix}${''}`);
return name.className;
}
private closeClass(_name: genspec.CodeName) {
this.code.closeBlock();
}
private emitPropsType(resourceContext: genspec.CodeName, spec: schema.ResourceType): genspec.CodeName | undefined {
if (!spec.Properties) {
return;
}
const name = genspec.CodeName.forResourceProperties(resourceContext);
this.docLink(spec.Documentation, `Properties for defining a \`${resourceContext.className}\`. `);
this.code.openBlock(`export interface ${name.className}`);
const conversionTable = this.emitPropsTypeProperties(resourceContext, spec.Properties, Container.Interface);
this.code.closeBlock();
this.code.line();
this.emitValidator(resourceContext, name, spec.Properties, conversionTable);
this.code.line();
this.emitRosTemplateMapper(resourceContext, name, spec.Properties, conversionTable, true);
return name;
}
/**
* Emit TypeScript for each of the ROS properties, while renaming
*
* Return a mapping of { originalName -> newName }.
*/
private emitPropsTypeProperties(
resource: genspec.CodeName,
propertiesSpec: { [name: string]: schema.Property },
container: Container,
): Dictionary<string> {
const propertyMap: Dictionary<string> = {};
Object.keys(propertiesSpec)
.sort(propertyComparator)
.forEach((propName) => {
this.code.line();
const propSpec = propertiesSpec[propName];
const additionalDocs = resource.specName!.relativeName(propName).fqn;
propertyMap[propName] = this.emitProperty(
{
context: resource,
propName,
spec: propSpec,
additionalDocs: quoteCode(additionalDocs),
},
container,
);
});
return propertyMap;
/**
* A comparator that places required properties before optional properties,
* and sorts properties alphabetically.
* @param l the left property name.
* @param r the right property name.
*/
function propertyComparator(l: string, r: string): number {
const lp = propertiesSpec[l];
const rp = propertiesSpec[r];
if (lp.Required === rp.Required) {
return l.localeCompare(r);
} else if (lp.Required) {
return -1;
}
return 1;
}
}
private emitRosResourceType(resourceName: genspec.CodeName, spec: schema.ResourceType): void {
this.beginNamespace(resourceName);
const rosTypeName = resourceName.specName!.fqn;
//
// Props Bag for this Resource
//
const propsType = this.emitPropsType(resourceName, spec);
if (propsType) {
this.code.line();
}
//
// The class declaration representing this Resource
//
let specificDescription = `This class is a base encapsulation around the ROS resource type \`${rosTypeName}\`.`;
if (spec.Description) {
specificDescription = `${specificDescription.replace('.', spec.Description.replace(rosTypeName, ', which'))}`;
}
const regex = new RegExp('Ros');
const note = `@Note This class does not contain additional functions, so it is recommended to use the \`${resourceName.className.replace(regex, '')}\` class instead of this class for a more convenient development experience.`;
this.docLink(spec.Documentation, specificDescription, note);
this.openClass(resourceName, RESOURCE_BASE_CLASS);
//
// Static inspectors.
//
const rosResourceTypeName = `${JSON.stringify(rosTypeName)}`;
this.code.line('/**');
this.code.line(' * The resource type name for this resource class.');
this.code.line(' */');
this.code.line(`public static readonly ROS_RESOURCE_TYPE_NAME = ${rosResourceTypeName};`);
//
// Attributes
//
const attributes = new Array<genspec.Attribute>();
if (spec.Attributes) {
for (const attributeName of Object.keys(spec.Attributes).sort()) {
if (
!(attributeName[0] >= 'a' && attributeName[0] <= 'z') &&
!(attributeName[0] >= 'A' && attributeName[0] <= 'Z')
)
continue;
const attributeSpec = spec.Attributes![attributeName];
this.code.line();
this.docLink(undefined, `@Attribute ${attributeName}: ${(attributeSpec as schema.Description).Description}`);
const attr = genspec.attributeDefinition(attributeName);
this.code.line(`public readonly ${attr.propertyName}: ${CORE}.IResolvable;`);
attributes.push(attr);
}
}
//
// Set class properties to match ROS Properties spec
//
let propMap;
if (propsType) {
this.code.line();
this.code.line(`public enableResourcePropertyConstraint: boolean;`);
this.code.line();
propMap = this.emitPropsTypeProperties(resourceName, spec.Properties!, Container.Class);
}
//
// Constructor
//
this.code.line();
this.code.line('/**');
this.code.line(' * @param scope - scope in which this resource is defined');
this.code.line(' * @param id - scoped id of the resource');
this.code.line(' * @param props - resource properties');
this.code.line(' */');
const propsArgument = propsType ? `, props: ${propsType.className}` : '';
this.code.openBlock(
`constructor(scope: ${CONSTRUCT_CLASS}, id: string${propsArgument}, enableResourcePropertyConstraint: boolean)`,
);
this.code.line(
`super(scope, id, { type: ${resourceName.className}.ROS_RESOURCE_TYPE_NAME${
propsType ? ', properties: props' : ''
} });`,
);
// initialize all attribute properties
for (const at of attributes) {
let attributeName = at.propertyName.substring(4);
if (
!(attributeName[0] >= 'a' && attributeName[0] <= 'z') &&
!(attributeName[0] >= 'A' && attributeName[0] <= 'Z')
)
continue;
this.code.line(`this.${at.propertyName} = ${at.constructorArguments};`);
}
// initialize all property class members
if (propsType && propMap) {
this.code.line();
this.code.line(`this.enableResourcePropertyConstraint = enableResourcePropertyConstraint;`);
for (const prop of Object.values(propMap)) {
if (schema.isTagPropertyName(upcaseFirst(prop)) && schema.isTaggableResource(spec)) {
this.code.line(
`this.tags = new ${TAG_MANAGER}(${tagType(
spec,
)}, ${rosResourceTypeName}, props.${prop}, { tagPropertyName: '${prop}' });`,
);
} else {
this.code.line(`this.${prop} = props.${prop};`);
}
}
}
this.code.closeBlock();
this.code.line();
// this.emitTreeAttributes(resourceName);
// setup render properties
if (propsType && propMap) {
this.code.line();
this.emitRosTemplateProperties(propsType, propMap, schema.isTaggableResource(spec));
}
this.closeClass(resourceName);
this.endNamespace(resourceName);
}
/**
* We resolve here.
*
* Since resolve() deep-resolves, we only need to do this once.
*/
private emitRosTemplateProperties(propsType: genspec.CodeName, propMap: Dictionary<string>, taggable: boolean): void {
this.code.openBlock('protected get rosProperties(): { [key: string]: any } ');
this.code.indent('return {');
for (const prop of Object.values(propMap)) {
// handle tag rendering because of special cases
if (taggable && schema.isTagPropertyName(upcaseFirst(prop))) {
this.code.line(`${prop}: this.tags.renderTags(),`);
continue;
}
this.code.line(`${prop}: this.${prop},`);
}
this.code.unindent('};');
this.code.closeBlock();
this.code.openBlock('protected renderProperties(props: {[key: string]: any}): { [key: string]: any } ');
this.code.line(`return ${genspec.rosMapperName(propsType).fqn}(props, this.enableResourcePropertyConstraint);`);
this.code.closeBlock();
}
/**
* Emit the function that is going to map the generated TypeScript object back into the schema that ROS template expects
*
* The generated code looks like this:
*
* function bucketPropsToRosTemplate(properties: any): any {
* if (!cdk.canInspect(properties)) return properties;
* BucketPropsValidator(properties).assertSuccess();
* return {
* AccelerateConfiguration: bucketAccelerateConfigurationPropertyToRosTemplate(properties.accelerateConfiguration),
* AccessControl: cdk.stringToRosTemplate(properties.accessControl),
* AnalyticsConfigurations: cdk.listMapper(bucketAnalyticsConfigurationPropertyToRosTemplate)
* (properties.analyticsConfigurations),
* // ...
* };
* }
*
* Generated as a top-level function outside any namespace so we can hide it from library consumers.
*/
private emitRosTemplateMapper(
resource: genspec.CodeName,
typeName: genspec.CodeName,
propSpecs: { [name: string]: schema.Property },
nameConversionTable: Dictionary<string>,
isResourceType: boolean = true,
) {
const mapperName = genspec.rosMapperName(typeName);
this.code.line('/**');
this.code.line(
` * Renders the AliCloud ROS Resource properties of an ${quoteCode(typeName.specName!.fqn)} resource`,
);
this.code.line(' *');
this.code.line(` * @param properties - the TypeScript properties of a ${quoteCode(typeName.className)}`);
this.code.line(' *');
this.code.line(
` * @returns the AliCloud ROS Resource properties of an ${quoteCode(typeName.specName!.fqn)} resource.`,
);
this.code.line(' */');
this.code.line('// @ts-ignore TS6133');
if (isResourceType) {
this.code.openBlock(
`function ${mapperName.functionName}(properties: any, enableResourcePropertyConstraint: boolean): any`,
);
} else {
this.code.openBlock(`function ${mapperName.functionName}(properties: any): any`);
}
// It might be that this value is 'null' or 'undefined', and that that's OK. Simply return
// the falsey value, the upstream struct is in a better position to know whether this is required or not.
this.code.line(`if (!${CORE}.canInspect(properties)) { return properties; }`);
const validatorName = genspec.validatorName(typeName);
// property type depend on the resource type, so the entry of validator, meaning whether validate the props,
// should be in main resource's validtor and there is no need for property type
if (isResourceType) {
this.code.openBlock(`if(enableResourcePropertyConstraint)`);
this.code.line(`${validatorName.fqn}(properties).assertSuccess();`);
this.code.closeBlock();
} else {
this.code.line(`${validatorName.fqn}(properties).assertSuccess();`);
}
// Generate the return object
this.code.line('return {');
const self = this;
Object.keys(nameConversionTable).forEach((rosTypeName) => {
const propName = nameConversionTable[rosTypeName];
const propSpec = propSpecs[rosTypeName];
const mapperExpression = genspec.typeDispatch<string>(resource, propSpec, {
visitAtom(type: genspec.CodeName) {
const specType = type.specName && self.spec.PropertyTypes[type.specName.fqn];
if (specType && !schema.isRecordType(specType)) {
return genspec.typeDispatch(resource, specType, this);
}
return genspec.rosMapperName(type).fqn;
},
visitAtomUnion(types: genspec.CodeName[]) {
const validators = types.map((type) => genspec.validatorName(type).fqn);
const mappers = types.map((type) => this.visitAtom(type));
return `${CORE}.unionMapper([${validators.join(', ')}], [${mappers.join(', ')}])`;
},
visitList(itemType: genspec.CodeName) {
return `${CORE}.listMapper(${this.visitAtom(itemType)})`;
},
visitUnionList(itemTypes: genspec.CodeName[]) {
const validators = itemTypes.map((type) => genspec.validatorName(type).fqn);
const mappers = itemTypes.map((type) => this.visitAtom(type));
return `${CORE}.listMapper(${CORE}.unionMapper([${validators.join(', ')}], [${mappers.join(', ')}]))`;
},
visitMap(itemType: genspec.CodeName) {
return `${CORE}.hashMapper(${this.visitAtom(itemType)})`;
},
visitUnionMap(itemTypes: genspec.CodeName[]) {
const validators = itemTypes.map((type) => genspec.validatorName(type).fqn);
const mappers = itemTypes.map((type) => this.visitAtom(type));
return `${CORE}.hashMapper(${CORE}.unionMapper([${validators.join(', ')}], [${mappers.join(', ')}]))`;
},
visitListOrAtom(types: genspec.CodeName[], itemTypes: genspec.CodeName[]) {
const validatorNames = types.map((type) => genspec.validatorName(type).fqn).join(', ');
const itemValidatorNames = itemTypes.map((type) => genspec.validatorName(type).fqn).join(', ');
const scalarValidator = `${CORE}.unionValidator(${validatorNames})`;
const listValidator = `${CORE}.listValidator(${CORE}.unionValidator(${itemValidatorNames}))`;
const scalarMapper = `${CORE}.unionMapper([${validatorNames}], [${types
.map((type) => this.visitAtom(type))
.join(', ')}])`;
// tslint:disable-next-line:max-line-length
const listMapper = `${CORE}.listMapper(${CORE}.unionMapper([${itemValidatorNames}], [${itemTypes
.map((type) => this.visitAtom(type))
.join(', ')}]))`;
return `${CORE}.unionMapper([${scalarValidator}, ${listValidator}], [${scalarMapper}, ${listMapper}])`;
},
});
self.code.line(` \'${propSpec.PrimitiveName}\': ${mapperExpression}(properties.${propName}),`);
});
this.code.line('};');
this.code.closeBlock();
}
/**
* Emit a function that will validate whether the given property bag matches the schema of this complex type
*
* Generated as a top-level function outside any namespace so we can hide it from library consumers.
*/
private emitValidator(
resource: genspec.CodeName,
typeName: genspec.CodeName,
propSpecs: { [name: string]: schema.Property },
nameConversionTable: Dictionary<string>,
) {
const validatorName = genspec.validatorName(typeName);
this.code.line('/**');
this.code.line(` * Determine whether the given properties match those of a ${quoteCode(typeName.className)}`);
this.code.line(' *');
this.code.line(` * @param properties - the TypeScript properties of a ${quoteCode(typeName.className)}`);
this.code.line(' *');
this.code.line(' * @returns the result of the validation.');
this.code.line(' */');
this.code.openBlock(`function ${validatorName.functionName}(properties: any): ${CORE}.ValidationResult`);
this.code.line(`if (!${CORE}.canInspect(properties)) { return ${CORE}.VALIDATION_SUCCESS; }`);
this.code.line(`const errors = new ${CORE}.ValidationResults();`);
Object.keys(propSpecs).forEach((rosPropName) => {
const propSpec = propSpecs[rosPropName];
const propName = nameConversionTable[rosPropName];
if (propSpec.Required) {
this.code.line(
`errors.collect(${CORE}.propertyValidator('${propName}', ${CORE}.requiredValidator)(properties.${propName}));`,
);
}
// props constraint
this.emitConstraints(propSpec, propName);
const self = this;
const validatorExpression = genspec.typeDispatch<string>(resource, propSpec, {
visitAtom(type: genspec.CodeName) {
const specType = type.specName && self.spec.PropertyTypes[type.specName.fqn];
if (specType && !schema.isRecordType(specType)) {
return genspec.typeDispatch(resource, specType, this);
}
return genspec.validatorName(type).fqn;
},
visitAtomUnion(types: genspec.CodeName[]) {
return `${CORE}.unionValidator(${types.map((type) => this.visitAtom(type)).join(', ')})`;
},
visitList(itemType: genspec.CodeName) {
return `${CORE}.listValidator(${this.visitAtom(itemType)})`;
},
visitUnionList(itemTypes: genspec.CodeName[]) {
return `${CORE}.listValidator(${CORE}.unionValidator(${itemTypes
.map((type) => this.visitAtom(type))
.join(', ')}))`;
},
visitMap(itemType: genspec.CodeName) {
return `${CORE}.hashValidator(${this.visitAtom(itemType)})`;
},
visitUnionMap(itemTypes: genspec.CodeName[]) {
return `${CORE}.hashValidator(${CORE}.unionValidator(${itemTypes
.map((type) => this.visitAtom(type))
.join(', ')}))`;
},
visitListOrAtom(types: genspec.CodeName[], itemTypes: genspec.CodeName[]) {
const scalarValidator = `${CORE}.unionValidator(${types.map((type) => this.visitAtom(type)).join(', ')})`;
const listValidator = `${CORE}.listValidator(${CORE}.unionValidator(${itemTypes
.map((type) => this.visitAtom(type))
.join(', ')}))`;
return `${CORE}.unionValidator(${scalarValidator}, ${listValidator})`;
},
});
self.code.line(
`errors.collect(${CORE}.propertyValidator('${propName}', ${validatorExpression})(properties.${propName}));`,
);
});
this.code.line(`return errors.wrap('supplied properties not correct for "${typeName.className}"');`);
this.code.closeBlock();
}
private emitConstraints(propSpec: schema.Property, propName: string) {
if (!propSpec.Constraints || propSpec.Type === 'Map') return;
for (let constraint of propSpec.Constraints) {
let constraintType = Object.keys(constraint)[0];
if (constraintType === 'Range') {
if (
(propSpec as schema.PrimitiveProperty).PrimitiveType === 'Integer' ||
(propSpec as schema.PrimitiveProperty).PrimitiveType === 'Number'
) {
this.code.openBlock(`if(properties.${propName} && (typeof properties.${propName}) !== 'object')`);
this.code.line(`errors.collect(${CORE}.propertyValidator('${propName}', ${CORE}.validate${constraintType})({
data: properties.${propName},
min: ${Object.values(constraint)[0]['Min']},
max: ${Object.values(constraint)[0]['Max']},
}));`);
this.code.closeBlock();
}
} else if (constraintType === 'Length') {
if ((propSpec as schema.PrimitiveProperty).PrimitiveType === 'String' || propSpec.Type === 'List') {
this.code.openBlock(
`if(properties.${propName} && (Array.isArray(properties.${propName}) || (typeof properties.${propName}) === 'string'))`,
);
this.code.line(`errors.collect(${CORE}.propertyValidator('${propName}', ${CORE}.validate${constraintType})({
data: properties.${propName}.length,
min: ${Object.values(constraint)[0]['Min']},
max: ${Object.values(constraint)[0]['Max']},
}));`);
this.code.closeBlock();
}
} else if (constraintType === 'AllowedValues') {
let allowedValuesCode = '';
if ((propSpec as schema.PrimitiveProperty).PrimitiveType === 'String') {
allowedValuesCode = `["${Object.values(constraint)[0].join('","')}"],`;
} else if (
(propSpec as schema.PrimitiveProperty).PrimitiveType === 'Integer' ||
(propSpec as schema.PrimitiveProperty).PrimitiveType === 'Number' ||
(propSpec as schema.PrimitiveProperty).PrimitiveType === 'Boolean'
) {
allowedValuesCode = `[${Object.values(constraint)[0]}],`;
} else continue;
this.code.openBlock(`if(properties.${propName} && (typeof properties.${propName}) !== 'object')`);
this.code.line(`errors.collect(${CORE}.propertyValidator('${propName}', ${CORE}.validateAllowedValues)({
data: properties.${propName},
allowedValues: ${allowedValuesCode}
}));`);
this.code.closeBlock();
} else if (constraintType === 'AllowedPattern') {
this.code.openBlock(`if(properties.${propName} && (typeof properties.${propName}) !== 'object')`);
this.code.line(`errors.collect(${CORE}.propertyValidator('${propName}', ${CORE}.validateAllowedPattern)({
data: properties.${propName},
reg: /${Object.values(constraint)[0].replaceAll('/', `\\/`)}/
}));`);
this.code.closeBlock();
}
}
}
private emitInterfaceProperty(props: EmitPropertyProps): string {
const javascriptPropertyName = genspec.rosTemplateToScriptName(props.propName);
this.docLink(
undefined,
`@Property ${javascriptPropertyName}: ${props.spec.Description?.replace(new RegExp('\n', 'gm'), '\n * ').replace(new RegExp('/', 'gm'), '\\/')}`,
);
const line =
props.propName === 'Tags' && (props.spec as schema.ComplexListProperty).ItemType === 'Tag'
? `: ${CORE}.RosTag[];`
: `: ${this.findNativeType(props.context, props.spec, props.propName)};`;
const question = props.spec.Required ? '' : '?';
this.code.line(`readonly ${javascriptPropertyName}${question}${line}`);
return javascriptPropertyName;
}
private emitClassProperty(props: EmitPropertyProps): string {
const javascriptPropertyName = genspec.rosTemplateToScriptName(props.propName);
this.docLink(
undefined,
`@Property ${javascriptPropertyName}: ${props.spec.Description?.replace(new RegExp('\n', 'gm'), '\n * ').replace(new RegExp('/', 'gm'), '\\/')}`,
);
const question = props.spec.Required ? ';' : ' | undefined;';
const line = `: ${`${this.findNativeType(props.context, props.spec, props.propName)}`}${question}`;
if (schema.isTagPropertyName(props.propName) && schema.isTagProperty(props.spec)) {
this.code.line(`public readonly tags: ${TAG_MANAGER};`);
} else {
this.code.line(`public ${javascriptPropertyName}${line}`);
}
return javascriptPropertyName;
}
private emitProperty(props: EmitPropertyProps, container: Container): string {
switch (container) {
case Container.Class:
return this.emitClassProperty(props);
case Container.Interface:
return this.emitInterfaceProperty(props);
default:
throw new Error(`Unsupported container ${container}`);
}
}
private beginNamespace(type: genspec.CodeName): void {
if (type.namespace) {
const parts = type.namespace.split('.');
for (const part of parts) {
this.code.openBlock(`export namespace ${part}`);
}
}
}
private endNamespace(type: genspec.CodeName): void {
if (type.namespace) {
const parts = type.namespace.split('.');
for (const _ of parts) {
this.code.closeBlock();
}
}
}
private emitPropertyType(
resourceContext: genspec.CodeName,
typeName: genspec.CodeName,
propTypeSpec: schema.RecordProperty,
): void {
this.code.line();
this.beginNamespace(typeName);
this.docLink(propTypeSpec.Documentation, '@stability external');
if (!propTypeSpec.Properties || Object.keys(propTypeSpec.Properties).length === 0) {
this.code.line('// tslint:disable-next-line:no-empty-interface | A genuine empty-object type');
}
this.code.openBlock(`export interface ${typeName.className}`);
const conversionTable: Dictionary<string> = {};
if (propTypeSpec.Properties) {
Object.keys(propTypeSpec.Properties).forEach((propName) => {
const propSpec = propTypeSpec.Properties[propName];
const additionalDocs = quoteCode(`${typeName.fqn}.${propName}`);
const newName = this.emitInterfaceProperty({
context: resourceContext,
propName,
spec: propSpec,
additionalDocs,
});
conversionTable[propName] = newName;
});
}
this.code.closeBlock();
this.endNamespace(typeName);
// this.code.line();
this.emitValidator(resourceContext, typeName, propTypeSpec.Properties, conversionTable);
this.code.line();
this.emitRosTemplateMapper(resourceContext, typeName, propTypeSpec.Properties, conversionTable, false);
}
/**
* Return the native type expression for the given propSpec
*/
private findNativeType(resourceContext: genspec.CodeName, propSpec: schema.Property, propName?: string): string {
const alternatives: string[] = [];
// render the union of all item types
if (schema.isCollectionProperty(propSpec)) {
// render the union of all item types
const itemTypes = genspec.specTypesToCodeTypes(resourceContext, itemTypeNames(propSpec));
// 'tokenizableType' operates at the level of rendered type names in TypeScript, so stringify
// the objects.
const renderedTypes = itemTypes.map((t) => this.renderCodeName(resourceContext, t));
if (!tokenizableType(renderedTypes) && !schema.isTagPropertyName(propName)) {
// Always accept a token in place of any list element (unless the list elements are tokenizable)
itemTypes.push(genspec.TOKEN_NAME);
}
const union = this.renderTypeUnion(resourceContext, itemTypes);
if (schema.isMapProperty(propSpec)) {
alternatives.push(`{ [key: string]: (${union}) }`);
} else if (schema.isListProperty(propSpec) &&
(propSpec as schema.PrimitiveListProperty).PrimitiveItemType === schema.PrimitiveType.AnyDict) {
alternatives.push(`Array<{ [key: string]: any }>`);
} else {
// To make TSLint happy, we have to either emit: SingleType[] or Array<Alt1 | Alt2>
if (union.indexOf('|') !== -1) {
alternatives.push(`Array<${union}>`);
} else {
alternatives.push(`${union}[]`);
}
}
}
// Yes, some types can be both collection and scalar. Looking at you, SAM.
if (schema.isScalarProperty(propSpec)) {
// Scalar type
const typeNames = scalarTypeNames(propSpec);
const types = genspec.specTypesToCodeTypes(resourceContext, typeNames);
alternatives.push(this.renderTypeUnion(resourceContext, types));
}
// Only if this property is not of a "tokenizable type" (string, string[],
// number in the future) we add a type union for `cdk.Token`. We rather
// everything to be tokenizable because there are languages that do not
// support union types (i.e. Java, .NET), so we lose type safety if we have
// a union.
if (!tokenizableType(alternatives) && !schema.isTagPropertyName(propName)) {
alternatives.push(genspec.TOKEN_NAME.fqn);
}
return alternatives.join(' | ');
}
/**
* Render a CodeName to a string representation of it in TypeScript
*/
private renderCodeName(context: genspec.CodeName, type: genspec.CodeName): string {
const rel = type.relativeTo(context);
const specType = rel.specName && this.spec.PropertyTypes[rel.specName.fqn];
if (!specType || schema.isRecordType(specType)) {
return rel.fqn;
}
return this.findNativeType(context, specType);
}
private renderTypeUnion(context: genspec.CodeName, types: genspec.CodeName[]): string {
return types.map((t) => this.renderCodeName(context, t)).join(' | ');
}
private docLink(link: string | undefined, ...before: string[]): void {
if (!link && before.length === 0) {
return;
}
this.code.line('/**');
before.forEach((line) => this.code.line(` * ${line}`.trimRight()));
if (link) {
this.code.line(` * See ${link}`);
}
this.code.line(' */');
return;
}
}
/**
* Quotes a code name for inclusion in a JSDoc comment, so it will render properly
* in the Markdown output.
*
* @param code a code name to be quoted.
*
* @returns the code name surrounded by double backticks.
*/
function quoteCode(code: string): string {
return '`' + code + '`';
}
function tokenizableType(alternatives: string[]): boolean {
if (alternatives.length > 1) {
return false;
}
// 支持IResolvable隐式转换
// const type = alternatives[0];
// if (type === 'string') {
// return true;
// }
//
// if (type === 'string[]') {
// return true;
// }
//
// if (type === 'number') {
// return true;
// }
return false;
}
function tagType(resource: schema.TaggableResource): string {
for (const name of Object.keys(resource.Properties)) {
if (!schema.isTagPropertyName(name)) {
continue;
}
if (schema.isTagPropertyStandard(resource.Properties[name])) {
return `${TAG_TYPE}.STANDARD`;
}
if (schema.isTagPropertyAutoScalingGroup(resource.Properties[name])) {
return `${TAG_TYPE}.AUTOSCALING_GROUP`;
}
if (
schema.isTagPropertyJson(resource.Properties[name]) ||
schema.isTagPropertyStringMap(resource.Properties[name])
) {
return `${TAG_TYPE}.MAP`;
}
}
return `${TAG_TYPE}.NOT_TAGGABLE`;
}
enum Container {
Interface = 'INTERFACE',
Class = 'CLASS',
}
interface EmitPropertyProps {
context: genspec.CodeName;
propName: string;
spec: schema.Property;
additionalDocs: string;
}