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

View file

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

View file

@ -2,6 +2,37 @@ import { computeConstructorIdFromEntry } from '../ctor-id'
import { TL_PRIMITIVES, TlEntry } from '../types' import { TL_PRIMITIVES, TlEntry } from '../types'
import { snakeToCamel } from './utils' 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 = const TL_WRITER_PRELUDE =
'function h(o,p){' + 'function h(o,p){' +
'var q=o[p];' + 'var q=o[p];' +
@ -11,37 +42,42 @@ const TL_WRITER_PRELUDE =
/** /**
* Generate writer code for a single entry. * 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 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 * @returns Code as a readers map entry
*/ */
export function generateWriterCodeForTlEntry( export function generateWriterCodeForTlEntry(
entry: TlEntry, entry: TlEntry,
withFlags = false, params = DEFAULT_OPTIONS,
): string { ): string {
const { bare, includeFlags, variableName } = {
...DEFAULT_OPTIONS,
...params,
}
if (entry.id === 0) entry.id = computeConstructorIdFromEntry(entry) if (entry.id === 0) entry.id = computeConstructorIdFromEntry(entry)
let ret = `'${entry.name}':function(w${ const name = bare ? entry.id : `'${entry.name}'`
entry.arguments.length ? ',v' : '' 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> = {} const flagsFields: Record<string, 1> = {}
entry.arguments.forEach((arg) => { entry.arguments.forEach((arg) => {
if (arg.type === '#') { 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) => { entry.arguments.forEach((arg1) => {
const predicate = arg1.typeModifiers?.predicate
let s let s
if ( if (!predicate || (s = predicate.split('.'))[0] !== arg.name) {
!arg1.predicate || return
(s = arg1.predicate.split('.'))[0] !== arg.name }
) { return }
const arg1Name = snakeToCamel(arg1.name) const arg1Name = snakeToCamel(arg1.name)
@ -49,7 +85,7 @@ export function generateWriterCodeForTlEntry(
if (isNaN(bitIndex) || bitIndex < 0 || bitIndex > 32) { if (isNaN(bitIndex) || bitIndex < 0 || bitIndex > 32) {
throw new Error( 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') { if (arg1.type === 'true') {
ret += `if(v.${arg1Name}===true)${action}` 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}` ret += `var _${arg1Name}=v.${arg1Name}&&v.${arg1Name}.length;if(_${arg1Name})${action}`
} else { } else {
ret += `var _${arg1Name}=v.${arg1Name}!==undefined;if(_${arg1Name})${action}` ret += `var _${arg1Name}=v.${arg1Name}!==undefined;if(_${arg1Name})${action}`
@ -72,21 +111,16 @@ export function generateWriterCodeForTlEntry(
const argName = snakeToCamel(arg.name) const argName = snakeToCamel(arg.name)
let vector = false
let type = arg.type let type = arg.type
const m = type.match(/^[Vv]ector[< ](.+?)[> ]$/)
if (m) { let accessor = `v.${argName}`
vector = true
type = m[1]
}
if (arg.predicate) { if (arg.typeModifiers?.predicate) {
if (type === 'true') return // included in flags if (type === 'true') return // included in flags
ret += `if(_${argName})` ret += `if(_${argName})`
} else { } else {
ret += `h(v,'${argName}');` accessor = `h(v,'${argName}')`
} }
if (type in TL_PRIMITIVES) { if (type in TL_PRIMITIVES) {
@ -95,10 +129,26 @@ export function generateWriterCodeForTlEntry(
type = 'object' type = 'object'
} }
if (vector) { let writer = `w.${type}`
ret += `w.vector(w.${type}, v.${argName});` 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 { } 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. * Generate writer code for a given TL schema.
* *
* @param entries Entries to generate writers for * @param entries Entries to generate writers for
* @param varName Name of the variable to use for the writers map * @param params Codegen options
* @param prelude Whether to include the prelude (containing `h` function)
* @param withFlags Whether to include `flags` field in the result object
*/ */
export function generateWriterCodeForTlEntries( export function generateWriterCodeForTlEntries(
entries: TlEntry[], entries: TlEntry[],
varName: string, params = DEFAULT_OPTIONS,
prelude = true,
withFlags = false,
): string { ): string {
let ret = '' const { includePrelude, variableName } = { ...DEFAULT_OPTIONS, ...params }
if (prelude) ret += TL_WRITER_PRELUDE
ret += `var ${varName}={\n`
let ret = ''
if (includePrelude) ret += TL_WRITER_PRELUDE
ret += `var ${variableName}={\n`
const usedAsBareIds: Record<number, 1> = {}
entries.forEach((entry) => { 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 + '}' return ret + '}'
} }

View file

@ -1,5 +1,6 @@
import CRC32 from 'crc-32' import CRC32 from 'crc-32'
import { parseTlToEntries } from './parse'
import { writeTlEntryToString } from './stringify' import { writeTlEntryToString } from './stringify'
import { TlEntry } from './types' import { TlEntry } from './types'
@ -9,19 +10,8 @@ import { TlEntry } from './types'
* @param line Line containing TL entry definition * @param line Line containing TL entry definition
*/ */
export function computeConstructorIdFromString(line: string): number { export function computeConstructorIdFromString(line: string): number {
return ( return computeConstructorIdFromEntry(
CRC32.str( parseTlToEntries(line, { forIdComputation: true })[0],
// 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
) )
} }

View file

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

View file

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

View file

@ -1,19 +1,10 @@
import { computeConstructorIdFromString } from './ctor-id' import { computeConstructorIdFromString } from './ctor-id'
import { TL_PRIMITIVES, TlEntry } from './types' import { TL_PRIMITIVES, TlArgument, TlEntry } from './types'
import { parseTdlibStyleComment } from './utils' import { parseArgumentType, parseTdlibStyleComment } from './utils'
const SINGLE_REGEX = const SINGLE_REGEX =
/^(.+?)(?:#([0-9a-f]{1,8}))?(?: \?)?(?: {(.+?:.+?)})? ((?:.+? )*)= (.+);$/ /^(.+?)(?:#([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. * Parse TL schema into a list of entries.
* *
@ -50,13 +41,17 @@ export function parseTlToEntries(
prefix?: string 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[] { ): TlEntry[] {
const ret: TlEntry[] = [] const ret: TlEntry[] = []
const entries: Record<string, TlEntry> = {}
const unions: Record<string, TlEntry[]> = {}
const lines = tl.split('\n') const lines = tl.split('\n')
let currentKind: TlEntry['kind'] = 'class' let currentKind: TlEntry['kind'] = 'class'
@ -124,9 +119,11 @@ export function parseTlToEntries(
return return
} }
const typeIdNum = typeId ? let typeIdNum = typeId ? parseInt(typeId, 16) : 0
parseInt(typeId, 16) :
computeConstructorIdFromString(line) if (typeIdNum === 0 && !params?.forIdComputation) {
typeIdNum = computeConstructorIdFromString(line)
}
const argsParsed = const argsParsed =
args && !args.match(/\[ [a-z]+ ]/i) ? args && !args.match(/\[ [a-z]+ ]/i) ?
@ -138,7 +135,7 @@ export function parseTlToEntries(
const entry: TlEntry = { const entry: TlEntry = {
kind: currentKind, kind: currentKind,
name: prefix + typeName, name: typeName,
id: typeIdNum, id: typeIdNum,
type, type,
arguments: [], arguments: [],
@ -153,31 +150,18 @@ export function parseTlToEntries(
} }
if (argsParsed.length) { if (argsParsed.length) {
argsParsed.forEach(([name, typ]) => { argsParsed.forEach(([name, type_]) => {
let [predicate, type] = typ.split('?') const [type, modifiers] = parseArgumentType(type_)
const item: TlArgument = {
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, name,
type, type,
predicate,
})
} }
if (Object.keys(modifiers).length) {
item.typeModifiers = modifiers
}
entry.arguments.push(item)
}) })
} }
@ -201,11 +185,59 @@ export function parseTlToEntries(
} }
ret.push(entry) 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) { if (currentComment && params?.onOrphanComment) {
params.onOrphanComment(currentComment) 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 return ret
} }

View file

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

View file

@ -1,4 +1,5 @@
import { TlEntry } from './types' import { TlEntry } from './types'
import { stringifyArgumentType } from './utils'
function normalizeType(s: string): string { function normalizeType(s: string): string {
return s return s
@ -39,18 +40,22 @@ export function writeTlEntryToString(
} }
for (const arg of entry.arguments) { 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 + ':' str += arg.name + ':'
if (arg.predicate) { const type = stringifyArgumentType(arg.type, arg.typeModifiers)
str += arg.predicate + '?'
}
if (forIdComputation) { if (forIdComputation) {
str += normalizeType(arg.type) + ' ' str += normalizeType(type) + ' '
} else { } 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 * An argument of a TL entry
*/ */
@ -8,15 +67,14 @@ export interface TlArgument {
name: string name: string
/** /**
* Type of the argument * Type of the argument. Usually a name of a Union, but not always
*/ */
type: string type: string
/** /**
* Predicate of the argument * Modifiers for {@link type}
* @example `flags.3`
*/ */
predicate?: string typeModifiers?: TlArgumentModifiers
/** /**
* Comment of the argument * Comment of the argument
@ -268,11 +326,6 @@ export interface TlArgumentDiff {
*/ */
type?: PropertyDiff<string> type?: PropertyDiff<string>
/**
* Predicate of the argument diff
*/
predicate?: PropertyDiff<string | undefined>
/** /**
* Comment of the argument diff * 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 * Split qualified TL entry name into namespace and name
@ -60,3 +60,43 @@ export function groupTlEntriesByNamespace(
return ret 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 { expect } from 'chai'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { generateReaderCodeForTlEntry } from '../../src/codegen/reader' import { generateReaderCodeForTlEntry, parseTlToEntries } from '../../src'
import { parseTlToEntries } from '../../src/parse'
describe('generateReaderCodeForTlEntry', () => { describe('generateReaderCodeForTlEntry', () => {
const test = (tl: string, ...js: string[]) => { const test = (tl: string, ...js: string[]) => {
const entry = parseTlToEntries(tl)[0] const entry = parseTlToEntries(tl).slice(-1)[0]
expect(generateReaderCodeForTlEntry(entry)).eq( expect(generateReaderCodeForTlEntry(entry)).eq(
`${entry.id}:function(r){${js.join('')}},`, `${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', () => { it('generates code with raw flags for constructors with flags', () => {
const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0] const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0]
expect(generateReaderCodeForTlEntry(entry, true)).eq( expect(generateReaderCodeForTlEntry(entry, { includeFlags: true })).eq(
`${entry.id}:function(r){${[ `${entry.id}:function(r){${[
'var flags=r.uint(),', 'var flags=r.uint(),',
'flags2=r.uint();', 'flags2=r.uint();',

View file

@ -4,9 +4,9 @@ import { describe, it } from 'mocha'
import { import {
generateTypescriptDefinitionsForTlEntry, generateTypescriptDefinitionsForTlEntry,
generateTypescriptDefinitionsForTlSchema, generateTypescriptDefinitionsForTlSchema,
} from '../../src/codegen/types' parseFullTlSchema,
import { parseTlToEntries } from '../../src/parse' parseTlToEntries,
import { parseFullTlSchema } from '../../src/schema' } from '../../src'
describe('generateTypescriptDefinitionsForTlEntry', () => { describe('generateTypescriptDefinitionsForTlEntry', () => {
const test = (tl: string, ...ts: string[]) => { const test = (tl: string, ...ts: string[]) => {
@ -156,7 +156,7 @@ describe('generateTypescriptDefinitionsForTlEntry', () => {
it('writes generic types', () => { it('writes generic types', () => {
test( test(
'---functions---\ninvokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;', '---functions---\ninvokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;',
'interface RawInvokeWithoutUpdatesRequest<X extends tl.TlObject> {', 'interface RawInvokeWithoutUpdatesRequest<X extends tl.TlObject = tl.TlObject> {',
" _: 'invokeWithoutUpdates';", " _: 'invokeWithoutUpdates';",
' query: X;', ' query: X;',
'}', '}',

View file

@ -1,12 +1,15 @@
import { expect } from 'chai' import { expect } from 'chai'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { generateWriterCodeForTlEntry } from '../../src/codegen/writer' import {
import { parseTlToEntries } from '../../src/parse' generateWriterCodeForTlEntries,
generateWriterCodeForTlEntry,
parseTlToEntries,
} from '../../src'
describe('generateWriterCodeForTlEntry', () => { describe('generateWriterCodeForTlEntry', () => {
const test = (tl: string, ...js: string[]) => { const test = (tl: string, ...js: string[]) => {
const entry = parseTlToEntries(tl)[0] const entry = parseTlToEntries(tl).slice(-1)[0]
expect(generateWriterCodeForTlEntry(entry)).eq( expect(generateWriterCodeForTlEntry(entry)).eq(
`'${entry.name}':function(w${ `'${entry.name}':function(w${
entry.arguments.length ? ',v' : '' entry.arguments.length ? ',v' : ''
@ -21,30 +24,21 @@ describe('generateWriterCodeForTlEntry', () => {
it('generates code for constructors with simple arguments', () => { it('generates code for constructors with simple arguments', () => {
test( test(
'inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;', 'inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;',
"h(v,'dcId');", "w.int(h(v,'dcId'));",
'w.int(v.dcId);', "w.long(h(v,'id'));",
"h(v,'id');", "w.long(h(v,'accessHash'));",
'w.long(v.id);',
"h(v,'accessHash');",
'w.long(v.accessHash);',
) )
test( test(
'contact#145ade0b user_id:long mutual:Bool = Contact;', 'contact#145ade0b user_id:long mutual:Bool = Contact;',
"h(v,'userId');", "w.long(h(v,'userId'));",
'w.long(v.userId);', "w.boolean(h(v,'mutual'));",
"h(v,'mutual');",
'w.boolean(v.mutual);',
) )
test( test(
'maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;', 'maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;',
"h(v,'n');", "w.int(h(v,'n'));",
'w.int(v.n);', "w.double(h(v,'x'));",
"h(v,'x');", "w.double(h(v,'y'));",
'w.double(v.x);', "w.double(h(v,'zoom'));",
"h(v,'y');",
'w.double(v.y);',
"h(v,'zoom');",
'w.double(v.zoom);',
) )
}) })
@ -65,8 +59,7 @@ describe('generateWriterCodeForTlEntry', () => {
'var _timeout=v.timeout!==undefined;', 'var _timeout=v.timeout!==undefined;',
'if(_timeout)flags|=2;', 'if(_timeout)flags|=2;',
'w.uint(flags);', 'w.uint(flags);',
"h(v,'pts');", "w.int(h(v,'pts'));",
'w.int(v.pts);',
'if(_timeout)w.int(v.timeout);', 'if(_timeout)w.int(v.timeout);',
) )
}) })
@ -79,8 +72,7 @@ describe('generateWriterCodeForTlEntry', () => {
'var _timeout=v.timeout!==undefined;', 'var _timeout=v.timeout!==undefined;',
'if(_timeout)flags|=2;', 'if(_timeout)flags|=2;',
'w.uint(flags);', 'w.uint(flags);',
"h(v,'pts');", "w.int(h(v,'pts'));",
'w.int(v.pts);',
'if(_timeout)w.int(v.timeout);', 'if(_timeout)w.int(v.timeout);',
'var flags2=0;', 'var flags2=0;',
'if(v.canDeleteChannel===true)flags2|=1;', 'if(v.canDeleteChannel===true)flags2|=1;',
@ -91,12 +83,9 @@ describe('generateWriterCodeForTlEntry', () => {
it('generates code for constructors with vector arguments', () => { it('generates code for constructors with vector arguments', () => {
test( test(
'contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector<Chat> users:Vector<User> = contacts.ResolvedPeer;', 'contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector<Chat> users:Vector<User> = contacts.ResolvedPeer;',
"h(v,'peer');", "w.object(h(v,'peer'));",
'w.object(v.peer);', "w.vector(w.object,h(v,'chats'));",
"h(v,'chats');", "w.vector(w.object,h(v,'users'));",
'w.vector(w.object, v.chats);',
"h(v,'users');",
'w.vector(w.object, v.users);',
) )
}) })
@ -107,8 +96,7 @@ describe('generateWriterCodeForTlEntry', () => {
'var _entities=v.entities&&v.entities.length;', 'var _entities=v.entities&&v.entities.length;',
'if(_entities)flags|=8;', 'if(_entities)flags|=8;',
'w.uint(flags);', 'w.uint(flags);',
"h(v,'message');", "w.string(h(v,'message'));",
'w.string(v.message);',
'if(_entities)w.vector(w.object,v.entities);', 'if(_entities)w.vector(w.object,v.entities);',
) )
}) })
@ -116,16 +104,47 @@ describe('generateWriterCodeForTlEntry', () => {
it('generates code for constructors with generics', () => { it('generates code for constructors with generics', () => {
test( test(
'invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;', 'invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;',
"h(v,'layer');", "w.int(h(v,'layer'));",
'w.int(v.layer);', "w.object(h(v,'query'));",
"h(v,'query');", )
'w.object(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', () => { it('generates code with raw flags for constructors with flags', () => {
const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0] const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0]
expect(generateWriterCodeForTlEntry(entry, true)).eq( expect(generateWriterCodeForTlEntry(entry, { includeFlags: true })).eq(
`'${entry.name}':function(w,v){${[ `'${entry.name}':function(w,v){${[
`w.uint(${entry.id});`, `w.uint(${entry.id});`,
'var flags=v.flags;', 'var flags=v.flags;',

View file

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

View file

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

View file

@ -44,18 +44,19 @@ describe('mergeTlEntries', () => {
'test flags:# baz:flags.1?true = Test;', '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:# foo:flags.0?true bar:flags.0?true baz:flags.1?true = Test;',
) )
// ordering of optional flags should not matter
test( test(
'test flags:# foo:flags.0?true = Test;\n' + 'test flags:# foo:flags.0?true = Test;\n' +
'test flags:# bar:flags.0?true = Test;\n' + 'test flags:# bar:flags.0?true = Test;\n' +
'test flags:# baz:flags.1?true = Test;', '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(
'test flags:# foo:flags.0?true = Test;\n' + 'test flags:# foo:flags.0?true = Test;\n' +
'test flags:# foo:flags.0?true bar: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:# baz:flags.1?true = Test;\n' +
'test flags:# bar:flags.0?true baz:flags.1?true = Test;', '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 { expect } from 'chai'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { parseTlToEntries } from '../src/parse' import { parseTlToEntries, TlEntry } from '../src'
import { TlEntry } from '../src/types'
describe('tl parser', () => { describe('tl parser', () => {
const test = (tl: string, expected: TlEntry[]) => { const test = (
expect(parseTlToEntries(tl)).eql(expected) tl: string,
expected: TlEntry[],
params?: Parameters<typeof parseTlToEntries>[1],
) => {
expect(parseTlToEntries(tl, params)).eql(expected)
} }
it('skips empty lines and comments', () => { 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', () => { it('parses methods with arguments', () => {
test( test(
'---functions---\nauth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization;', '---functions---\nauth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization;',
@ -148,7 +277,7 @@ boolTrue#997275b5 = Bool;
it('parses predicates', () => { it('parses predicates', () => {
test( 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', kind: 'class',
@ -163,8 +292,10 @@ boolTrue#997275b5 = Bool;
{ {
name: 'proxy', name: 'proxy',
type: 'true', type: 'true',
typeModifiers: {
predicate: 'flags.0', predicate: 'flags.0',
}, },
},
{ {
name: 'expires', name: 'expires',
type: 'int', type: 'int',
@ -173,24 +304,20 @@ boolTrue#997275b5 = Bool;
name: 'peer', name: 'peer',
type: 'Peer', type: 'Peer',
}, },
{
name: 'chats',
type: 'Vector<Chat>',
},
{
name: 'users',
type: 'Vector<User>',
},
{ {
name: 'psa_type', name: 'psa_type',
type: 'string', type: 'string',
typeModifiers: {
predicate: 'flags.1', predicate: 'flags.1',
}, },
},
{ {
name: 'psa_message', name: 'psa_message',
type: 'string', type: 'string',
typeModifiers: {
predicate: 'flags.2', predicate: 'flags.2',
}, },
},
], ],
}, },
], ],
@ -299,4 +426,42 @@ users.getUsers id:Vector<InputUser> = Vector<User>;
'yet another at the end', '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 { describe, it } from 'mocha'
import { writeTlEntryToString } from '../src/stringify' import { writeTlEntryToString } from '../src/stringify'
import { TlEntry } from '../src/types' import { TlArgument, TlEntry } from '../src/types'
describe('writeTlEntryToString', () => { describe('writeTlEntryToString', () => {
const make = (name: string, type: string, ...args: string[]): TlEntry => ({ const make = (name: string, type: string, ...args: string[]): TlEntry => ({
@ -18,14 +18,16 @@ describe('writeTlEntryToString', () => {
return { return {
name: a[0], name: a[0],
type: t[1], type: t[1],
typeModifiers: {
predicate: t[0], predicate: t[0],
} },
} satisfies TlArgument
} }
return { return {
name: a[0], name: a[0],
type: t[0], type: t[0],
} } satisfies TlArgument
}), }),
}) })

View file

@ -1,7 +1,8 @@
{ {
"What is this?": [ "What is this?": [
"It is guaranteed that user/chat/channel ids fit in int53,", "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", "Note: this is a non-exhaustive list",
"When contributing, please maintain alphabetical key ordering" "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() const schema = await fetchMtprotoSchema()
console.log('Parsing...') console.log('Parsing...')
let entries = parseTlToEntries(schema, { let entries = parseTlToEntries(schema, { prefix: 'mt_' })
prefix: 'mt_',
applyPrefixToArguments: true,
})
// remove manually parsed types // remove manually parsed types
entries = entries.filter( entries = entries.filter(

View file

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