tgapi
Version:
Actual Telegram bot API with Rx-driven updates and full Flow type coverage
351 lines (312 loc) • 8.18 kB
JavaScript
/* @flow */
import { resolve } from 'path'
import { promises } from 'fs'
import { JSDOM } from 'jsdom'
import { CLIEngine } from 'eslint'
import jquery from 'jquery'
import wordwrap from 'wordwrap'
import fetch from 'isomorphic-fetch'
import { toUpperFirst } from '../util'
const generatedPath = resolve(__dirname, '..', 'Bot', 'generated')
const typesFile = resolve(generatedPath, 'apiTypes.js')
const clientFile = resolve(generatedPath, 'BotCore.js')
const cli = new CLIEngine({
fix: true,
useEslintrc: true,
})
const wrap0 = wordwrap(77)
const wrap1 = wordwrap(75)
const wrap2 = wordwrap(73)
const starIndent = (str: string) => `* ${str}`
const regType = /^[A-Z][a-zA-Z0-9]*$/
const regMethod = /^[a-z][a-zA-Z0-9]*$/
const regOptional = /^Optional\. */
const regArray = /^Array of +/
const parseType = (str: string, prefix: string = ''): string => {
if (regArray.test(str)) {
return `$ReadOnlyArray<${parseType(str.replace(regArray, ''), prefix)}>`
}
return str
.split(/ +(?:or|and) +/)
.map(type => {
switch (type) {
case 'Integer':
case 'Float':
case 'Float number':
return 'number'
case 'True':
case 'False':
case 'String':
case 'Boolean':
return type.toLowerCase()
default:
return prefix + type
}
})
.join('|')
}
type Prop = {
name: string,
type: string,
optional: boolean,
description: string,
}
type TypeSpec =
| {
type: 'object',
name: string,
description: $ReadOnlyArray<string>,
props: $ReadOnlyArray<Prop>,
}
| {
type: 'union',
name: string,
description: $ReadOnlyArray<string>,
types: $ReadOnlyArray<string>,
}
| {
type: 'defined',
name: string,
description: $ReadOnlyArray<string>,
value: string,
}
| {
type: 'any',
name: string,
description: $ReadOnlyArray<string>,
}
type MethodSpec = {
name: string,
description: $ReadOnlyArray<string>,
props: $ReadOnlyArray<Prop>,
}
const definedTypes = {
InputFile: `stream$Readable | string`,
}
const serializable = [
'reply_markup',
'media',
'mask_position',
'results',
'shipping_options',
'errors',
]
const renderProps = (props: $ReadOnlyArray<Prop>) => {
const toSerialize = props
.filter(prop => serializable.includes(prop.name))
.map(
prop =>
`${prop.name}: props.${prop.name} && JSON.stringify(props.${
prop.name
})`,
)
return toSerialize.length
? `{ ...props, ${toSerialize.join(', ')} }`
: 'props'
}
// eslint-disable-next-line
;(async () => {
const response = await fetch('https://core.telegram.org/bots/API')
const content = await response.text()
const $: typeof jquery = (jquery(new JSDOM(content).window): any)
const $headers: $ReadOnlyArray<*> = $('#dev_page_content h4')
.toArray()
.map((el: HTMLElement) => $(el))
const getText = el => $(el).text()
const getDescription = header =>
header
.nextUntil('h3, h4, .table', 'p')
.toArray()
.map(getText)
const getTable = header =>
header
.nextUntil('h3, h4', '.table:first')
.find('tr')
.toArray()
.slice(1)
.map(tr =>
$(tr)
.find('td')
.toArray()
.map(getText),
)
const types: $ReadOnlyArray<TypeSpec> = $headers
.filter(header => regType.test(header.text()))
.map(header => {
const name = header.text()
const description = getDescription(header)
const table = getTable(header)
if (table.length) {
return {
name,
description,
type: 'object',
props: getTable(header).map(tr => ({
name: tr[0],
type: tr[1],
optional: regOptional.test(tr[2]),
description: tr[2],
})),
}
}
const list = header
.nextUntil('h3, h4', 'ul:first')
.find('li')
.toArray()
.map(getText)
if (list.length) {
return {
name,
description,
type: 'union',
types: list,
}
}
if (definedTypes[name]) {
return {
type: 'defined',
name,
description,
value: definedTypes[name],
}
}
return { name, description, type: 'any' }
})
const methods: $ReadOnlyArray<MethodSpec> = $headers
.filter(header => regMethod.test(header.text()))
.map(header => ({
name: header.text(),
description: getDescription(header),
props: getTable(header).map(tr => ({
name: tr[0],
type: tr[1],
optional: tr[2] === 'Optional',
description: tr[3],
})),
}))
const flowMethods: $ReadOnlyArray<string> = methods.map(method => {
const haveProps = !!method.props.length
const props = haveProps
? [
'props: {',
method.props
.map(prop =>
[
'/**',
...wrap2(prop.description)
.split('\n')
.map(starIndent),
'*/',
`${prop.name}${prop.optional ? '?' : ''}: ${parseType(
prop.type,
'a.',
)},`,
].join('\n'),
)
.join('\n\n'),
'}',
].join('\n')
: ''
const arr = [
'/**',
`* ${method.name}`,
'*',
method.description
.map(p =>
wrap1(p)
.split('\n')
.map(starIndent)
.join('\n'),
)
.join('\n*\n'),
'*/',
`${method.name}(`,
`${props}${
method.props.length && method.props.every(prop => prop.optional)
? ' = {}'
: ''
}`,
`): Promise<t.Result<r.${toUpperFirst(method.name)}Result>> {`,
`return callMethod(this, '${method.name}'${
haveProps ? `, ${renderProps(method.props)}` : ''
})`,
'}',
]
return arr.join('\n')
})
const flowTypes: $ReadOnlyArray<string> = types.map(type =>
[
'/**',
`* ${type.name}`,
'*',
type.description
.map(p =>
wrap0(p)
.split('\n')
.map(starIndent)
.join('\n'),
)
.join('\n*\n'),
'*/',
...(() => {
switch (type.type) {
case 'object':
return [
`export type ${type.name} = {`,
type.props
.map(prop =>
[
'/**',
...wrap2(prop.description)
.split('\n')
.map(starIndent),
'*/',
`${prop.name}${prop.optional ? '?' : ''}: ${parseType(
prop.type,
)},`,
].join('\n'),
)
.join('\n\n'),
'}',
]
case 'union':
return [`export type ${type.name} = ${type.types.join('|')}`]
case 'defined':
return [`export type ${type.name} = ${type.value}`]
case 'any':
return [`export type ${type.name} = any`]
default:
/* :: (type: empty) */
throw new Error()
}
})(),
'',
].join('\n'),
)
const flowTypesResult = cli.executeOnText(
['/* @flow */', ...flowTypes].join('\n\n'),
typesFile,
)
const flowMethodsResult = cli.executeOnText(
[
'/* @flow */',
'',
'/* :: ',
"import * as t from '../types'",
"import * as a from './apiTypes'",
"import * as r from '../returnTypes'",
'*/',
'',
"import { callMethod } from '../callMethod'",
'',
'export class BotCore {',
flowMethods.join('\n\n'),
'}',
].join('\n'),
clientFile,
)
await Promise.all([
promises.writeFile(typesFile, flowTypesResult.results[0].output, 'utf8'),
promises.writeFile(clientFile, flowMethodsResult.results[0].output, 'utf8'),
])
})()