jsii-pacmak
Version:
A code generation framework for jsii backend languages
408 lines • 17.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.StructValidator = exports.ParameterValidator = void 0;
const crypto_1 = require("crypto");
const jsii_reflect_1 = require("jsii-reflect");
const dependencies_1 = require("../dependencies");
const types_1 = require("../types");
const constants_1 = require("./constants");
class ParameterValidator {
constructor(baseName, validations, receiver) {
this.receiver = receiver;
this.name = `validate${baseName}Parameters`;
this.validations = validations;
this.parameters = Array.from(validations.keys());
}
static forConstructor(ctor) {
return ParameterValidator.fromParts(`New${ctor.parent.name}`, ctor.parameters);
}
static forMethod(method) {
return ParameterValidator.fromParts(method.name, method.parameters, method.static
? undefined
: {
name: method.instanceArg,
type: `*${method.parent.proxyName}`,
});
}
static forProperty(property) {
if (property.immutable) {
return undefined;
}
return ParameterValidator.fromParts(property.setterName, [
syntheticParameter(property.parent, 'val', property.reference.reference.spec, property.property.optional),
], property.static
? undefined
: {
name: property.instanceArg,
type: `*${property.parent.proxyName}`,
});
}
static fromParts(name, parameters, receiver) {
if (parameters.length === 0) {
return undefined;
}
const parameterValidations = new Map();
for (const param of parameters) {
const expr = param.name;
const descr = `parameter ${param.name}`;
const validations = new Array();
if (!param.isOptional && !param.isVariadic) {
validations.push(Validation.nullCheck(expr, descr, param.reference));
}
const validation = Validation.forTypeMap(expr, descr, param.isVariadic
? { type: 'array', value: param.reference }
: param.reference.typeMap);
if (validation) {
validations.push(validation);
}
if (validations.length !== 0) {
parameterValidations.set(param, validations);
}
}
if (parameterValidations.size === 0) {
return undefined;
}
return new ParameterValidator(name, parameterValidations, receiver);
}
get dependencies() {
return [
...this.parameters.flatMap((p) => p.reference.withTransparentUnions.dependencies),
...Array.from(this.validations.values()).flatMap((vs) => vs.flatMap((v) => v.dependencies)),
];
}
get specialDependencies() {
return (0, dependencies_1.reduceSpecialDependencies)(...this.parameters.map((p) => p.reference.specialDependencies), ...Array.from(this.validations.values()).flatMap((vs) => vs.flatMap((v) => v.specialDependencies)));
}
emitCall(code) {
const recv = this.receiver?.name ? `${this.receiver.name}.` : '';
const params = this.parameters
.map((p) => (p.isVariadic ? `&${p.name}` : p.name))
.join(', ');
code.openBlock(`if err := ${recv}${this.name}(${params}); err != nil`);
code.line(`panic(err)`);
code.closeBlock();
}
emitImplementation(code, scope, noOp = false) {
code.openBlock(`func ${this.receiver ? `(${this.receiver.name} ${this.receiver.type}) ` : ''}${this.name}(${this.parameters
.map((p) => p.isVariadic
? `${p.name} *[]${p.reference.scopedReference(scope)}`
: p.toString())
.join(', ')}) error`);
if (noOp) {
code.line('return nil');
}
else {
for (const [_parameter, validations] of this.validations) {
for (const validation of validations) {
validation.emit(code, scope);
}
code.line();
}
code.line('return nil');
}
code.closeBlock();
code.line();
}
}
exports.ParameterValidator = ParameterValidator;
class StructValidator {
constructor(receiver, validations) {
this.receiver = receiver;
this.validations = validations;
}
static for(struct) {
const receiver = {
name: struct.name.slice(0, 1).toLowerCase(),
type: `*${struct.name}`,
};
const fieldValidations = new Map();
for (const prop of struct.properties) {
const expr = `${receiver.name}.${prop.name}`;
const descr = `@{desc()}.${prop.name}`;
const validations = new Array();
if (!prop.property.optional) {
validations.push(Validation.nullCheck(expr, descr, prop.reference));
}
const validation = Validation.forTypeMap(expr, descr, prop.reference.typeMap);
if (validation) {
validations.push(validation);
}
if (validations.length > 0) {
fieldValidations.set(prop, validations);
}
}
if (fieldValidations.size === 0) {
return undefined;
}
return new StructValidator(receiver, fieldValidations);
}
get dependencies() {
return Array.from(this.validations.values()).flatMap((vs) => vs.flatMap((v) => v.dependencies));
}
get specialDependencies() {
return (0, dependencies_1.reduceSpecialDependencies)({
fmt: true,
init: false,
internal: false,
runtime: false,
time: false,
}, ...Array.from(this.validations.values()).flatMap((vs) => vs.flatMap((v) => v.specialDependencies)));
}
emitImplementation(code, scope, noOp = false) {
code.openBlock(`func (${this.receiver.name} ${this.receiver.type}) validate(desc func() string) error`);
if (noOp) {
code.line('return nil');
}
else {
for (const [_prop, validations] of this.validations) {
for (const validation of validations) {
validation.emit(code, scope);
}
code.line();
}
code.line('return nil');
}
code.closeBlock();
code.line();
}
}
exports.StructValidator = StructValidator;
class Validation {
static forTypeMap(expression, description, typeMap) {
switch (typeMap.type) {
case 'union':
return Validation.unionCheck(expression, description, typeMap.value);
case 'interface':
return Validation.interfaceCheck(expression, description, typeMap.value);
case 'array':
case 'map':
return Validation.collectionCheck(expression, description, typeMap.value);
case 'primitive':
case 'void':
return undefined;
}
}
static collectionCheck(expression, description, elementType) {
// We need to come up with a unique-enough ID here... so we use a hash.
const idx = `idx_${(0, crypto_1.createHash)('sha256')
.update(expression)
.digest('hex')
.slice(0, 6)}`;
// This is actually unused
const elementValidator = Validation.forTypeMap('v', `${description}[@{${idx}:#v}]`, elementType.typeMap);
if (elementValidator == null) {
return undefined;
}
class CollectionCheck extends Validation {
get specialDependencies() {
return elementValidator.specialDependencies;
}
get dependencies() {
return elementValidator.dependencies;
}
emit(code, scope) {
// We need to de-reference the pointer here (range does not operate on pointers)
code.openBlock(`for ${idx}, v := range *${expression}`);
elementValidator.emit(code, scope);
code.closeBlock();
}
}
return new CollectionCheck();
}
static interfaceCheck(expression, description, iface) {
if (!iface.datatype) {
return undefined;
}
class InterfaceCheck extends Validation {
get dependencies() {
return [];
}
get specialDependencies() {
return {
fmt: INTERPOLATION.test(description),
init: false,
internal: false,
runtime: !!iface.datatype,
time: false,
};
}
emit(code, _scope) {
code.openBlock(`if err := ${constants_1.JSII_RT_ALIAS}.ValidateStruct(${expression}, func() string { return ${interpolated(description)} }); err != nil`);
code.line(`return err`);
code.closeBlock();
}
}
return new InterfaceCheck();
}
static nullCheck(expression, description, typeRef) {
class NullCheck extends Validation {
get dependencies() {
return [];
}
get specialDependencies() {
return {
fmt: true,
init: false,
internal: false,
runtime: false,
time: false,
};
}
emit(code) {
const nullValue = typeRef.type?.type?.isEnumType()
? `""` // Enums are represented as string-valued constants
: 'nil';
code.openBlock(`if ${expression} == ${nullValue}`);
code.line(returnErrorf(`${description} is required, but nil was provided`));
code.closeBlock();
}
}
return new NullCheck();
}
static unionCheck(expression, description, types) {
const hasInterface = types.some((t) => t.typeMap.type === 'interface');
class UnionCheck extends Validation {
get dependencies() {
return types.flatMap((t) => t.dependencies);
}
get specialDependencies() {
return (0, dependencies_1.reduceSpecialDependencies)({
fmt: true,
init: false,
internal: false,
runtime: hasInterface,
time: false,
}, ...types.flatMap((t) => {
const validator = Validation.forTypeMap(expression, description, t.typeMap);
if (validator == null)
return [];
return [validator.specialDependencies];
}));
}
emit(code, scope) {
const validTypes = new Array();
code.line(`switch ${expression}.(type) {`);
for (const type of types) {
const typeName = type.scopedReference(scope);
validTypes.push(typeName);
// Maps a type to the conversion instructions to the ${typeName} type
const acceptableTypes = new Map();
acceptableTypes.set(typeName, undefined);
switch (typeName) {
case '*float64':
// For numbers, we accept everything that implictly converts to float64 (pointer & not)
acceptableTypes.set('float64', (code, inVar, outVar) => code.line(`${outVar} := &${inVar}`));
const ALTERNATE_TYPES = [
'int',
'uint',
'int8',
'int16',
'int32',
'int64',
'uint8',
'uint16',
'uint32',
'uint64',
];
for (const otherType of ALTERNATE_TYPES) {
const varName = (0, crypto_1.createHash)('sha256')
.update(expression)
.digest('hex')
.slice(6);
acceptableTypes.set(`*${otherType}`, (code) => {
code.openBlock(`${varName} := func (v *${otherType}) *float64`);
code.openBlock('if v == nil {');
code.line('return nil');
code.closeBlock();
code.line(`val := float64(*v)`);
code.line(`return &val`);
code.closeBlock('()');
});
acceptableTypes.set(otherType, (code) => {
code.openBlock(`${varName} := func (v ${otherType}) *float64`);
code.line(`val := float64(v)`);
code.line(`return &val`);
code.closeBlock('()');
});
}
break;
default:
// Accept pointer and non-pointer versions of everything
if (typeName.startsWith('*')) {
const nonPointerType = typeName.slice(1);
acceptableTypes.set(nonPointerType, (code, inVar, outVar) => code.line(`${outVar} := &${inVar}`));
}
}
for (const [acceptableType, conversion] of acceptableTypes) {
code.indent(`case ${acceptableType}:`);
const outVar = /^[a-z0-9_]+$/.test(expression) ? expression : `v`;
const validation = Validation.forTypeMap(outVar, description, type.typeMap);
if (validation) {
const inVar = conversion ? `${outVar}_` : outVar;
code.line(`${inVar} := ${expression}.(${acceptableType})`);
if (conversion) {
conversion(code, inVar, outVar);
}
validation.emit(code, scope);
}
else {
code.line('// ok');
}
code.unindent(false);
}
}
code.indent('default:');
if (hasInterface)
code.openBlock(`if !${constants_1.JSII_RT_ALIAS}.IsAnonymousProxy(${expression})`);
code.line(returnErrorf(`${description} must be one of the allowed types: ${validTypes.join(', ')}; received @{${expression}:#v} (a @{${expression}:T})`));
if (hasInterface)
code.closeBlock();
code.unindent('}');
}
}
return new UnionCheck();
}
constructor() { }
}
const INTERPOLATION = /@\{([^}:]+)(?::([^}]+))?\}/;
function interpolated(message) {
// Need to escape literal percent signes, as a precaution.
let escaped = message.replace(/%/gm, '%%');
const args = new Array();
let match;
while ((match = INTERPOLATION.exec(escaped))) {
const before = escaped.slice(0, match.index);
const expr = match[1];
const mod = match[2];
const after = escaped.slice(match.index + match[1].length + 3 + (mod ? mod.length + 1 : 0));
escaped = `${before}%${mod || 'v'}${after}`;
args.push(expr);
}
if (args.length === 0) {
return JSON.stringify(message);
}
return `fmt.Sprintf(${JSON.stringify(escaped)}, ${args.join(', ')})`;
}
function returnErrorf(message) {
const args = new Array();
// Need to escape literal percent signes, as a precaution.
message = message.replace(/%/gm, '%%');
let match;
while ((match = INTERPOLATION.exec(message))) {
const before = message.slice(0, match.index);
const expr = match[1];
const mod = match[2];
const after = message.slice(match.index + match[1].length + 3 + (mod ? mod.length + 1 : 0));
message = `${before}%${mod || 'v'}${after}`;
args.push(expr);
}
return `return fmt.Errorf(${[JSON.stringify(message), ...args].join(', ')})`;
}
function syntheticParameter(parent, name, type, optional) {
return new types_1.GoParameter(parent, new jsii_reflect_1.Parameter(parent.type.system, parent.type, new jsii_reflect_1.Method(parent.type.system, parent.type.assembly, parent.type, parent.type, { name: '__synthetic__' }), {
name,
optional,
type,
}));
}
//# sourceMappingURL=runtime-type-checking.js.map
;