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";
}