packages/ros-cdk-codegen/lib/genspec.ts (205 lines of code) (raw):

// Classes and definitions that have to do with modeling and decisions around code generation // // Does not include the actual code generation itself. import { schema } from '@alicloud/ros-cdk-spec'; import * as codemaker from 'codemaker'; import { itemTypeNames, PropertyAttributeName, scalarTypeNames, SpecName } from './spec-utils'; import * as util from './util'; export const RESOURCE_CLASS_PREFIX = 'Ros'; export const CORE_NAMESPACE = 'ros'; /** * The name of a class or method in the generated code. * * Has constructor functions to generate them from the ROS specification. * * This refers to TypeScript constructs (typically a class) */ export class CodeName { public static forRosResource(specName: SpecName, affix: string): CodeName { const className = RESOURCE_CLASS_PREFIX + specName.resourceName + affix; return new CodeName(packageName(specName), '', className, specName); } public static forResource(specName: SpecName, affix: string): CodeName { const className = specName.resourceName + affix; return new CodeName(packageName(specName), '', className, specName); } public static forResourceProperties(resourceName: CodeName): CodeName { return new CodeName( resourceName.packageName, resourceName.namespace, `${resourceName.className}Props`, resourceName.specName, ); } public static forResourceInterface(resourceName: CodeName): CodeName { let className = resourceName.className; if(!/[a-z]/.test(className)) { let lowClassName: string = className.toLowerCase(); className = lowClassName.charAt(0).toUpperCase() + lowClassName.substring(1); } return new CodeName( resourceName.packageName, resourceName.namespace, `I${className}`, resourceName.specName, ); } public static forPropertyType(specName: PropertyAttributeName, resourceClass: CodeName): CodeName { // Exception for an intrinsic type if (specName.propAttrName === 'Tag' && specName.resourceName === '') { return TAG_NAME; } // These are in a namespace named after the resource return new CodeName(packageName(specName), resourceClass.className, `${specName.propAttrName}Property`, specName); } public static forPrimitive(primitiveName: string): CodeName { return new CodeName('', '', primitiveName); } // tslint:disable:no-shadowed-variable constructor( readonly packageName: string, readonly namespace: string, readonly className: string, readonly specName?: SpecName, readonly methodName?: string, ) {} // tslint:enable:no-shadowed-variable /** * Alias for className * * Simply returns the top-level declaration name, but reads better at the call site if * we're generating a function instead of a class. */ public get functionName(): string { return this.className; } /** * Return the fully qualified name of the TypeScript object * * (When referred to it from the same package) */ public get fqn(): string { return util.joinIf(this.namespace, '.', util.joinIf(this.className, '.', this.methodName)); } public referToMethod(methodName: string): CodeName { return new CodeName(this.packageName, this.namespace, this.className, this.specName, methodName); } /** * Return a relative name from a given name to this name. * * Strips off the namespace if they're the same, otherwise leaves the namespace on. */ public relativeTo(fromName: CodeName): CodeName { if (this.namespace === fromName.namespace) { return new CodeName(this.packageName, '', this.className, this.specName, this.methodName); } return this; } } export const TAG_NAME = new CodeName('', CORE_NAMESPACE, 'RosTag'); export const TOKEN_NAME = new CodeName('', CORE_NAMESPACE, 'IResolvable'); /** * Resource attribute */ export class Attribute { constructor(readonly propertyName: string, readonly attributeType: string, readonly constructorArguments: string) {} } /** * Return the package in which this RosName should be stored * * Example: ALIYUN::ECS -> ecs */ export function packageName(module: SpecName | string): string { if (module instanceof SpecName) { module = module.module; } const parts = module.split('::'); return overridePackageName(parts[parts.length - 1].toLowerCase()); } /** * Overrides special-case namespaces like serverless=>sam */ function overridePackageName(name: string): string { if (name === 'serverless') { return 'sam'; } return name; } /** * Return the name by which the property mapping function will be defined * * It will not be defined in a namespace, because otherwise we would have to export it and * we don't want to expose it to clients. */ export function rosMapperName(typeName: CodeName): CodeName { if (!typeName.packageName) { // Built-in or intrinsic type, built-in mappers const mappedType = typeName.className === 'any' ? 'object' : typeName.className; return new CodeName('', CORE_NAMESPACE, '', undefined, util.downcaseFirst(`${mappedType}ToRosTemplate`)); } return new CodeName( typeName.packageName, '', util.downcaseFirst(`${typeName.namespace}${typeName.className}ToRosTemplate`), ); } /** * Return the name for the type-checking method */ export function validatorName(typeName: CodeName): CodeName { if (typeName.packageName === '') { // Built-in or intrinsic type, built-in validators const validatedType = typeName.className === 'any' ? 'Any' : codemaker.toPascalCase(typeName.className); return new CodeName('', CORE_NAMESPACE, '', undefined, `validate${validatedType}`); } return new CodeName(typeName.packageName, '', `${util.joinIf(typeName.namespace, '_', typeName.className)}Validator`); } /** * Determine how we will render a attribute in the code * * This consists of: * * - The type we will generate for the attribute, including its base class and docs. * - The property name we will use to refer to the attribute. */ export function attributeDefinition(attributeName: string): Attribute { const descriptiveName = attributeName.replace(/\./g, ''); const suffixName = codemaker.toPascalCase(rosTemplateToScriptName(descriptiveName)); const propertyName = `attr${suffixName}`; // "attrArn" const constructorArguments = `this.getAtt('${attributeName}')`; return new Attribute(propertyName, 'string', constructorArguments); } /** * Convert a ROS template name to a nice TypeScript name * * We use a library to camelcase, and fix up some things that translate incorrectly. * * For example, the library breaks when pluralizing an abbreviation, such as "ProviderARNs" -> "providerArNs". * * We currently recognize "ARNs", "MBs" and "AZs". */ export function rosTemplateToScriptName(name: string): string { if (name === 'VPCs') { return 'vpcs'; } const ret = codemaker.toCamelCase(name); const suffixes: { [key: string]: string } = { ARNs: 'Arns', MBs: 'MBs', AZs: 'AZs' }; for (const suffix of Object.keys(suffixes)) { if (name.endsWith(suffix)) { return ret.substr(0, ret.length - suffix.length) + suffixes[suffix]; } } return ret; } function specPrimitiveToCodePrimitive(type: schema.PrimitiveType): CodeName { switch (type) { case 'Boolean': return CodeName.forPrimitive('boolean'); case 'Double': return CodeName.forPrimitive('number'); case 'Integer': return CodeName.forPrimitive('number'); case 'Number': return CodeName.forPrimitive('number'); case 'Json': return CodeName.forPrimitive('any'); case 'Long': return CodeName.forPrimitive('number'); case 'String': return CodeName.forPrimitive('string'); case 'Timestamp': return CodeName.forPrimitive('Date'); case 'Any': return CodeName.forPrimitive('any'); case 'AnyDict': return CodeName.forPrimitive('AnyDict'); default: throw new Error(`Invalid primitive type: ${type}`); } } export function isPrimitive(type: CodeName): boolean { return ( type.className === 'boolean' || type.className === 'number' || type.className === 'any' || type.className === 'string' || type.className === 'Date' || type.className === 'AnyDict' ); } function specTypeToCodeType(resourceContext: CodeName, type: string): CodeName { if (schema.isPrimitiveType(type)) { return specPrimitiveToCodePrimitive(type); } else if (type === 'Tag') { // Tags are not considered primitive by the spec (even though they are intrinsic) // so we won't consider them primitive either. return TAG_NAME; } const typeName = resourceContext.specName!.relativeName(type); return CodeName.forPropertyType(typeName, resourceContext); } /** * Translate a list of type references in a resource context to a list of code names */ export function specTypesToCodeTypes(resourceContext: CodeName, types: string[]): CodeName[] { return types.map((type) => specTypeToCodeType(resourceContext, type)); } export interface PropertyVisitor<T> { /** * A single type (either built-in or complex) */ visitAtom(type: CodeName): T; /** * A union of atomic types */ visitAtomUnion(types: CodeName[]): T; /** * A list of atoms */ visitList(itemType: CodeName): T; /** * List of unions */ visitUnionList(itemTypes: CodeName[]): T; /** * Map of atoms */ visitMap(itemType: CodeName): T; /** * Map of unions */ visitUnionMap(itemTypes: CodeName[]): T; /** * Union of list type and atom type */ visitListOrAtom(scalarTypes: CodeName[], itemTypes: CodeName[]): any; } /** * Invoke the right visitor method for the given property, depending on its type * * We use the term "atom" in this context to mean a type that can only accept a single * value of a given type. This is to contrast it with collections and unions. */ export function typeDispatch<T>(resourceContext: CodeName, spec: schema.Property, visitor: PropertyVisitor<T>): T { const scalarTypes = specTypesToCodeTypes(resourceContext, scalarTypeNames(spec)); const itemTypes = specTypesToCodeTypes(resourceContext, itemTypeNames(spec)); if (schema.isCollectionProperty(spec)) { // List or map, of either atoms or unions if (schema.isMapProperty(spec)) { if (itemTypes.length > 1) { return visitor.visitUnionMap(itemTypes); } else { return visitor.visitMap(itemTypes[0]); } } else { if (itemTypes.length > 1) { return visitor.visitUnionList(itemTypes); } else { return visitor.visitList(itemTypes[0]); } } } // Atom or union of atoms if (scalarTypes.length > 1) { return visitor.visitAtomUnion(scalarTypes); } else { return visitor.visitAtom(scalarTypes[0]); } }