tynder
Version:
TypeScript friendly Data validator for JavaScript.
437 lines (386 loc) • 15 kB
text/typescript
// Copyright (c) 2020 Shellyl_N and Authors
// license: ISC
// https://github.com/shellyln
import { TypeAssertion,
PrimitiveTypeAssertion,
PrimitiveValueTypeAssertion,
RepeatedAssertion,
SpreadAssertion,
SequenceAssertion,
OneOfAssertion,
OptionalAssertion,
EnumAssertion,
ObjectAssertion,
TypeAssertionMap,
CodegenContext } from '../../types';
import { escapeString } from '../../lib/escape';
import { nvl2 } from './../util';
function formatTypeName(ty: TypeAssertion, ctx: CodegenContext, typeName: string) {
if (typeName.includes('.') || ty.kind === 'symlink' || ty.kind === 'enum') {
return generateCSharpCodeInner(ty, false, ctx);
}
return typeName;
}
function formatCSharpCodeDocComment(ty: TypeAssertion | string, nestLevel: number) {
let code = '';
const indent = ' '.repeat(nestLevel);
const docComment = typeof ty === 'string' ? ty : ty.docComment;
if (docComment) {
if (0 <= docComment.indexOf('\n')) {
code += `${indent}/**\n${indent} ${
docComment
.split('\n')
.map(x => x.trimLeft())
.join(`\n${indent} `)}\n${indent} */\n`;
} else {
code += `${indent}/** ${docComment} */\n`;
}
}
return code;
}
function formatMemberType(ty: TypeAssertion, ctx: CodegenContext): string {
if (ty.typeName) {
return formatTypeName(ty, ctx, ty.typeName);
} else {
switch (ty.kind) {
case 'primitive':
return generateCSharpCodePrimitive(ty, ctx);
case 'primitive-value':
return generateCSharpCodePrimitiveValue(ty, ctx);
case 'repeated':
return generateCSharpCodeRepeated(ty, ctx);
case 'one-of':
return generateCSharpCodeOneOf(ty, ctx);
default:
return 'object';
}
}
}
function appendOptionalModifier(name: string) {
switch (name) {
case 'decimal': case 'int': case 'double': case 'bool':
return `${name}?`;
default:
return name;
}
}
function isNullableOneOf(ty: OneOfAssertion, ctx: CodegenContext) {
const filtered = ty.oneOf.filter(x => !(
x.kind === 'primitive' && (x.primitiveName === 'null' || x.primitiveName === 'undefined') ||
x.kind === 'primitive-value' && (x.value === null || x.value === void 0)));
return (filtered.length === 1 && ty.oneOf.length !== 1 ? filtered[0] : null) ;
}
function generateCSharpCodePrimitive(ty: PrimitiveTypeAssertion, ctx: CodegenContext) {
// TODO: Function, DateStr, DateTimeStr
switch (ty.primitiveName) {
case 'null': case 'undefined':
return 'object';
case 'integer':
return 'int';
case 'bigint':
return 'decimal';
case 'number':
return 'double';
case 'boolean':
return 'bool';
default:
return ty.primitiveName;
}
}
function generateCSharpCodePrimitiveValue(ty: PrimitiveValueTypeAssertion, ctx: CodegenContext) {
if (ty.value === null || ty.value === void 0) {
return 'object';
}
switch (typeof ty.primitiveName) {
case 'bigint':
return 'decimal';
default:
switch (typeof ty.value) {
case 'number':
return 'double';
case 'string':
return 'string';
case 'boolean':
return 'bool';
default:
return 'object';
}
}
}
function generateCSharpCodeRepeated(ty: RepeatedAssertion, ctx: CodegenContext): string {
return `${formatMemberType(ty.repeated, ctx)}[]`;
}
function generateCSharpCodeSpread(ty: SpreadAssertion, ctx: CodegenContext) {
return '';
}
function generateCSharpCodeSequence(ty: SequenceAssertion, ctx: CodegenContext) {
return 'object[]';
}
function generateCSharpCodeOneOf(ty: OneOfAssertion, ctx: CodegenContext) {
const z = isNullableOneOf(ty, ctx);
if (z) {
return appendOptionalModifier(formatMemberType(z, ctx));
} else {
return 'object';
}
}
function generateCSharpCodeOptional(ty: OptionalAssertion, ctx: CodegenContext) {
return appendOptionalModifier(generateCSharpCodeInner(ty.optional, false, ctx));
}
function generateCSharpCodeEnum(ty: EnumAssertion, ctx: CodegenContext) {
return 'object';
}
function addAttributes(ty: TypeAssertion, ctx: CodegenContext, typeName: string) {
const attrs: string[] = [];
let ty2: TypeAssertion = ty;
if (ty2.kind !== 'optional') {
switch (typeName) {
case 'decimal': case 'int': case 'double': case 'bool':
break;
default:
if (ty2.kind === 'one-of') {
if (! isNullableOneOf(ty2, ctx)) {
attrs.push('Required');
}
} else {
attrs.push('Required');
}
break;
}
ty2 = ty;
}
switch (ty2.kind) {
case 'primitive':
{
if (typeof ty2.minLength === 'number') {
attrs.push(`MinLength(${ty2.minLength})`);
}
if (typeof ty2.maxLength === 'number') {
attrs.push(`MaxLength(${ty2.maxLength})`);
}
if (ty2.minValue !== null && ty2.minValue !== void 0 ||
ty2.maxValue !== null && ty2.maxValue !== void 0) {
switch (ty2.primitiveName) {
case 'string':
attrs.push(`Range(typeof(string), "${
nvl2(ty2.minValue, x => escapeString(x), '')}", "${
nvl2(ty2.maxValue, x => escapeString(x), '\\U00010FFFF')}")`);
break;
case 'bigint':
attrs.push(`Range(typeof(decimal), ${
nvl2(ty2.minValue, x => `new decimal(@"${String(x)}").ToString()`, 'Decimal.MinValue')}, ${
nvl2(ty2.maxValue, x => `new decimal(@"${String(x)}").ToString()`, 'Decimal.MaxValue')})`);
break;
case 'integer':
attrs.push(`Range(${
nvl2(ty2.minValue, x => `(int)${String(x)}`, 'Int32.MinValue')}, ${
nvl2(ty2.maxValue, x => `(int)${String(x)}`, 'Int32.MaxValue')})`);
break;
case 'number':
attrs.push(`Range(${
nvl2(ty2.minValue, x => `(double)${String(x)}`, 'Double.MinValue')}, ${
nvl2(ty2.maxValue, x => `(double)${String(x)}`, 'Double.MaxValue')})`);
break;
}
}
if (ty2.pattern) {
attrs.push(`RegularExpression(@"${ty2.pattern.source.replace(/"/g, '""')}")`);
}
}
break;
case 'repeated':
{
if (typeof ty2.min === 'number') {
attrs.push(`MinLength(${ty2.min})`);
}
if (typeof ty2.max === 'number') {
attrs.push(`MaxLength(${ty2.max})`);
}
}
break;
}
if (0 < attrs.length) {
return `[${attrs.join(', ')}]\n${' '.repeat(ctx.nestLevel + 1)}`;
} else{
return '';
}
}
function generateCSharpCodeObject(ty: ObjectAssertion, isInterface: boolean, ctx: CodegenContext) {
const sep = '\n\n';
const memberLines =
ty.members.filter(x => !(x[2]))
.map(x => {
const typeName =
x[1].typeName ?
formatTypeName(x[1], {...ctx, nestLevel: ctx.nestLevel + 1}, x[1].typeName) :
generateCSharpCodeInner(x[1], false, {...ctx, nestLevel: ctx.nestLevel + 1});
return (
`${formatCSharpCodeDocComment(x[3] || '', ctx.nestLevel + 1)}${
' '.repeat(ctx.nestLevel + 1)}${addAttributes(x[1], ctx, typeName)}public ${
typeName} ${x[0]} { get; set; }`
);
});
if (memberLines.length === 0) {
return (`\n${
' '.repeat(ctx.nestLevel)}{\n${
' '.repeat(ctx.nestLevel)}}`
);
}
return (`\n${
' '.repeat(ctx.nestLevel)}{\n${memberLines.join(sep)}\n${
' '.repeat(ctx.nestLevel)}}`
);
}
function generateCSharpCodeInner(ty: TypeAssertion, isInterface: boolean, ctx: CodegenContext): string {
switch (ty.kind) {
case 'never': case 'any': case 'unknown':
return 'object';
case 'primitive':
return generateCSharpCodePrimitive(ty, ctx);
case 'primitive-value':
return generateCSharpCodePrimitiveValue(ty, ctx);
case 'repeated':
return generateCSharpCodeRepeated(ty, ctx);
case 'spread':
return generateCSharpCodeSpread(ty, ctx);
case 'sequence':
return generateCSharpCodeSequence(ty, ctx);
case 'one-of':
return generateCSharpCodeOneOf(ty, ctx);
case 'optional':
return generateCSharpCodeOptional(ty, ctx);
case 'enum':
return generateCSharpCodeEnum(ty, ctx);
case 'object':
return generateCSharpCodeObject(ty, isInterface, ctx);
case 'symlink':
if (ctx.schema?.has(ty.symlinkTargetName)) {
const target = ctx.schema.get(ty.symlinkTargetName);
switch (target?.ty.kind) {
case 'enum':
return 'object';
}
}
return ty.symlinkTargetName;
case 'operator':
throw new Error(`Unexpected type assertion: ${(ty as any).kind}`);
default:
throw new Error(`Unknown type assertion: ${(ty as any).kind}`);
}
}
export function generateCSharpCode(schema: TypeAssertionMap): string {
let code =
`using System.ComponentModel.DataAnnotations;
namespace Tynder.UserSchema
{
`;
const ctx: CodegenContext = {
nestLevel: 1,
schema,
};
for (const ty of schema.entries()) {
const indent0 = ' '.repeat(ctx.nestLevel);
if (ty[1].ty.kind === 'object') {
// nothing to do
} else if (ty[1].ty.kind === 'enum') {
// nothing to do
} else if (ty[1].ty.kind === 'never' && ty[1].ty.passThruCodeBlock) {
// nothing to do
} else {
code += formatCSharpCodeDocComment(ty[1].ty, ctx.nestLevel);
let tyName = 'System.Object';
switch (ty[1].ty.kind) {
case 'primitive':
switch (ty[1].ty.primitiveName) {
case 'integer':
tyName = 'System.Int32';
break;
case 'bigint':
tyName = 'System.Decimal';
break;
case 'number':
tyName = 'System.Double';
break;
case 'boolean':
tyName = 'System.Boolean';
break;
case 'string':
tyName = 'System.String';
break;
}
break;
case 'primitive-value':
if (ty[1].ty.value !== null && ty[1].ty.value !== void 0) {
switch (typeof ty[1].ty.primitiveName) {
case 'bigint':
tyName = 'System.Decimal';
break;
default:
switch (typeof ty[1].ty.value) {
case 'number':
tyName = 'System.Double';
break;
case 'boolean':
tyName = 'System.Boolean';
break;
case 'string':
tyName = 'System.String';
break;
}
}
}
break;
}
code += `${indent0}using ${ty[0]} = ${tyName};\n\n`;
}
}
let isFirst = true;
for (const ty of schema.entries()) {
const accessModifier = ty[1].exported ? 'public' : 'public';
const indent0 = ' '.repeat(ctx.nestLevel);
const indent1 = ' '.repeat(ctx.nestLevel + 1);
if (ty[1].ty.kind === 'object' || ty[1].ty.kind === 'enum') {
if (isFirst) {
isFirst = false;
code += '\n';
} else {
code += '\n\n';
}
code += formatCSharpCodeDocComment(ty[1].ty, ctx.nestLevel);
}
if (ty[1].ty.kind === 'object') {
code += `${indent0}${accessModifier} class ${ty[0]}${
ty[1].ty.baseTypes && ty[1].ty.baseTypes.length ? ` : ${
ty[1].ty.baseTypes
.filter(x => x.typeName)
.map(x => formatTypeName(x, {...ctx, nestLevel: ctx.nestLevel + 1}, x.typeName as string))
.join(', ')}` : ''} ${
generateCSharpCodeInner(ty[1].ty, true, ctx)}\n`;
} else if (ty[1].ty.kind === 'enum') {
let value: number | null = 0;
code += `${indent0}${accessModifier} static class ${ty[0]}\n${indent0}{\n${
ty[1].ty.values
.map(x => `${
formatCSharpCodeDocComment(x[2] || '', ctx.nestLevel + 1)}${
indent1}${(() => {
if (value !== null && x[1] === value) {
value++;
return `public static double ${x[0]} { get { return ${x[1]}; } }`;
} else {
if (typeof x[1] === 'number') {
value = x[1] + 1;
return `public static double ${x[0]} { get { return ${x[1]}; } }`;
} else {
return `public static string ${x[0]} { get { return "${escapeString(x[1])}"; } }`;
}
}
})()}`)
.join('\n\n')}\n${indent0}}\n`;
} else if (ty[1].ty.kind === 'never' && ty[1].ty.passThruCodeBlock) {
// nothing to do
} else {
// nothing to do
}
}
return code + '}\n';
}