// Copyright (c) 2019 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'; function formatTypeName(ty: TypeAssertion, ctx: CodegenContext, typeName: string) { if (typeName.includes('.')) { return generateProto3CodeInner(ty, false, ctx); } return typeName; } function formatProto3CodeDocComment(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 generateProto3CodePrimitive(ty, ctx); case 'primitive-value': return generateProto3CodePrimitiveValue(ty, ctx); case 'repeated': return generateProto3CodeRepeated(ty, ctx); case 'one-of': return generateProto3CodeOneOf(ty, ctx); default: return 'object'; } } } function appendOptionalModifier(name: string) { switch (name) { case 'double': return 'google.protobuf.DoubleValue'; case 'int64': return 'google.protobuf.Int64Value'; case 'int32': return 'google.protobuf.Int32Value'; case 'string': return 'google.protobuf.StringValue'; case 'bool': return 'google.protobuf.BoolValue'; 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 generateProto3CodePrimitive(ty: PrimitiveTypeAssertion, ctx: CodegenContext) { switch (ty.primitiveName) { case 'number': return 'double'; case 'integer': return 'int32'; case 'bigint': return 'string'; case 'string': return 'string'; case 'boolean': return 'bool'; case 'undefined': case 'null': default: return 'google.protobuf.Any'; } // TODO: Function, integer, DateStr, DateTimeStr } function generateProto3CodePrimitiveValue(ty: PrimitiveValueTypeAssertion, ctx: CodegenContext) { if (ty.value === null) { return 'google.protobuf.Any'; } if (ty.value === void 0) { return 'google.protobuf.Any'; } switch (typeof ty.value) { case 'number': return 'double'; case 'bigint': return 'string'; case 'string': return 'string'; case 'boolean': return 'bool'; default: return 'google.protobuf.Any'; } } function generateProto3CodeRepeated(ty: RepeatedAssertion, ctx: CodegenContext) { return (`repeated ${ty.repeated.typeName ? formatTypeName(ty.repeated, ctx, ty.repeated.typeName) : ty.repeated.kind === 'repeated' ? 'google.protobuf.Any' : generateProto3CodeInner(ty.repeated, false, ctx)}` ); } function generateProto3CodeSpread(ty: SpreadAssertion, ctx: CodegenContext) { return ''; } function generateProto3CodeSequence(ty: SequenceAssertion, ctx: CodegenContext) { return 'repeated google.protobuf.Any'; } function generateProto3CodeOneOf(ty: OneOfAssertion, ctx: CodegenContext) { const z = isNullableOneOf(ty, ctx); if (z) { return appendOptionalModifier(formatMemberType(z, ctx)); } else { return 'google.protobuf.Any'; } } function generateProto3CodeOptional(ty: OptionalAssertion, ctx: CodegenContext) { return appendOptionalModifier(generateProto3CodeInner(ty.optional, false, ctx)); } function generateProto3CodeEnum(ty: EnumAssertion, ctx: CodegenContext) { return (ty.typeName ? formatTypeName(ty, ctx, ty.typeName) : 'google.protobuf.Any' ); } function generateProto3CodeObject(ty: ObjectAssertion, isInterface: boolean, ctx: CodegenContext) { if (ty.members.length === 0) { return '{}'; } const sep = isInterface ? ';\n' : ',\n'; let count = 1; const memberLines = ty.members .map(x => `${formatProto3CodeDocComment(x[3] || '', ctx.nestLevel + 1)}${ ' '.repeat(ctx.nestLevel + 1)}${ x[1].typeName ? formatTypeName(x[1], {...ctx, nestLevel: ctx.nestLevel + 1}, x[1].typeName) : generateProto3CodeInner(x[1], false, {...ctx, nestLevel: ctx.nestLevel + 1})} ${ x[0]} = ${count++}`); return ( `{\n${memberLines.join(sep)}${sep}${' '.repeat(ctx.nestLevel)}}` ); } function generateProto3CodeInner(ty: TypeAssertion, isInterface: boolean, ctx: CodegenContext): string { switch (ty.kind) { case 'never': case 'any': case 'unknown': return 'google.protobuf.Any'; case 'primitive': return generateProto3CodePrimitive(ty, ctx); case 'primitive-value': return generateProto3CodePrimitiveValue(ty, ctx); case 'repeated': return generateProto3CodeRepeated(ty, ctx); case 'spread': return generateProto3CodeSpread(ty, ctx); case 'sequence': return generateProto3CodeSequence(ty, ctx); case 'one-of': return generateProto3CodeOneOf(ty, ctx); case 'optional': return generateProto3CodeOptional(ty, ctx); case 'enum': return generateProto3CodeEnum(ty, ctx); case 'object': return generateProto3CodeObject(ty, isInterface, ctx); case 'symlink': 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 generateProto3Code(types: TypeAssertionMap): string { let code = ` syntax = "proto3"; import "google/protobuf/wrappers.proto"; import "google/protobuf/any.proto"; `; const ctx = {nestLevel: 0}; for (const ty of types.entries()) { if (ty[1].ty.noOutput) { const indent0 = ' '.repeat(ctx.nestLevel); const indent1 = ' '.repeat(ctx.nestLevel + 1); code += `message ${ty[0]} {\n${indent1}google.protobuf.Any value = 1;\n${indent0}}\n\n`; continue; } code += formatProto3CodeDocComment(ty[1].ty, ctx.nestLevel); if (ty[1].ty.kind === 'object') { code += `message ${ty[0]} ${ generateProto3CodeInner(ty[1].ty, true, ctx)}\n\n`; } else if (ty[1].ty.kind === 'enum') { const indent0 = ' '.repeat(ctx.nestLevel); const indent1 = ' '.repeat(ctx.nestLevel + 1); if (0 < ty[1].ty.values.filter(x => typeof x[1] !== 'number').length) { // NOTE: string enum is not allowed code += `message ${ty[0]} {\n${indent1}google.protobuf.Any value = 1;\n${indent0}}\n\n`; } else { code += `enum ${ty[0]} {\n${ indent1}option allow_alias = true;\n${ ty[1].ty.values.filter(x => x[1] === 0).length === 0 ? `${indent1}${ty[0]}__UNKNOWN__ = 0;\n` : // NOTE: 0 value item is required ''}${ ty[1].ty.values .map(x => `${ formatProto3CodeDocComment(x[2] || '', ctx.nestLevel + 1)}${ indent1}${(() => { if (typeof x[1] === 'number') { return `${ty[0]}_${x[0]} = ${x[1]}`; // NOTE: label namespace is shared by all top-level enum } else { return `${ty[0]}_${x[0]} = '${escapeString(x[1])}'`; // NOTE: string enum is not allowed } })()};\n`) .join('')}${indent0}}\n\n`; } } else if (ty[1].ty.kind === 'never' && ty[1].ty.passThruCodeBlock) { // nothing to do } else { const indent0 = ' '.repeat(ctx.nestLevel); const indent1 = ' '.repeat(ctx.nestLevel + 1); code += `message ${ty[0]} {\n${indent1}${generateProto3CodeInner(ty[1].ty, false, ctx)} value = 1;\n${indent0}}\n\n`; } } return code; }