UNPKG

tdjson

Version:

TypeScript types for libtdjson.so

463 lines (385 loc) 11.7 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import { Buffer } from 'node:buffer'; import { inspect } from 'node:util'; import fetch from 'node-fetch'; import { outdent } from 'outdent'; import invariant from 'invariant'; import * as A from 'arcsecond'; import { pascalCase } from 'pascal-case'; import * as ttyStrings from 'tty-strings'; function wordWrap(text: string): string { return ttyStrings.wordWrap(text, 120); } function trimFilterJoin(strings: string[]): string { return strings.map(s => s.trim()).filter(s => s.length > 0).join(' '); } interface Identifier { readonly type: 'Identifier'; readonly name: string; } const identifierParser = A.regex(/^\w+/).map(name => ({ type: 'Identifier', name, } as const)); interface Comment { readonly type: 'Comment'; readonly tag: string; readonly text: string; } const commentParser = A.sequenceOf([ A.str('//@'), identifierParser, A.everyCharUntil(A.char('\n')), A.many(A.sequenceOf([ A.char('\n'), A.str('//-'), A.everyCharUntil(A.char('\n')), ]).map(([ _0, _1, comment ]) => comment)).map(s => trimFilterJoin(s)), ]).map(([ _0, tag, comment, comment_ ]) => ({ type: 'Comment', tag: tag.name, text: trimFilterJoin([ comment, comment_ ]), } as const)); const commentsParser: A.Parser<Comment[]> = A.many(A.sequenceOf([ A.optionalWhitespace, commentParser, ]).map(([ _, comment ]) => comment)).map(comments => comments.flatMap(comment => { const [ head, ...tail ] = comment.text.split('@'); return [ { ...comment, text: head, } as const, ...tail.map(commentText => { const [ tag, ...textWords ] = commentText.split(' '); return { type: 'Comment', tag, text: textWords.join(' '), } as const; }), ]; })); interface TypeApp { readonly type: 'TypeApplication'; readonly callee: Identifier; readonly argument: Identifier | TypeApp; } const typeAppParser = A.sequenceOf([ identifierParser, A.char('<'), A.recursiveParser(() => typeParser), A.char('>'), ]).map(([ callee, _1, argument ]) => ({ type: 'TypeApplication', callee, argument, } as const)); const typeParser: A.Parser<Identifier | TypeApp> = A.choice([ typeAppParser, identifierParser, ]); interface AnnotatedIdentifier { readonly type: 'AnnotatedIdentifier'; readonly id: Identifier; readonly typeAnnotation: Identifier | TypeApp; } const annotatedIdentifierParser = A.sequenceOf([ identifierParser, A.char(':'), typeParser, ]).map(([ id, _1, typeAnnotation ]) => ({ type: 'AnnotatedIdentifier', id, typeAnnotation, } as const)); const typeVariableParser = A.sequenceOf([ A.char('{'), annotatedIdentifierParser, A.char('}'), ]).map(([ _0, annotatedIdentifier ]) => annotatedIdentifier); const argumentParser = A.choice([ A.char('?'), A.char('#'), A.str('[ t ]'), annotatedIdentifierParser, identifierParser, typeVariableParser, ]); const argumentsParser = A.many(A.sequenceOf([ A.whitespace, argumentParser, ]).map(([ _0, argument ]) => argument)); interface Combinator { type: 'Combinator'; comments: Comment[]; left: { id: Identifier; arguments: Array<string | Identifier | AnnotatedIdentifier>; }; right: { id: Identifier; arguments: Array<string | Identifier | AnnotatedIdentifier>; }; } const combinatorParser: A.Parser<Combinator> = A.sequenceOf([ identifierParser, argumentsParser, A.whitespace, A.char('='), A.whitespace, identifierParser, A.possibly(argumentsParser), ]).map(([ name, arguments_, _2, _3, _4, resultName, resultArguments ]) => ({ type: 'Combinator', comments: [], left: { id: name, arguments: arguments_ ?? [], } as const, right: { id: resultName, arguments: resultArguments ?? [], } as const, })); const tdApiParser: A.Parser<Combinator> = A.sequenceOf([ A.possibly(A.sequenceOf([ commentsParser, A.whitespace, ]).map(([ comments ]) => comments)), combinatorParser, ]).map(([ comments, combinator ]) => ({ ...combinator, comments: [ ...combinator.comments, ...(comments ?? []) ], })); function stringifyTypeAnnotation(typeAnnotation: Identifier | TypeApp): string { if (typeAnnotation.type === 'Identifier') { if ( typeAnnotation.name === 'String' || typeAnnotation.name === 'Int64' || typeAnnotation.name === 'string' || typeAnnotation.name === 'int64' ) { return 'string'; } if ( typeAnnotation.name === 'Double' || typeAnnotation.name === 'Int32' || typeAnnotation.name === 'Int53' || typeAnnotation.name === 'double' || typeAnnotation.name === 'int32' || typeAnnotation.name === 'int53' ) { return 'number'; } if ( typeAnnotation.name === 'Bool' || typeAnnotation.name === 'bool' ) { return 'boolean'; } if ( typeAnnotation.name === 'Bytes' || typeAnnotation.name === 'bytes' ) { return 'string'; } if ( typeAnnotation.name === 'Vector' || typeAnnotation.name === 'vector' ) { return 'Array'; } return pascalCase(typeAnnotation.name); } if (typeAnnotation.type === 'TypeApplication') { return `${stringifyTypeAnnotation(typeAnnotation.callee)}<${stringifyTypeAnnotation(typeAnnotation.argument)}>`; } invariant(false, 'Cannot stringify type annotation: %s', inspect(typeAnnotation)); } function stringifyCommentText(lines: Array<undefined | string>, thisInterfaceName?: string): string { let text = lines.map(line => line?.trim()).filter(Boolean).map(line => { if (line && !line.endsWith('.')) { line += '.'; } return line; }).join('\n'); if (thisInterfaceName) { text = text.replaceAll(/(?<!{)@(\w+)/g, (_, referencedIdentifier: string) => `{@link ${thisInterfaceName}#${referencedIdentifier}}`); } return wordWrap(text); } function stringifyConstructorName(constructor_: Combinator) { return pascalCase(constructor_.left.id.name); } function stringifyConstructorArgument(constructor_: Combinator, argument: string | Identifier | AnnotatedIdentifier) { invariant( typeof argument === 'object' && argument.type === 'AnnotatedIdentifier', 'Can not stringify constructor argument: %s', inspect(argument), ); const interfaceName = stringifyConstructorName(constructor_); const comment = constructor_.comments.find(comment => comment.tag === argument.id.name); const commentText = stringifyCommentText([ comment?.text ], interfaceName); const typeAnnotation = stringifyTypeAnnotation(argument.typeAnnotation); const isOptional = typeAnnotation === 'boolean'; const optionalPropertySign = isOptional ? '?' : ''; return outdent` /** ${commentText} */ ${argument.id.name}${optionalPropertySign}: ${typeAnnotation}; `; } function stringifyConstructor(constructor_: Combinator, isFunction = false) { if ( constructor_.right.id.name === 'Double' || constructor_.right.id.name === 'String' || constructor_.right.id.name === 'Int32' || constructor_.right.id.name === 'Int53' || constructor_.right.id.name === 'Int64' || constructor_.right.id.name === 'Bytes' || constructor_.right.id.name === 'Bool' || constructor_.right.id.name === 'Vector' ) { return ''; } const interfaceName = stringifyConstructorName(constructor_); const constructorCommentText = ( isFunction ? outdent` Request type for {@link Tdjson#${constructor_.left.id.name}}. ` : ( constructor_.right.id.name === interfaceName ? '' : outdent` Subtype of {@link ${constructor_.right.id.name}}. ` ) ); const descriptionComment = constructor_.comments.find(comment => comment.tag === 'description'); const descriptionCommentText = stringifyCommentText([ descriptionComment?.text, constructorCommentText, ], interfaceName); return outdent` /** ${descriptionCommentText} */ export interface ${interfaceName} { '@type': ${JSON.stringify(constructor_.left.id.name)}; ${constructor_.left.arguments.map(argument => stringifyConstructorArgument(constructor_, argument)).join('\n')} } `; } function stringifyFunctionOptionsType(function_: Combinator) { const interfaceName = stringifyConstructorName(function_); return `Omit<${interfaceName}, '@type'>`; } function stringifyFunction(function_: Combinator) { const descriptionComment = function_.comments.find(comment => comment.tag === 'description'); const descriptionCommentText = stringifyCommentText([ descriptionComment?.text, ]); const hasArguments = function_.left.arguments.length > 0; if (!hasArguments) { return outdent` /** ${descriptionCommentText} */ ${function_.left.id.name}(): Promise<${function_.right.id.name}> { return this._request({ '@type': ${JSON.stringify(function_.left.id.name)}, }); } `; } return outdent` /** ${descriptionCommentText} */ ${function_.left.id.name}(options: ${stringifyFunctionOptionsType(function_)}): Promise<${function_.right.id.name}> { return this._request({ ...options, '@type': ${JSON.stringify(function_.left.id.name)}, }); } `; } function stringifyFunctions(functions: Combinator[]) { return outdent` export abstract class Tdjson { ${functions.map(f => stringifyFunction(f)).join('\n')} /** Send a request to the actual libtdjson.so here. Do not forget to handle {@link Error} responses and timeouts. */ protected abstract _request<R extends Request>(message: R): Promise<any>; } `; } function stringifyConstructorUnion(unionName: string, constructors: Iterable<Combinator>) { if (unionName === 'Bool') { return ''; } const constructorsUnion = ( [ ...constructors ] .map(constructor_ => '| ' + stringifyConstructorName(constructor_)) .join('\n') ); return outdent` export type ${unionName} = ${constructorsUnion}; `; } async function main() { const response = await fetch('https://raw.githubusercontent.com/tdlib/td/master/td/generate/scheme/td_api.tl'); let text = await response.text(); text = text.replaceAll(/\n+/g, '\n').replaceAll(/[ \t]+/g, ' '); const [ constructorsText, functionsText ] = text.split('---functions---'); const [ constructors, functions ] = [ constructorsText, functionsText ].map(text => { const lines = text.split(/;\n(?!\/\/-)/).flatMap(line => { line = line.trim(); if (!line) { return []; } return [ line ]; }); const parseResult = lines.flatMap(line => { const lineParseResult = tdApiParser.run(line); const { isError, index } = lineParseResult; if (isError || index !== Buffer.byteLength(line, 'utf8')) { const before = line.slice(0, index); const after = line.slice(index, -1); console.error(lineParseResult, line.length); console.error([ before, after, ].join('<<HERE>>').trim()); invariant(false, 'Parsing failed or did not consume everything'); } return [ lineParseResult.result ]; }); return parseResult; }); const constructorUnions = constructors.reduce((groups, constructor_) => { if (!groups.has(constructor_.right.id.name)) { groups.set(constructor_.right.id.name, new Set()); } const group = groups.get(constructor_.right.id.name)!; group.add(constructor_); return groups; }, new Map<string, Set<Combinator>>()); const typescript = [ '/* eslint-disable @typescript-eslint/no-unsafe-return */', ...constructors.map(c => stringifyConstructor(c)), ...[ ...constructorUnions.entries() ].filter(([ _, group ]) => group.size > 1).map(([ unionName, constructors ]) => stringifyConstructorUnion(unionName, constructors)), ...functions.map(c => stringifyConstructor(c, true)), stringifyConstructorUnion('Request', functions), stringifyFunctions(functions), ].filter(Boolean).join('\n\n'); await fs.writeFile(path.join(__dirname, 'tdjson.ts'), typescript); } void main();