feat(codegen): support bare types and vectors

closes MTQ-48
This commit is contained in:
alina 🌸 2023-06-25 03:09:04 +03:00
parent 754a288c87
commit 99e83b40aa
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
23 changed files with 726 additions and 257 deletions

View file

@ -2,17 +2,44 @@ import { computeConstructorIdFromEntry } from '../ctor-id'
import { TL_PRIMITIVES, TlEntry } from '../types'
import { snakeToCamel } from './utils'
export interface ReaderCodegenOptions {
/**
* Whether to include `flags` field in the result object
* @default false
*/
includeFlags?: boolean
/**
* Name of the variable to use for the readers map
* @default 'm'
*/
variableName?: string
/**
* Whether to include methods in the readers map
*/
includeMethods?: boolean
}
const DEFAULT_OPTIONS: ReaderCodegenOptions = {
includeFlags: false,
variableName: 'm',
includeMethods: false,
}
/**
* Generate binary reader code for a given entry.
*
* @param entry Entry to generate reader for
* @param includeFlags Whether to include `flags` field in the result object
* @param params Options
* @returns Code as a writers map entry
*/
export function generateReaderCodeForTlEntry(
entry: TlEntry,
includeFlags = false,
params = DEFAULT_OPTIONS,
): string {
const { variableName, includeFlags } = { ...DEFAULT_OPTIONS, ...params }
if (entry.id === 0) entry.id = computeConstructorIdFromEntry(entry)
const pre = `${entry.id}:function(r){`
@ -49,20 +76,19 @@ export function generateReaderCodeForTlEntry(
const argName = snakeToCamel(arg.name)
if (arg.predicate) {
const s = arg.predicate.split('.')
if (arg.typeModifiers?.predicate) {
const predicate = arg.typeModifiers.predicate
const s = predicate.split('.')
const fieldName = s[0]
const bitIndex = parseInt(s[1])
if (!(fieldName in flagsFields)) {
throw new Error(
`Invalid predicate: ${arg.predicate} - unknown field (in ${entry.name})`,
`Invalid predicate: ${predicate} - unknown field (in ${entry.name})`,
)
}
if (isNaN(bitIndex) || bitIndex < 0 || bitIndex > 32) {
throw new Error(
`Invalid predicate: ${arg.predicate} - invalid bit`,
)
throw new Error(`Invalid predicate: ${predicate} - invalid bit`)
}
const condition = `${fieldName}&${1 << bitIndex}`
@ -86,30 +112,39 @@ export function generateReaderCodeForTlEntry(
returnCode += `${argName}:`
}
let vector = false
let type = arg.type
const m = type.match(/^[Vv]ector[< ](.+?)[> ]$/)
if (m) {
vector = true
type = m[1]
}
if (type in TL_PRIMITIVES) {
if (type === 'Bool') type = 'boolean'
if (type === 'Bool' || type === 'bool') type = 'boolean'
} else {
type = 'object'
}
let code
let reader = `r.${type}`
const isBare =
arg.typeModifiers?.isBareType || arg.typeModifiers?.isBareUnion
if (vector) {
code = `r.vector(r.${type})`
} else {
code = `r.${type}()`
if (isBare) {
if (!arg.typeModifiers?.constructorId) {
throw new Error(
`Cannot generate reader for ${entry.name}#${arg.name} - no constructor id referenced`,
)
}
reader = `${variableName}[${arg.typeModifiers.constructorId}]`
}
if (arg.predicate) {
let code
if (arg.typeModifiers?.isVector) {
code = `r.vector(${reader})`
} else if (arg.typeModifiers?.isBareVector) {
code = `r.vector(${reader},1)`
} else {
code = `${reader}(${isBare ? 'r' : ''})`
}
if (arg.typeModifiers?.predicate) {
code += ':void 0'
}
@ -127,20 +162,19 @@ export function generateReaderCodeForTlEntry(
* Generate binary reader code for a given schema.
*
* @param entries Entries to generate reader for
* @param varName Name of the variable containing the result
* @param methods Whether to include method readers
* @param params Codegen options
*/
export function generateReaderCodeForTlEntries(
entries: TlEntry[],
varName: string,
methods = true,
params = DEFAULT_OPTIONS,
): string {
let ret = `var ${varName}={\n`
const { variableName, includeMethods } = { ...DEFAULT_OPTIONS, ...params }
let ret = `var ${variableName}={\n`
entries.forEach((entry) => {
if (entry.kind === 'method' && !methods) return
if (entry.kind === 'method' && !includeMethods) return
ret += generateReaderCodeForTlEntry(entry) + '\n'
ret += generateReaderCodeForTlEntry(entry, params) + '\n'
})
return ret + '}'

View file

@ -31,11 +31,6 @@ function fullTypeName(
link = false,
): string {
if (type in PRIMITIVE_TO_TS) return PRIMITIVE_TO_TS[type]
let m
if ((m = type.match(/^[Vv]ector[< ](.+?)[> ]$/))) {
return fullTypeName(m[1], baseNamespace, namespace, method, link) + '[]'
}
const [ns, name] = splitNameToNamespace(type)
let res = baseNamespace
@ -145,7 +140,7 @@ export function generateTypescriptDefinitionsForTlEntry(
ret += ` ${snakeToCamel(arg.name)}`
if (arg.predicate) ret += '?'
if (arg.typeModifiers?.predicate) ret += '?'
let type = arg.type
let typeFinal = false
@ -158,6 +153,10 @@ export function generateTypescriptDefinitionsForTlEntry(
if (!typeFinal) type = fullTypeName(arg.type, baseNamespace)
if (arg.typeModifiers?.isVector || arg.typeModifiers?.isBareVector) {
type += '[]'
}
ret += `: ${type};\n`
})
@ -219,7 +218,10 @@ export function generateTypescriptDefinitionsForTlSchema(
namespace = 'tl',
errors?: TlErrors,
): [string, string] {
let ts = PRELUDE.replace('$NS$', namespace).replace('$LAYER$', String(layer))
let ts = PRELUDE.replace('$NS$', namespace).replace(
'$LAYER$',
String(layer),
)
let js = PRELUDE_JS.replace('$NS$', namespace).replace(
'$LAYER$',
String(layer),

View file

@ -2,8 +2,39 @@ import { computeConstructorIdFromEntry } from '../ctor-id'
import { TL_PRIMITIVES, TlEntry } from '../types'
import { snakeToCamel } from './utils'
export interface WriterCodegenOptions {
/**
* Whether to use `flags` field from the input
* @default false
*/
includeFlags?: boolean
/**
* Name of the variable to use for the writers map
* @default 'm'
*/
variableName?: string
/**
* Whether to include prelude code (function `h`)
*/
includePrelude?: boolean
/**
* Whether to generate bare writer (without constructor id write)
*/
bare?: boolean
}
const DEFAULT_OPTIONS: WriterCodegenOptions = {
includeFlags: false,
variableName: 'm',
includePrelude: true,
bare: false,
}
const TL_WRITER_PRELUDE =
'function h(o, p){' +
'function h(o,p){' +
'var q=o[p];' +
'if(q===void 0)' +
"throw Error('Object '+o._+' is missing required property '+p);" +
@ -11,37 +42,42 @@ const TL_WRITER_PRELUDE =
/**
* Generate writer code for a single entry.
* `h` (has) function should be available
* `h` (has) function from the prelude should be available
*
* @param entry Entry to generate writer for
* @param withFlags Whether to include `flags` field in the result object
* @param params Options
* @returns Code as a readers map entry
*/
export function generateWriterCodeForTlEntry(
entry: TlEntry,
withFlags = false,
params = DEFAULT_OPTIONS,
): string {
const { bare, includeFlags, variableName } = {
...DEFAULT_OPTIONS,
...params,
}
if (entry.id === 0) entry.id = computeConstructorIdFromEntry(entry)
let ret = `'${entry.name}':function(w${
entry.arguments.length ? ',v' : ''
}){`
const name = bare ? entry.id : `'${entry.name}'`
let ret = `${name}:function(w${entry.arguments.length ? ',v' : ''}){`
ret += `w.uint(${entry.id});`
if (!bare) ret += `w.uint(${entry.id});`
const flagsFields: Record<string, 1> = {}
entry.arguments.forEach((arg) => {
if (arg.type === '#') {
ret += `var ${arg.name}=${withFlags ? `v.${arg.name}` : '0'};`
ret += `var ${arg.name}=${includeFlags ? `v.${arg.name}` : '0'};`
entry.arguments.forEach((arg1) => {
const predicate = arg1.typeModifiers?.predicate
let s
if (
!arg1.predicate ||
(s = arg1.predicate.split('.'))[0] !== arg.name
) { return }
if (!predicate || (s = predicate.split('.'))[0] !== arg.name) {
return
}
const arg1Name = snakeToCamel(arg1.name)
@ -49,7 +85,7 @@ export function generateWriterCodeForTlEntry(
if (isNaN(bitIndex) || bitIndex < 0 || bitIndex > 32) {
throw new Error(
`Invalid predicate: ${arg1.predicate} - invalid bit`,
`Invalid predicate: ${predicate} - invalid bit`,
)
}
@ -57,7 +93,10 @@ export function generateWriterCodeForTlEntry(
if (arg1.type === 'true') {
ret += `if(v.${arg1Name}===true)${action}`
} else if (arg1.type.match(/^[Vv]ector/)) {
} else if (
arg1.typeModifiers?.isVector ||
arg1.typeModifiers?.isBareVector
) {
ret += `var _${arg1Name}=v.${arg1Name}&&v.${arg1Name}.length;if(_${arg1Name})${action}`
} else {
ret += `var _${arg1Name}=v.${arg1Name}!==undefined;if(_${arg1Name})${action}`
@ -72,21 +111,16 @@ export function generateWriterCodeForTlEntry(
const argName = snakeToCamel(arg.name)
let vector = false
let type = arg.type
const m = type.match(/^[Vv]ector[< ](.+?)[> ]$/)
if (m) {
vector = true
type = m[1]
}
let accessor = `v.${argName}`
if (arg.predicate) {
if (arg.typeModifiers?.predicate) {
if (type === 'true') return // included in flags
ret += `if(_${argName})`
} else {
ret += `h(v,'${argName}');`
accessor = `h(v,'${argName}')`
}
if (type in TL_PRIMITIVES) {
@ -95,10 +129,26 @@ export function generateWriterCodeForTlEntry(
type = 'object'
}
if (vector) {
ret += `w.vector(w.${type}, v.${argName});`
let writer = `w.${type}`
const isBare =
arg.typeModifiers?.isBareType || arg.typeModifiers?.isBareUnion
if (isBare) {
if (!arg.typeModifiers?.constructorId) {
throw new Error(
`Cannot generate writer for ${entry.name}#${arg.name} - no constructor id referenced`,
)
}
writer = `${variableName}._bare[${arg.typeModifiers.constructorId}]`
}
if (arg.typeModifiers?.isVector) {
ret += `w.vector(${writer},${accessor});`
} else if (arg.typeModifiers?.isBareVector) {
ret += `w.vector(${writer},${accessor},1);`
} else {
ret += `w.${type}(v.${argName});`
ret += `${writer}(${isBare ? 'w,' : ''}${accessor});`
}
})
@ -109,23 +159,47 @@ export function generateWriterCodeForTlEntry(
* Generate writer code for a given TL schema.
*
* @param entries Entries to generate writers for
* @param varName Name of the variable to use for the writers map
* @param prelude Whether to include the prelude (containing `h` function)
* @param withFlags Whether to include `flags` field in the result object
* @param params Codegen options
*/
export function generateWriterCodeForTlEntries(
entries: TlEntry[],
varName: string,
prelude = true,
withFlags = false,
params = DEFAULT_OPTIONS,
): string {
let ret = ''
if (prelude) ret += TL_WRITER_PRELUDE
ret += `var ${varName}={\n`
const { includePrelude, variableName } = { ...DEFAULT_OPTIONS, ...params }
let ret = ''
if (includePrelude) ret += TL_WRITER_PRELUDE
ret += `var ${variableName}={\n`
const usedAsBareIds: Record<number, 1> = {}
entries.forEach((entry) => {
ret += generateWriterCodeForTlEntry(entry, withFlags) + '\n'
ret += generateWriterCodeForTlEntry(entry, params) + '\n'
entry.arguments.forEach((arg) => {
if (arg.typeModifiers?.constructorId) {
usedAsBareIds[arg.typeModifiers.constructorId] = 1
}
})
})
if (Object.keys(usedAsBareIds).length) {
ret += '_bare:{\n'
Object.keys(usedAsBareIds).forEach((id) => {
const entry = entries.find((e) => e.id === parseInt(id))
if (!entry) {
return
}
ret +=
generateWriterCodeForTlEntry(entry, {
...params,
bare: true,
}) + '\n'
})
ret += '}'
}
return ret + '}'
}

View file

@ -1,5 +1,6 @@
import CRC32 from 'crc-32'
import { parseTlToEntries } from './parse'
import { writeTlEntryToString } from './stringify'
import { TlEntry } from './types'
@ -9,19 +10,8 @@ import { TlEntry } from './types'
* @param line Line containing TL entry definition
*/
export function computeConstructorIdFromString(line: string): number {
return (
CRC32.str(
// normalize
line
.replace(
/[{};]|[a-zA-Z0-9_]+:flags\.[0-9]+\?true|#[0-9a-f]{1,8}/g,
'',
)
.replace(/[<>]/g, ' ')
.replace(/ +/g, ' ')
.replace(':bytes', ':string')
.trim(),
) >>> 0
return computeConstructorIdFromEntry(
parseTlToEntries(line, { forIdComputation: true })[0],
)
}

View file

@ -7,6 +7,7 @@ import {
TlFullSchema,
TlSchemaDiff,
} from './types'
import { stringifyArgumentType } from './utils'
/**
* Compute difference between two TL entries.
@ -89,17 +90,16 @@ export function generateTlEntriesDifference(
name: arg.name,
}
if (arg.type !== oldArg.type) {
diff.type = {
old: oldArg.type,
new: arg.type,
}
}
const argStr = stringifyArgumentType(arg.type, arg.typeModifiers)
const oldArgStr = stringifyArgumentType(
oldArg.type,
oldArg.typeModifiers,
)
if (arg.predicate !== oldArg.predicate) {
diff.predicate = {
old: oldArg.predicate,
new: arg.predicate,
if (argStr !== oldArgStr) {
diff.type = {
old: oldArgStr,
new: argStr,
}
}
@ -110,7 +110,7 @@ export function generateTlEntriesDifference(
}
}
if (diff.type || diff.predicate || diff.comment) {
if (diff.type || diff.comment) {
argsDiff.modified.push(diff)
}
})

View file

@ -33,10 +33,10 @@ export function mergeTlEntries(entries: TlEntry[]): TlEntry | string {
if (arg.type === '#') {
flagsLastIndex[arg.name] = idx
}
if (arg.predicate) {
const flagsField = arg.predicate.split('.')[0]
flagsLastIndex[flagsField] = idx
}
// if (arg.predicate) {
// const flagsField = arg.predicate.split('.')[0]
// flagsLastIndex[flagsField] = idx
// }
})
for (let i = 1; i < entries.length; i++) {
@ -55,7 +55,9 @@ export function mergeTlEntries(entries: TlEntry[]): TlEntry | string {
result.name !== entry.name ||
result.type !== entry.type ||
result.id !== ctorId
) { return 'basic info mismatch' }
) {
return 'basic info mismatch'
}
// since we re-calculated id manually, we can skip checking
// generics and arguments, and get straight to merging
@ -72,13 +74,14 @@ export function mergeTlEntries(entries: TlEntry[]): TlEntry | string {
// yay a new arg
// we can only add optional true args, since any others will change id
// ids match, so this must be the case
if (!entryArgument.predicate) {
if (!entryArgument.typeModifiers?.predicate) {
throw new Error('new argument is not optional')
}
// we also need to make sure we put it *after* the respective flags field
const flagsField = entryArgument.predicate.split('.')[0]
const flagsField =
entryArgument.typeModifiers.predicate.split('.')[0]
const targetIdx = flagsLastIndex[flagsField]
// targetIdx *must* exist, otherwise ids wouldn't match

View file

@ -1,19 +1,10 @@
import { computeConstructorIdFromString } from './ctor-id'
import { TL_PRIMITIVES, TlEntry } from './types'
import { parseTdlibStyleComment } from './utils'
import { TL_PRIMITIVES, TlArgument, TlEntry } from './types'
import { parseArgumentType, parseTdlibStyleComment } from './utils'
const SINGLE_REGEX =
/^(.+?)(?:#([0-9a-f]{1,8}))?(?: \?)?(?: {(.+?:.+?)})? ((?:.+? )*)= (.+);$/
function applyPrefix(prefix: string, type: string): string {
if (type in TL_PRIMITIVES) return type
const m = type.match(/^[Vv]ector[< ](.+?)[> ]$/)
if (m) return `Vector<${applyPrefix(prefix, m[1])}>`
return prefix + type
}
/**
* Parse TL schema into a list of entries.
*
@ -50,13 +41,17 @@ export function parseTlToEntries(
prefix?: string
/**
* Whether to apply the prefix to arguments as well
* Whether this invocation is for computing constructor ids.
* If true, the `id` field will be set to 0 for all entries.
*/
applyPrefixToArguments?: boolean
forIdComputation?: boolean
},
): TlEntry[] {
const ret: TlEntry[] = []
const entries: Record<string, TlEntry> = {}
const unions: Record<string, TlEntry[]> = {}
const lines = tl.split('\n')
let currentKind: TlEntry['kind'] = 'class'
@ -124,9 +119,11 @@ export function parseTlToEntries(
return
}
const typeIdNum = typeId ?
parseInt(typeId, 16) :
computeConstructorIdFromString(line)
let typeIdNum = typeId ? parseInt(typeId, 16) : 0
if (typeIdNum === 0 && !params?.forIdComputation) {
typeIdNum = computeConstructorIdFromString(line)
}
const argsParsed =
args && !args.match(/\[ [a-z]+ ]/i) ?
@ -138,7 +135,7 @@ export function parseTlToEntries(
const entry: TlEntry = {
kind: currentKind,
name: prefix + typeName,
name: typeName,
id: typeIdNum,
type,
arguments: [],
@ -153,31 +150,18 @@ export function parseTlToEntries(
}
if (argsParsed.length) {
argsParsed.forEach(([name, typ]) => {
let [predicate, type] = typ.split('?')
if (!type) {
// no predicate, `predicate` is the type
if (params?.applyPrefixToArguments) {
predicate = applyPrefix(prefix, predicate)
}
entry.arguments.push({
name,
type: predicate,
})
} else {
// there is a predicate
if (params?.applyPrefixToArguments) {
type = applyPrefix(prefix, type)
}
entry.arguments.push({
name,
type,
predicate,
})
argsParsed.forEach(([name, type_]) => {
const [type, modifiers] = parseArgumentType(type_)
const item: TlArgument = {
name,
type,
}
if (Object.keys(modifiers).length) {
item.typeModifiers = modifiers
}
entry.arguments.push(item)
})
}
@ -201,11 +185,59 @@ export function parseTlToEntries(
}
ret.push(entry)
entries[entry.name] = entry
if (entry.kind === 'class') {
if (!unions[entry.type]) unions[entry.type] = []
unions[entry.type].push(entry)
}
})
if (currentComment && params?.onOrphanComment) {
params.onOrphanComment(currentComment)
}
// post-process:
// - find arguments where type is not a union and put corresponding modifiers
// - apply prefix
ret.forEach((entry, entryIdx) => {
entry.arguments.forEach((arg) => {
const type = arg.type
if (type in TL_PRIMITIVES) {
return
}
if (type in unions && arg.typeModifiers?.isBareUnion) {
if (unions[type].length !== 1) {
const err = new Error(
`Union ${type} has more than one entry, cannot use it like %${type} (found in ${entry.name}#${arg.name})`,
)
if (params?.panicOnError) {
throw err
} else if (params?.onError) {
params.onError(err, '', entryIdx)
} else {
console.warn(err)
}
}
arg.typeModifiers.constructorId = unions[type][0].id
} else if (type in entries) {
if (!arg.typeModifiers) arg.typeModifiers = {}
arg.typeModifiers.isBareType = true
arg.typeModifiers.constructorId = entries[type].id
if (prefix) {
arg.type = prefix + arg.type
}
}
})
if (prefix) {
entry.name = prefix + entry.name
}
})
return ret
}

View file

@ -29,11 +29,21 @@ export function patchRuntimeTlSchema(
} {
const entries = parseTlToEntries(schema)
const readersCode = generateReaderCodeForTlEntries(entries, '_', false)
const writersCode = generateWriterCodeForTlEntries(entries, '_', true)
const readersCode = generateReaderCodeForTlEntries(entries, {
variableName: '_',
includeMethods: false,
})
const writersCode = generateWriterCodeForTlEntries(entries, {
variableName: '_',
includePrelude: true,
})
const newReaders = evalForResult<TlReaderMap>(readersCode.replace('var _=', 'return'))
const newWriters = evalForResult<TlWriterMap>(writersCode.replace('var _=', 'return'))
const newReaders = evalForResult<TlReaderMap>(
readersCode.replace('var _=', 'return'),
)
const newWriters = evalForResult<TlWriterMap>(
writersCode.replace('var _=', 'return'),
)
return {
readerMap: {

View file

@ -1,4 +1,5 @@
import { TlEntry } from './types'
import { stringifyArgumentType } from './utils'
function normalizeType(s: string): string {
return s
@ -39,18 +40,22 @@ export function writeTlEntryToString(
}
for (const arg of entry.arguments) {
if (forIdComputation && arg.predicate && arg.type === 'true') continue
if (
forIdComputation &&
arg.typeModifiers?.predicate &&
arg.type === 'true'
) {
continue
}
str += arg.name + ':'
if (arg.predicate) {
str += arg.predicate + '?'
}
const type = stringifyArgumentType(arg.type, arg.typeModifiers)
if (forIdComputation) {
str += normalizeType(arg.type) + ' '
str += normalizeType(type) + ' '
} else {
str += arg.type + ' '
str += type + ' '
}
}

View file

@ -1,3 +1,62 @@
/**
* Modifiers for {@link TlArgument.type}
*/
export interface TlArgumentModifiers {
/**
* Predicate of the argument
* @example `flags.3`
*/
predicate?: string
/**
* Whether `type` is in fact a `Vector`
* @example `type=long, isVector=true => Vector<long>
*/
isVector?: boolean
/**
* Whether `type` is in fact a `vector` (a bare vector, not to be confused with `Vector`).
*
* The difference between `Vector<T>` and `vector<T>` is that in the latter case
* constructor ID of the vector itself (1cb5c415) is omitted
*
* @example `type=long, isVector=false, isBareVector=true => vector<long>
*/
isBareVector?: boolean
/**
* Whether `type` is in fact a "bare" type (a %-prefixed type) from within a union.
*
* The difference between `T` and `%T` is that in the latter case
* constructor ID of `T` is omitted.
*
* Note: If there are more than 1 types within that union, this syntax is not valid.
*
* @example `type=Message, isBare=true => %Message
*/
isBareUnion?: boolean
/**
* Whether `type` is in fact a "bare" type (a %-prefixed type)
*
* The difference between `T` and `%T` is that in the latter case
* constructor ID of `T` is omitted.
*
* The difference with {@link isBareUnion} is in the kind of `type`.
* For {@link isBareUnion}, `type` is a name of a union (e.g. `Message`),
* for {@link isBareType} it is a name of a type (e.g. `message`).
*/
isBareType?: boolean
/**
* For simplicity, when {@link isBareUnion} or {@link isBareType} is true,
* this field contains the constructor ID of the type being referenced.
*
* May still be undefined if the constructor ID is not known.
*/
constructorId?: number
}
/**
* An argument of a TL entry
*/
@ -8,15 +67,14 @@ export interface TlArgument {
name: string
/**
* Type of the argument
* Type of the argument. Usually a name of a Union, but not always
*/
type: string
/**
* Predicate of the argument
* @example `flags.3`
* Modifiers for {@link type}
*/
predicate?: string
typeModifiers?: TlArgumentModifiers
/**
* Comment of the argument
@ -268,11 +326,6 @@ export interface TlArgumentDiff {
*/
type?: PropertyDiff<string>
/**
* Predicate of the argument diff
*/
predicate?: PropertyDiff<string | undefined>
/**
* Comment of the argument diff
*/

View file

@ -1,4 +1,4 @@
import { TlEntry } from './types'
import { TlArgumentModifiers, TlEntry } from './types'
/**
* Split qualified TL entry name into namespace and name
@ -60,3 +60,43 @@ export function groupTlEntriesByNamespace(
return ret
}
export function stringifyArgumentType(
type: string,
modifiers?: TlArgumentModifiers,
) {
if (!modifiers) return type
let ret = type
if (modifiers?.isBareUnion) ret = `%${ret}`
if (modifiers?.isVector) ret = `Vector<${ret}>`
else if (modifiers?.isBareVector) ret = `vector<${ret}>`
if (modifiers.predicate) ret = `${modifiers.predicate}?${ret}`
return ret
}
export function parseArgumentType(type: string): [string, TlArgumentModifiers] {
const modifiers: TlArgumentModifiers = {}
const [predicate, type_] = type.split('?')
if (type_) {
modifiers.predicate = predicate
type = type_
}
if (type.startsWith('Vector<')) {
modifiers.isVector = true
type = type.substring(7, type.length - 1)
} else if (type.startsWith('vector<') || type.startsWith('%vector<')) {
modifiers.isBareVector = true
type = type.substring(7, type.length - 1)
}
if (type.startsWith('%')) {
modifiers.isBareUnion = true
type = type.substring(1)
}
return [type, modifiers]
}

View file

@ -1,12 +1,11 @@
import { expect } from 'chai'
import { describe, it } from 'mocha'
import { generateReaderCodeForTlEntry } from '../../src/codegen/reader'
import { parseTlToEntries } from '../../src/parse'
import { generateReaderCodeForTlEntry, parseTlToEntries } from '../../src'
describe('generateReaderCodeForTlEntry', () => {
const test = (tl: string, ...js: string[]) => {
const entry = parseTlToEntries(tl)[0]
const entry = parseTlToEntries(tl).slice(-1)[0]
expect(generateReaderCodeForTlEntry(entry)).eq(
`${entry.id}:function(r){${js.join('')}},`,
)
@ -139,9 +138,38 @@ describe('generateReaderCodeForTlEntry', () => {
)
})
it('generates code for bare types', () => {
test(
'message#0949d9dc = Message;\n' +
'msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;',
'return{',
"_:'msg_container',",
'messages:r.vector(m[155834844],1),',
'}',
)
test(
'future_salt#0949d9dc = FutureSalt;\n' +
'future_salts#ae500895 salts:Vector<future_salt> current:FutureSalt = FutureSalts;',
'return{',
"_:'future_salts',",
'salts:r.vector(m[155834844]),',
'current:r.object(),',
'}',
)
test(
'future_salt#0949d9dc = FutureSalt;\n' +
'future_salts#ae500895 salts:vector<future_salt> current:future_salt = FutureSalts;',
'return{',
"_:'future_salts',",
'salts:r.vector(m[155834844],1),',
'current:m[155834844](r),',
'}',
)
})
it('generates code with raw flags for constructors with flags', () => {
const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0]
expect(generateReaderCodeForTlEntry(entry, true)).eq(
expect(generateReaderCodeForTlEntry(entry, { includeFlags: true })).eq(
`${entry.id}:function(r){${[
'var flags=r.uint(),',
'flags2=r.uint();',

View file

@ -4,9 +4,9 @@ import { describe, it } from 'mocha'
import {
generateTypescriptDefinitionsForTlEntry,
generateTypescriptDefinitionsForTlSchema,
} from '../../src/codegen/types'
import { parseTlToEntries } from '../../src/parse'
import { parseFullTlSchema } from '../../src/schema'
parseFullTlSchema,
parseTlToEntries,
} from '../../src'
describe('generateTypescriptDefinitionsForTlEntry', () => {
const test = (tl: string, ...ts: string[]) => {
@ -112,7 +112,7 @@ describe('generateTypescriptDefinitionsForTlEntry', () => {
it('wraps long comments', () => {
test(
'// This is a test constructor with a very very very very very very very very long comment\n' +
'test = Test;',
'test = Test;',
'/**',
' * This is a test constructor with a very very very very very',
' * very very very long comment',
@ -124,8 +124,8 @@ describe('generateTypescriptDefinitionsForTlEntry', () => {
test(
'---functions---\n' +
'// This is a test method with a very very very very very very very very long comment\n' +
'test = Test;',
'// This is a test method with a very very very very very very very very long comment\n' +
'test = Test;',
'/**',
' * This is a test method with a very very very very very very',
' * very very long comment',
@ -141,7 +141,7 @@ describe('generateTypescriptDefinitionsForTlEntry', () => {
it('should not break @link tags', () => {
test(
'// This is a test constructor with a very long comment {@link whatever} more text\n' +
'test = Test;',
'test = Test;',
'/**',
' * This is a test constructor with a very long comment',
' * {@link whatever} more text',
@ -156,7 +156,7 @@ describe('generateTypescriptDefinitionsForTlEntry', () => {
it('writes generic types', () => {
test(
'---functions---\ninvokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;',
'interface RawInvokeWithoutUpdatesRequest<X extends tl.TlObject> {',
'interface RawInvokeWithoutUpdatesRequest<X extends tl.TlObject = tl.TlObject> {',
" _: 'invokeWithoutUpdates';",
' query: X;',
'}',

View file

@ -1,12 +1,15 @@
import { expect } from 'chai'
import { describe, it } from 'mocha'
import { generateWriterCodeForTlEntry } from '../../src/codegen/writer'
import { parseTlToEntries } from '../../src/parse'
import {
generateWriterCodeForTlEntries,
generateWriterCodeForTlEntry,
parseTlToEntries,
} from '../../src'
describe('generateWriterCodeForTlEntry', () => {
const test = (tl: string, ...js: string[]) => {
const entry = parseTlToEntries(tl)[0]
const entry = parseTlToEntries(tl).slice(-1)[0]
expect(generateWriterCodeForTlEntry(entry)).eq(
`'${entry.name}':function(w${
entry.arguments.length ? ',v' : ''
@ -21,30 +24,21 @@ describe('generateWriterCodeForTlEntry', () => {
it('generates code for constructors with simple arguments', () => {
test(
'inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;',
"h(v,'dcId');",
'w.int(v.dcId);',
"h(v,'id');",
'w.long(v.id);',
"h(v,'accessHash');",
'w.long(v.accessHash);',
"w.int(h(v,'dcId'));",
"w.long(h(v,'id'));",
"w.long(h(v,'accessHash'));",
)
test(
'contact#145ade0b user_id:long mutual:Bool = Contact;',
"h(v,'userId');",
'w.long(v.userId);',
"h(v,'mutual');",
'w.boolean(v.mutual);',
"w.long(h(v,'userId'));",
"w.boolean(h(v,'mutual'));",
)
test(
'maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;',
"h(v,'n');",
'w.int(v.n);',
"h(v,'x');",
'w.double(v.x);',
"h(v,'y');",
'w.double(v.y);',
"h(v,'zoom');",
'w.double(v.zoom);',
"w.int(h(v,'n'));",
"w.double(h(v,'x'));",
"w.double(h(v,'y'));",
"w.double(h(v,'zoom'));",
)
})
@ -65,8 +59,7 @@ describe('generateWriterCodeForTlEntry', () => {
'var _timeout=v.timeout!==undefined;',
'if(_timeout)flags|=2;',
'w.uint(flags);',
"h(v,'pts');",
'w.int(v.pts);',
"w.int(h(v,'pts'));",
'if(_timeout)w.int(v.timeout);',
)
})
@ -79,8 +72,7 @@ describe('generateWriterCodeForTlEntry', () => {
'var _timeout=v.timeout!==undefined;',
'if(_timeout)flags|=2;',
'w.uint(flags);',
"h(v,'pts');",
'w.int(v.pts);',
"w.int(h(v,'pts'));",
'if(_timeout)w.int(v.timeout);',
'var flags2=0;',
'if(v.canDeleteChannel===true)flags2|=1;',
@ -91,12 +83,9 @@ describe('generateWriterCodeForTlEntry', () => {
it('generates code for constructors with vector arguments', () => {
test(
'contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector<Chat> users:Vector<User> = contacts.ResolvedPeer;',
"h(v,'peer');",
'w.object(v.peer);',
"h(v,'chats');",
'w.vector(w.object, v.chats);',
"h(v,'users');",
'w.vector(w.object, v.users);',
"w.object(h(v,'peer'));",
"w.vector(w.object,h(v,'chats'));",
"w.vector(w.object,h(v,'users'));",
)
})
@ -107,25 +96,55 @@ describe('generateWriterCodeForTlEntry', () => {
'var _entities=v.entities&&v.entities.length;',
'if(_entities)flags|=8;',
'w.uint(flags);',
"h(v,'message');",
'w.string(v.message);',
'if(_entities)w.vector(w.object, v.entities);',
"w.string(h(v,'message'));",
'if(_entities)w.vector(w.object,v.entities);',
)
})
it('generates code for constructors with generics', () => {
test(
'invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;',
"h(v,'layer');",
'w.int(v.layer);',
"h(v,'query');",
'w.object(v.query);',
"w.int(h(v,'layer'));",
"w.object(h(v,'query'));",
)
})
it('generates code for bare vectors', () => {
test(
'message#0949d9dc = Message;\n' +
'msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;',
"w.vector(m._bare[155834844],h(v,'messages'),1);",
)
test(
'future_salt#0949d9dc = FutureSalt;\n' +
'future_salts#ae500895 salts:Vector<future_salt> current:FutureSalt = FutureSalts;',
"w.vector(m._bare[155834844],h(v,'salts'));",
"w.object(h(v,'current'));",
)
})
it('generates code for bare types', () => {
const entries = parseTlToEntries(
'future_salt#0949d9dc salt:bytes = FutureSalt;\n' +
'future_salts#ae500895 salts:vector<future_salt> current:future_salt = FutureSalts;',
)
expect(
generateWriterCodeForTlEntries(entries, { includePrelude: false }),
).eq(
`
var m={
'future_salt':function(w,v){w.uint(155834844);w.bytes(h(v,'salt'));},
'future_salts':function(w,v){w.uint(2924480661);w.vector(m._bare[155834844],h(v,'salts'),1);m._bare[155834844](w,h(v,'current'));},
_bare:{
155834844:function(w,v){w.bytes(h(v,'salt'));},
}}`.replace(/^\s+/gm, ''),
)
})
it('generates code with raw flags for constructors with flags', () => {
const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0]
expect(generateWriterCodeForTlEntry(entry, true)).eq(
expect(generateWriterCodeForTlEntry(entry, { includeFlags: true })).eq(
`'${entry.name}':function(w,v){${[
`w.uint(${entry.id});`,
'var flags=v.flags;',

View file

@ -4,8 +4,9 @@ import { describe, it } from 'mocha'
import {
computeConstructorIdFromEntry,
computeConstructorIdFromString,
} from '../src/ctor-id'
import { TlEntry } from '../src/types'
TlArgument,
TlEntry,
} from '../src'
describe('computeConstructorIdFromString', () => {
const test = (tl: string, expected: number) => {
@ -75,8 +76,10 @@ describe('computeConstructorIdFromEntry', () => {
return {
name: a[0],
type: t[1],
predicate: t[0],
}
typeModifiers: {
predicate: t[0],
},
} satisfies TlArgument
}
return {

View file

@ -99,9 +99,9 @@ describe('generateTlEntriesDifference', () => {
},
{
name: 'egg',
predicate: {
old: 'flags.0',
new: 'flags.1',
type: {
old: 'flags.0?Egg',
new: 'flags.1?Egg',
},
},
],

View file

@ -44,18 +44,19 @@ describe('mergeTlEntries', () => {
'test flags:# baz:flags.1?true = Test;',
'test#e86481ba flags:# foo:flags.0?true bar:flags.0?true baz:flags.1?true = Test;',
)
// ordering of optional flags should not matter
test(
'test flags:# foo:flags.0?true = Test;\n' +
'test flags:# bar:flags.0?true = Test;\n' +
'test flags:# baz:flags.1?true = Test;',
'test#e86481ba flags:# foo:flags.0?true bar:flags.0?true baz:flags.1?true = Test;',
'test#e86481ba flags:# bar:flags.0?true baz:flags.1?true foo:flags.0?true = Test;',
)
test(
'test flags:# foo:flags.0?true = Test;\n' +
'test flags:# foo:flags.0?true bar:flags.0?true = Test;\n' +
'test flags:# baz:flags.1?true = Test;\n' +
'test flags:# bar:flags.0?true baz:flags.1?true = Test;',
'test#e86481ba flags:# foo:flags.0?true bar:flags.0?true baz:flags.1?true = Test;',
'test#e86481ba flags:# bar:flags.0?true baz:flags.1?true foo:flags.0?true = Test;',
)
})

View file

@ -1,12 +1,15 @@
import { expect } from 'chai'
import { describe, it } from 'mocha'
import { parseTlToEntries } from '../src/parse'
import { TlEntry } from '../src/types'
import { parseTlToEntries, TlEntry } from '../src'
describe('tl parser', () => {
const test = (tl: string, expected: TlEntry[]) => {
expect(parseTlToEntries(tl)).eql(expected)
const test = (
tl: string,
expected: TlEntry[],
params?: Parameters<typeof parseTlToEntries>[1],
) => {
expect(parseTlToEntries(tl, params)).eql(expected)
}
it('skips empty lines and comments', () => {
@ -69,6 +72,132 @@ boolTrue#997275b5 = Bool;
])
})
it('parses vectors', () => {
test('msg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq;', [
{
kind: 'class',
name: 'msg_resend_req',
id: 0x7d861a08,
type: 'MsgResendReq',
arguments: [
{
name: 'msg_ids',
type: 'long',
typeModifiers: {
isVector: true,
},
},
],
},
])
})
it('parses bare vectors', () => {
// note: not from schema, schema uses bare `future_salt` instead
test(
'future_salts#ae500895 req_msg_id:long now:int salts:vector<FutureSalt> = FutureSalts;',
[
{
kind: 'class',
name: 'future_salts',
id: 0xae500895,
type: 'FutureSalts',
arguments: [
{
name: 'req_msg_id',
type: 'long',
},
{
name: 'now',
type: 'int',
},
{
name: 'salts',
type: 'FutureSalt',
typeModifiers: {
isBareVector: true,
},
},
],
},
],
)
})
it('parses bare unions', () => {
test(
'message#0949d9dc = Message;\n' + // stub so we can reference it
'msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;',
[
{
kind: 'class',
name: 'message',
id: 0x0949d9dc,
type: 'Message',
arguments: [],
},
{
kind: 'class',
name: 'msg_container',
id: 0x73f1f8dc,
type: 'MessageContainer',
arguments: [
{
name: 'messages',
type: 'Message',
typeModifiers: {
isBareVector: true,
isBareUnion: true,
constructorId: 0x0949d9dc,
},
},
],
},
],
)
})
it('parses bare types', () => {
test(
'future_salt#0949d9dc = FutureSalt;\n' + // stub so we can reference it
'future_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts;',
[
{
kind: 'class',
name: 'future_salt',
id: 0x0949d9dc,
type: 'FutureSalt',
arguments: [],
},
{
kind: 'class',
name: 'future_salts',
id: 0xae500895,
type: 'FutureSalts',
arguments: [
{
name: 'req_msg_id',
type: 'long',
},
{
name: 'now',
type: 'int',
},
{
name: 'salts',
type: 'future_salt',
typeModifiers: {
isBareVector: true,
isBareType: true,
constructorId: 0x0949d9dc,
},
},
],
},
],
)
})
it('parses methods with arguments', () => {
test(
'---functions---\nauth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization;',
@ -148,7 +277,7 @@ boolTrue#997275b5 = Bool;
it('parses predicates', () => {
test(
'help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:Vector<Chat> users:Vector<User> psa_type:flags.1?string psa_message:flags.2?string = help.PromoData;',
'help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer psa_type:flags.1?string psa_message:flags.2?string = help.PromoData;',
[
{
kind: 'class',
@ -163,7 +292,9 @@ boolTrue#997275b5 = Bool;
{
name: 'proxy',
type: 'true',
predicate: 'flags.0',
typeModifiers: {
predicate: 'flags.0',
},
},
{
name: 'expires',
@ -173,23 +304,19 @@ boolTrue#997275b5 = Bool;
name: 'peer',
type: 'Peer',
},
{
name: 'chats',
type: 'Vector<Chat>',
},
{
name: 'users',
type: 'Vector<User>',
},
{
name: 'psa_type',
type: 'string',
predicate: 'flags.1',
typeModifiers: {
predicate: 'flags.1',
},
},
{
name: 'psa_message',
type: 'string',
predicate: 'flags.2',
typeModifiers: {
predicate: 'flags.2',
},
},
],
},
@ -299,4 +426,42 @@ users.getUsers id:Vector<InputUser> = Vector<User>;
'yet another at the end',
])
})
it('applies prefix to constructors', () => {
test(
'future_salt#0949d9dc = FutureSalt;\n' + // stub to reference
'future_salts#ae500895 salts:vector<future_salt> current:FutureSalt = FutureSalts;',
[
{
kind: 'class',
name: 'mt_future_salt',
id: 0x0949d9dc,
type: 'FutureSalt',
arguments: [],
},
{
kind: 'class',
name: 'mt_future_salts',
id: 0xae500895,
type: 'FutureSalts',
arguments: [
{
name: 'salts',
type: 'mt_future_salt',
typeModifiers: {
isBareVector: true,
isBareType: true,
constructorId: 0x0949d9dc,
},
},
{
name: 'current',
type: 'FutureSalt', // prefix is not applied to non-constructors
},
],
},
],
{ prefix: 'mt_' },
)
})
})

View file

@ -2,7 +2,7 @@ import { expect } from 'chai'
import { describe, it } from 'mocha'
import { writeTlEntryToString } from '../src/stringify'
import { TlEntry } from '../src/types'
import { TlArgument, TlEntry } from '../src/types'
describe('writeTlEntryToString', () => {
const make = (name: string, type: string, ...args: string[]): TlEntry => ({
@ -18,14 +18,16 @@ describe('writeTlEntryToString', () => {
return {
name: a[0],
type: t[1],
predicate: t[0],
}
typeModifiers: {
predicate: t[0],
},
} satisfies TlArgument
}
return {
name: a[0],
type: t[0],
}
} satisfies TlArgument
}),
})

View file

@ -1,7 +1,8 @@
{
"What is this?": [
"It is guaranteed that user/chat/channel ids fit in int53,",
"so we can safely replace `long` with `int53` there.",
"as well as file size (up to 4 gigs),",
"so we can safely replace `long` with `int53` for simpler usage from within JS.",
"Note: this is a non-exhaustive list",
"When contributing, please maintain alphabetical key ordering"
],

File diff suppressed because one or more lines are too long

View file

@ -24,10 +24,7 @@ async function main() {
const schema = await fetchMtprotoSchema()
console.log('Parsing...')
let entries = parseTlToEntries(schema, {
prefix: 'mt_',
applyPrefixToArguments: true,
})
let entries = parseTlToEntries(schema, { prefix: 'mt_' })
// remove manually parsed types
entries = entries.filter(

View file

@ -56,11 +56,16 @@ async function generateReaders(
) {
console.log('Generating readers...')
let code = generateReaderCodeForTlEntries(apiSchema.entries, 'r', false)
let code = generateReaderCodeForTlEntries(apiSchema.entries, {
variableName: 'm',
includeMethods: false,
})
const mtpCode = generateReaderCodeForTlEntries(mtpSchema.entries, '')
code = code.substring(0, code.length - 1) + mtpCode.substring(7)
code += '\nexports.default = r;'
const mtpCode = generateReaderCodeForTlEntries(mtpSchema.entries, {
variableName: 'm',
})
code = code.substring(0, code.length - 1) + mtpCode.substring(8)
code += '\nexports.default = m;'
await writeFile(OUT_READERS_FILE, ESM_PRELUDE + code)
}
@ -71,11 +76,16 @@ async function generateWriters(
) {
console.log('Generating writers...')
let code = generateWriterCodeForTlEntries(apiSchema.entries, 'r')
let code = generateWriterCodeForTlEntries(apiSchema.entries, {
variableName: 'm',
})
const mtpCode = generateWriterCodeForTlEntries(mtpSchema.entries, '', false)
const mtpCode = generateWriterCodeForTlEntries(mtpSchema.entries, {
variableName: 'm',
includePrelude: false,
})
code = code.substring(0, code.length - 1) + mtpCode.substring(7)
code += '\nexports.default = r;'
code += '\nexports.default = m;'
await writeFile(OUT_WRITERS_FILE, ESM_PRELUDE + code)
}