packages/@alicloud/ros-cdk-core/lib/runtime.ts (334 lines of code) (raw):

import { Construct } from "./construct-compat"; // ---------------------------------------------------------------------- // PROPERTY MAPPERS // // These are used while converting generated classes/property bags to ROS property objects // // We use identity mappers for the primitive types. These don't do anything but are there to make the code // generation work out nicely (so the code generator doesn't need to emit different code for primitive // vs. complex types). export type Mapper = (x: any) => any; function identity(x: any) { return x; } export const stringToRosTemplate: Mapper = identity; export const booleanToRosTemplate: Mapper = identity; export const objectToRosTemplate: Mapper = identity; export const numberToRosTemplate: Mapper = identity; export const anyDictToRosTemplate: Mapper = identity; /** * The date needs to be formatted as an ISO date in UTC * * Some usage sites require a date, some require a timestamp. We'll * always output a timestamp and hope the parser on the other end * is smart enough to ignore the time part... (?) */ export function dateToRosTemplate(x?: Date): any { if (!x) { return undefined; } // tslint:disable-next-line:max-line-length return `${x.getUTCFullYear()}-${pad(x.getUTCMonth() + 1)}-${pad( x.getUTCDate() )}T${pad(x.getUTCHours())}:${pad(x.getUTCMinutes())}:${pad( x.getUTCSeconds() )}`; } /** * Pad a number to 2 decimal places */ function pad(x: number) { if (x < 10) { return "0" + x.toString(); } return x.toString(); } /** * Turn a tag object into the proper ROS representation */ export function rosTagToRosTemplate(x: any): any { return { Key: x.key, Value: x.value, }; } export function listMapper(elementMapper: Mapper): Mapper { return (x: any) => { if (!canInspect(x)) { return x; } return x.map(elementMapper); }; } export function hashMapper(elementMapper: Mapper): Mapper { return (x: any) => { if (!canInspect(x)) { return x; } const ret: any = {}; Object.keys(x).forEach((key) => { ret[key] = elementMapper(x[key]); }); return ret; }; } /** * Return a union mapper * * Takes a list of validators and a list of mappers, which should correspond pairwise. * * The mapper of the first successful validator will be called. */ export function unionMapper( validators: Validator[], mappers: Mapper[] ): Mapper { if (validators.length !== mappers.length) { throw Error( "Not the same amount of validators and mappers passed to unionMapper()" ); } return (x: any) => { if (!canInspect(x)) { return x; } for (let i = 0; i < validators.length; i++) { if (validators[i](x).isSuccess) { return mappers[i](x); } } // Should not be possible because the union must have passed validation before this function // will be called, but catch it anyway. throw new TypeError("No validators matched in the union()"); }; } // ---------------------------------------------------------------------- // VALIDATORS // // These are used while checking that supplied property bags match the expected schema // // We have a couple of datatypes that model validation errors and collections of validation // errors (together forming a tree of errors so that we can trace validation errors through // an object graph), and validators. // // Validators are simply functions that take a value and return a validation results. Then // we have some combinators to turn primitive validators into more complex validators. // /** * Representation of validation results * * Models a tree of validation errors so that we have as much information as possible * about the failure that occurred. */ export class ValidationResult { constructor( readonly errorMessage: string = "", readonly results: ValidationResults = new ValidationResults() ) {} public get isSuccess(): boolean { return !this.errorMessage && this.results.isSuccess; } /** * Turn a failed validation into an exception */ public assertSuccess() { if (!this.isSuccess) { let message = this.errorTree(); // The first letter will be lowercase, so uppercase it for a nicer error message message = message.substr(0, 1).toUpperCase() + message.substr(1); throw new RosSynthesisError(message); } } /** * Return a string rendering of the tree of validation failures */ public errorTree(): string { const childMessages = this.results.errorTreeList(); return ( this.errorMessage + (childMessages.length ? `\n ${childMessages.replace(/\n/g, "\n ")}` : "") ); } /** * Wrap this result with an error message, if it concerns an error */ public prefix(message: string): ValidationResult { if (this.isSuccess) { return this; } return new ValidationResult( `${message}: ${this.errorMessage}`, this.results ); } } /** * A collection of validation results */ export class ValidationResults { constructor(public results: ValidationResult[] = []) {} public collect(result: ValidationResult) { // Only collect failures if (!result.isSuccess) { this.results.push(result); } } public get isSuccess(): boolean { return this.results.every((x) => x.isSuccess); } public errorTreeList(): string { return this.results.map((child) => child.errorTree()).join("\n"); } /** * Wrap up all validation results into a single tree node * * If there are failures in the collection, add a message, otherwise * return a success. */ public wrap(message: string): ValidationResult { if (this.isSuccess) { return VALIDATION_SUCCESS; } return new ValidationResult(message, this); } } // Singleton object to save on allocations export const VALIDATION_SUCCESS = new ValidationResult(); export type Validator = (x: any) => ValidationResult; /** * Return whether this object can be validated at all * * True unless it's undefined or a ROS intrinsic */ export function canInspect(x: any) { // Note: using weak equality on purpose, we also want to catch undefined return x != null && !isRosIntrinsic(x); } export function validateLength(prop: { min?: number; max?: number; data: number; }): ValidationResult { if (prop.min && prop.data < prop.min) { return new ValidationResult( `${JSON.stringify(prop.data)} is less than min value(${prop.min})` ); } if (prop.max && prop.data > prop.max) { return new ValidationResult( `${JSON.stringify(prop.data)} is larger than max value(${prop.max})` ); } return VALIDATION_SUCCESS; } export function validateRange(prop: { min?: number; max?: number; data: number; }): ValidationResult { if (prop.min && prop.data < prop.min) { return new ValidationResult( `${JSON.stringify(prop.data)} is less than min value(${prop.min})` ); } if (prop.max && prop.data > prop.max) { return new ValidationResult( `${JSON.stringify(prop.data)} is larger than min value(${prop.min})` ); } return VALIDATION_SUCCESS; } export function validateAllowedPattern(prop: { reg: string; data: string; }): ValidationResult { const regExp = new RegExp(prop.reg); if (regExp.test(prop.data)) { return VALIDATION_SUCCESS; } else { return new ValidationResult( `The string ${JSON.stringify( prop.data )} does not match the regular expression "${prop.reg}"` ); } } export function validateAllowedValues(prop: { allowedValues: [any]; data: any; }): ValidationResult { for (let value of prop.allowedValues) { if (value === prop.data) return VALIDATION_SUCCESS; } return new ValidationResult( `${JSON.stringify(prop.data)} doesn't exist in [${prop.allowedValues}]` ); } // ROS validators for primitive types export function validateString(x: any): ValidationResult { if (canInspect(x) && typeof x !== "string") { return new ValidationResult(`${JSON.stringify(x)} should be a string`); } return VALIDATION_SUCCESS; } export function validateNumber(x: any): ValidationResult { if (canInspect(x) && typeof x !== "number") { return new ValidationResult(`${JSON.stringify(x)} should be a number`); } return VALIDATION_SUCCESS; } export function validateBoolean(x: any): ValidationResult { if (canInspect(x) && typeof x !== "boolean") { return new ValidationResult(`${JSON.stringify(x)} should be a boolean`); } return VALIDATION_SUCCESS; } export function validateAny(x: any): ValidationResult { // avoid tsc error -> 'x' is declared but its value is never read if (canInspect(x) || true) { return VALIDATION_SUCCESS; } } export function validateDate(x: any): ValidationResult { if (canInspect(x) && !(x instanceof Date)) { return new ValidationResult(`${JSON.stringify(x)} should be a Date`); } if (x !== undefined && isNaN(x.getTime())) { return new ValidationResult("got an unparseable Date"); } return VALIDATION_SUCCESS; } export function validateObject(x: any): ValidationResult { if (canInspect(x) && typeof x !== "object") { return new ValidationResult(`${JSON.stringify(x)} should be an 'object'`); } return VALIDATION_SUCCESS; } export function validateAnyDict(x: any): ValidationResult { if (canInspect(x) && typeof x !== "object") { return new ValidationResult(`${JSON.stringify(x)} should be an 'Dict[str, Any]'`); } return VALIDATION_SUCCESS; } export function validateRosTag(x: any): ValidationResult { if (!canInspect(x)) { return VALIDATION_SUCCESS; } if (x.key == null || x.value == null) { return new ValidationResult( `${JSON.stringify(x)} should have a 'key' and a 'value' property` ); } return VALIDATION_SUCCESS; } /** * Return a list validator based on the given element validator */ export function listValidator(elementValidator: Validator): Validator { return (x: any) => { if (!canInspect(x)) { return VALIDATION_SUCCESS; } if (!x.forEach) { return new ValidationResult(`${JSON.stringify(x)} should be a list`); } for (let i = 0; i < x.length; i++) { const element = x[i]; const result = elementValidator(element); if (!result.isSuccess) { return result.prefix(`element ${i}`); } } return VALIDATION_SUCCESS; }; } /** * Return a hash validator based on the given element validator */ export function hashValidator(elementValidator: Validator): Validator { return (x: any) => { if (!canInspect(x)) { return VALIDATION_SUCCESS; } for (const key of Object.keys(x)) { const result = elementValidator(x[key]); if (!result.isSuccess) { return result.prefix(`element '${key}'`); } } return VALIDATION_SUCCESS; }; } /** * Decorate a validator with a message clarifying the property the failure is for. */ export function propertyValidator( propName: string, validator: Validator ): Validator { return (x: any) => { return validator(x).prefix(propName); }; } /** * Return a validator that will fail if the passed property is not present * * Does not distinguish between the property actually not being present, vs being present but 'null' * or 'undefined' (courtesy of JavaScript), which is generally the behavior that we want. * * Empty strings are considered "present"--don't know if this agrees with how ROS looks at the world. */ export function requiredValidator(x: any) { if (x == null) { return new ValidationResult("required but missing"); } return VALIDATION_SUCCESS; } /** * Require a property from a property bag. * * @param props the property bag from which a property is required. * @param name the name of the required property. * @param typeName the name of the construct type that requires the property * * @returns the value of ``props[name]`` * * @throws if the property ``name`` is not present in ``props``. */ export function requireProperty( props: { [name: string]: any }, name: string, context: Construct ): any { const value = props[name]; if (value == null) { throw new Error( `${context.toString()} is missing required property: ${name}` ); } // Possibly add type-checking here... return value; } /** * Validates if any of the given validators matches * * We add either/or words to the front of the error messages so that they read * more nicely. * */ export function unionValidator(...validators: Validator[]): Validator { return (x: any) => { const results = new ValidationResults(); let eitherOr = "either"; for (const validator of validators) { const result = validator(x); if (result.isSuccess) { return result; } results.collect(result.prefix(eitherOr)); eitherOr = "or"; } return results.wrap("not one of the possible types"); }; } /** * Return whether the indicated value represents a ROS intrinsic. * * ROS intrinsics are modeled as objects with a single key, which * look like: { "Fn::GetAtt": [...] } or similar. */ function isRosIntrinsic(x: any) { if (!(typeof x === "object")) { return false; } const keys = Object.keys(x); if (keys.length !== 1) { return false; } return keys[0] === "Ref" || keys[0].substr(0, 4) === "Fn::"; } // Cannot be public because JSII gets confused about es5.d.ts class RosSynthesisError extends Error { public readonly type = "RosSynthesisError"; }