First release

This commit is contained in:
Sys-001 2020-02-01 19:07:36 +01:00
parent e533722af2
commit c8de8ba74f
5 changed files with 791 additions and 0 deletions

22
composer.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "sys-001/tgbotapi-gen",
"description": "Utility to extract scheme from Telegram Bot API webpage.",
"type": "project",
"autoload": {
"psr-4": {
"TGBotApi\\": "src/"
}
},
"require": {
"php": "^7.2",
"ext-json": "*",
"nette/php-generator": "^3.3",
"paquettg/php-html-parser": "^2.0"
},
"authors": [
{
"name": "sys-001",
"email": "honfu7891@etlgr.com"
}
]
}

259
composer.lock generated Normal file
View File

@ -0,0 +1,259 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e0c67734f3ab978c6aebf8a35453ba15",
"packages": [
{
"name": "nette/php-generator",
"version": "v3.3.3",
"source": {
"type": "git",
"url": "https://github.com/nette/php-generator.git",
"reference": "a4ff22c91681fefaa774cf952a2b69c2ec9477c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/php-generator/zipball/a4ff22c91681fefaa774cf952a2b69c2ec9477c1",
"reference": "a4ff22c91681fefaa774cf952a2b69c2ec9477c1",
"shasum": ""
},
"require": {
"nette/utils": "^2.4.2 || ^3.0",
"php": ">=7.1"
},
"require-dev": {
"nette/tester": "^2.0",
"phpstan/phpstan": "^0.12",
"tracy/tracy": "^2.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.3-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.4 features.",
"homepage": "https://nette.org",
"keywords": [
"code",
"nette",
"php",
"scaffolding"
],
"time": "2020-01-20T11:40:42+00:00"
},
{
"name": "nette/utils",
"version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "d6cd63d77dd9a85c3a2fae707e1255e44c2bc182"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/d6cd63d77dd9a85c3a2fae707e1255e44c2bc182",
"reference": "d6cd63d77dd9a85c3a2fae707e1255e44c2bc182",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"nette/tester": "~2.0",
"phpstan/phpstan": "^0.12",
"tracy/tracy": "^2.3"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize() and toAscii()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()",
"ext-xml": "to use Strings::length() etc. when mbstring is not available"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0",
"GPL-3.0"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"time": "2020-01-03T18:13:31+00:00"
},
{
"name": "paquettg/php-html-parser",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/paquettg/php-html-parser.git",
"reference": "668c770fc5724ea3f15b8791435f054835be8d5e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paquettg/php-html-parser/zipball/668c770fc5724ea3f15b8791435f054835be8d5e",
"reference": "668c770fc5724ea3f15b8791435f054835be8d5e",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-mbstring": "*",
"ext-zlib": "*",
"paquettg/string-encode": "~1.0.0",
"php": ">=7.1"
},
"require-dev": {
"infection/infection": "^0.13.4",
"mockery/mockery": "^1.2",
"phan/phan": "^2.4",
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^7.5.1"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPHtmlParser\\": "src/PHPHtmlParser"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gilles Paquette",
"email": "paquettg@gmail.com",
"homepage": "http://gillespaquette.ca"
}
],
"description": "An HTML DOM parser. It allows you to manipulate HTML. Find tags on an HTML page with selectors just like jQuery.",
"homepage": "https://github.com/paquettg/php-html-parser",
"keywords": [
"dom",
"html",
"parser"
],
"time": "2020-01-20T12:59:15+00:00"
},
{
"name": "paquettg/string-encode",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/paquettg/string-encoder.git",
"reference": "a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paquettg/string-encoder/zipball/a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee",
"reference": "a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5.1"
},
"type": "library",
"autoload": {
"psr-0": {
"stringEncode": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gilles Paquette",
"email": "paquettg@gmail.com",
"homepage": "http://gillespaquette.ca"
}
],
"description": "Facilitating the process of altering string encoding in PHP.",
"homepage": "https://github.com/paquettg/string-encoder",
"keywords": [
"charset",
"encoding",
"string"
],
"time": "2018-12-21T02:25:09+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.2",
"ext-json": "*"
},
"platform-dev": []
}

7
main.php Normal file
View File

@ -0,0 +1,7 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$json_scheme = TGBotApi\Generator::toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
echo $json_scheme;

265
src/Generator.php Normal file
View File

@ -0,0 +1,265 @@
<?php
namespace TGBotApi;
use PHPHtmlParser\Dom;
class Generator
{
private const EMPTY_FIELDS = [
'deleteWebhook',
'getWebhookInfo',
'getMe',
'InputFile',
'InputMedia',
'InlineQueryResult',
'InputMessageContent',
'PassportElementError',
'CallbackGame'
];
private const BOOL_RETURNS = [
'answerShippingQuery',
'answerPreCheckoutQuery'
];
/**
* @param string $target_directory
* @param string $namespace_prefix
* @return bool
* @throws \Exception
*/
public static function toClasses(string $target_directory = '', string $namespace_prefix = ''): bool
{
$target_directory = self::getTargetDirectory($target_directory);
mkdir($target_directory . '/Methods', 0755);
mkdir($target_directory . '/Types', 0755);
try {
$stub_provider = new StubProvider($namespace_prefix);
$code = $stub_provider->generateCode(self::extractScheme());
foreach ($code['methods'] as $class_name => $method) {
file_put_contents($target_directory . '/Methods/' . $class_name . '.php', $method);
}
foreach ($code['types'] as $class_name => $type) {
file_put_contents($target_directory . '/Types/' . $class_name . '.php', $type);
}
} catch (\Exception $e) {
return false;
}
return true;
}
private static function getTargetDirectory(string $target_directory): string
{
mkdir($target_directory, 0755);
$target_directory = realpath($target_directory);
if (false == $target_directory) {
$target_directory = __DIR__ . '/generated';
if (!file_exists($target_directory)) {
mkdir($target_directory, 0755);
}
}
return $target_directory;
}
/**
* @return array
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\CircularException
* @throws \PHPHtmlParser\Exceptions\CurlException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws \PHPHtmlParser\Exceptions\ParentNotFoundException
* @throws \PHPHtmlParser\Exceptions\StrictException
*/
private static function extractScheme(): array
{
$dom = new Dom;
$dom->loadFromURL('https://core.telegram.org/bots/api');
$elements = $dom->find('h4');
$i = 0;
$data = [];
foreach ($elements as $element) {
if (false === strpos($name = $element->text, ' ')) {
$is_method = self::isMethod($name);
$path = $is_method ? 'methods' : 'types';
$empty = in_array($name, self::EMPTY_FIELDS);
/* @var Dom $fields_table */
$fields_table = $dom->find('table')[$i];
$unparsed_fields = $fields_table->find('tbody')->find('tr');
/* @var Dom\AbstractNode $element */
/** @noinspection PhpUndefinedFieldInspection */
$data[$path][] = self::generateElement($name, $element->nextSibling()->nextSibling()->innerHtml,
($empty ? null : $unparsed_fields), $is_method);
if (!$empty) {
$i++;
}
}
}
return $data;
}
private static function isMethod(string $name): bool
{
$first_letter = substr($name, 0, 1);
return (strtolower($first_letter) == $first_letter);
}
/**
* @param string $name
* @param string $description
* @param Dom\Collection|null $unparsed_fields
* @param bool $is_method
* @return array
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\CircularException
* @throws \PHPHtmlParser\Exceptions\CurlException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws \PHPHtmlParser\Exceptions\StrictException
*/
private static function generateElement(
string $name,
string $description,
?Dom\Collection $unparsed_fields,
bool $is_method
): array {
$fields = self::parseFields($unparsed_fields, $is_method);
if (!$is_method) {
return [
'name' => $name,
'description' => htmlspecialchars_decode(strip_tags($description), ENT_QUOTES),
'fields' => $fields
];
}
$return_types = self::parseReturnTypes($description);
if (empty($return_types) and in_array($name, self::BOOL_RETURNS)) {
$return_types[] = 'bool';
}
return [
'name' => $name,
'description' => htmlspecialchars_decode(strip_tags($description), ENT_QUOTES),
'fields' => $fields,
'return_types' => $return_types
];
}
/**
* @param Dom\Collection|null $fields
* @param bool $is_method
* @return array
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
*/
private static function parseFields(?Dom\Collection $fields, bool $is_method): array
{
$parsed_fields = [];
$fields = $fields ?? [];
foreach ($fields as $field) {
/* @var Dom $field */
$field_data = $field->find('td');
$parsed_data = [
'name' => $field_data[0]->text,
'type' => strip_tags($field_data[1]->innerHtml)
];
if ($is_method) {
$parsed_data['required'] = ($field_data[2]->text == 'Yes');
$parsed_data['types'] = self::parseMethodFieldTypes($parsed_data['type']);
unset($parsed_data['type']);
$parsed_data['description'] = htmlspecialchars_decode(strip_tags($field_data[3]->innerHtml ?? $field_data[3]->text ?? ''),
ENT_QUOTES);
} else {
$parsed_data['type'] = self::parseObjectFieldType($parsed_data['type']);
$parsed_data['description'] = htmlspecialchars_decode(strip_tags($field_data[2]->innerHtml),
ENT_QUOTES);
}
$parsed_fields[] = $parsed_data;
}
return $parsed_fields;
}
/**
* @param string $raw_type
* @return array
*/
private static function parseMethodFieldTypes(string $raw_type): array
{
$types = explode(' or ', $raw_type);
$parsed_types = [];
foreach ($types as $type) {
$type = trim(str_replace(['number', 'of'], '', $type));
$multiples_count = substr_count(strtolower($type), 'array');
$parsed_types[] = trim(str_replace(['Array', 'Integer', 'String', 'Boolean', 'Float'],
['', 'int', 'string', 'bool', 'float'], $type)) . str_repeat('[]', $multiples_count);
}
return $parsed_types;
}
/**
* @param string $raw_type
* @return string
*/
private static function parseObjectFieldType(string $raw_type): string
{
$type = trim(str_replace(['number', 'of'], '', $raw_type));
$multiples_count = substr_count(strtolower($type), 'array');
return trim(str_replace(['Array', 'Integer', 'String', 'Boolean', 'Float'],
['', 'int', 'string', 'bool', 'float'], $type)) . str_repeat('[]', $multiples_count);
}
/**
* @param string $description
* @return array
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\CircularException
* @throws \PHPHtmlParser\Exceptions\CurlException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws \PHPHtmlParser\Exceptions\StrictException
*/
private static function parseReturnTypes(string $description): array
{
$return_types = [];
$phrases = explode('.', $description);
$phrases = array_filter($phrases, function ($phrase) {
return (false !== stripos($phrase, 'returns') or false !== stripos($phrase, 'is returned'));
});
foreach ($phrases as $phrase) {
$dom = new Dom;
$dom->load($phrase);
$a = $dom->find('a');
$em = $dom->find('em');
foreach ($a as $element) {
if ($element->text == 'Messages') {
$return_types[] = 'Message[]';
continue;
}
$multiples_count = substr_count(strtolower($phrase), 'array');
$return_types[] = $element->text . str_repeat('[]', $multiples_count);
}
foreach ($em as $element) {
if (in_array($element->text, ['False', 'force', 'Array'])) {
continue;
}
$type = str_replace(['True', 'Int', 'String'], ['bool', 'int', 'string'], $element->text);
$return_types[] = $type;
}
}
return $return_types;
}
/**
* @param int $options
* @return string
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\CircularException
* @throws \PHPHtmlParser\Exceptions\CurlException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws \PHPHtmlParser\Exceptions\ParentNotFoundException
* @throws \PHPHtmlParser\Exceptions\StrictException
*/
public static function toJson(int $options = 0): string
{
$scheme = self::extractScheme();
return json_encode($scheme, $options);
}
}

238
src/StubProvider.php Normal file
View File

@ -0,0 +1,238 @@
<?php
namespace TGBotApi;
use Nette\{PhpGenerator, PhpGenerator\Type};
class StubProvider
{
private $namespace_prefix = '';
private $methods = [];
private $types = [];
/**
* StubProvider constructor.
* @param string $namespace_prefix
* @throws \Exception
*/
public function __construct(string $namespace_prefix = '')
{
if (substr($namespace_prefix, -1) == '\\') {
$namespace_prefix = substr($namespace_prefix, 0, -1);
}
if (!empty($namespace_prefix)) {
if (!PhpGenerator\Helpers::isNamespaceIdentifier($namespace_prefix)) {
throw new \Exception('Invalid namespace prefix provided');
}
$namespace_prefix .= '\\';
}
$types_namespace = $namespace_prefix . 'Types';
$this->namespace_prefix = $namespace_prefix;
$this->methods = $this->createDefaultMethods();
$this->types = $this->createDefaultTypes($types_namespace);
}
private function createDefaultMethods(): array
{
$method_inteface = (new PhpGenerator\ClassType('MethodInterface'))
->setInterface();
$method_inteface->addMethod('getParams')
->setPublic()
->setReturnType(Type::ARRAY);
$method_inteface->addMethod('getMethodName')
->setPublic()
->setStatic()
->setReturnType(Type::STRING);
$method_inteface->addMethod('isMultipart')
->setPublic()
->setReturnType(Type::BOOL);
$method_inteface->addMethod('getResultParams')
->setPublic()
->setStatic()
->setReturnType(Type::ARRAY);
$method_abstract = (new PhpGenerator\ClassType('DefaultMethod'))
->setClass()
->setAbstract()
->addImplement('MethodInterface');
$method_abstract->addConstant('METHOD_NAME', '')
->setPrivate();
$method_abstract->addConstant('RESULT_TYPE', '')
->setPrivate();
$method_abstract->addConstant('MULTIPLE_RESULTS', false)
->setPrivate();
$method_abstract->addProperty('multipart', false)
->setPrivate();
$method_abstract->addMethod('getMethodName')
->setPublic()
->setStatic()
->setReturnType(Type::STRING)
->setBody('return static::METHOD_NAME;');
$method_abstract->addMethod('isMultipart')
->setPublic()
->setReturnType(Type::BOOL)
->setBody('return $this->multipart;');
$method_abstract->addMethod('getResultParams')
->setPublic()
->setStatic()
->setReturnType(Type::ARRAY)
->addBody('return [')
->addBody(' \'type\' => static::RESULT_TYPE,')
->addBody(' \'multiple\' => static::MULTIPLE_RESULTS')
->addBody('];');
return [
'MethodInterface' => $this->addNamespace($method_inteface, 'Methods'),
'DefaultMethod' => $this->addNamespace($method_abstract, 'Methods')
];
}
private function addNamespace(string $code, string $sub_namespace): string
{
return '<?php' . str_repeat(PHP_EOL,
2) . 'namespace ' . $this->namespace_prefix . $sub_namespace . ';' . str_repeat(PHP_EOL, 2) . $code;
}
private function createDefaultTypes(string $namespace): array
{
$response = (new PhpGenerator\ClassType('Response'))
->setType('class');
$response->addProperty('ok')
->setPublic();
$response->addProperty('result')
->setPublic();
$response->addProperty('error_code')
->setPublic();
$response->addProperty('description')
->setPublic();
$response->addMethod('parseResponse')
->setReturnType(Type::SELF)
->setReturnNullable(true)
->setPublic()
->setStatic()
->setBody('if (null == $response) {
return null;
}
$parsed_response = (new self())
->setOk($response->ok ?? null)
->setErrorCode($response->error_code ?? null)
->setDescription($response->description ?? null);
if (empty($response->result)) {
$parsed_response->setResult(null);
} elseif (!empty($response->result->migrate_to_chat_id) or !empty($response->result->retry_after)) {
$parsed_response->setResult(ResponseParameters::parseResponseParameters($response->result ?? null));
} elseif (!empty($response->result_type)) {
$result_class = sprintf(\'' . $namespace . '\\%s\', $response->result_type->class);
$parsed_response->setResult(call_user_func([$result_class, $response->result_type->method],
$response->result ?? null));
} else {
$parsed_response->setResult($response->result ?? null);
}
return $parsed_response;')
->addParameter('response')
->setType('\stdClass')
->setNullable(true);
$this->createSetters($response, [
['name' => 'ok', 'type' => 'bool', 'nullable' => true],
['name' => 'result', 'type' => null, 'nullable' => false],
['name' => 'error_code', 'type' => 'int', 'nullable' => true],
['name' => 'description', 'type' => 'string', 'nullable' => true]
]);
return ['Response' => $this->addNamespace($response, 'Types')];
}
private function createSetters(PhpGenerator\ClassType $class, array $properties): void
{
foreach ($properties as $property) {
$class->addMethod('set' . $this->getCamelCaseName($property['name']))
->setPublic()
->setReturnType(Type::SELF)
->setBody('$this->' . $property['name'] . ' = $' . $property['name'] . ';' . PHP_EOL . 'return $this;')
->addParameter($property['name'])
->setType($property['type'])
->setNullable($property['nullable']);
}
return;
}
private function getCamelCaseName(string $snake_case): string
{
return str_replace(' ', '', ucwords(str_replace('_', ' ', $snake_case)));
}
public function generateCode(array $scheme): array
{
foreach ($scheme['types'] as $type) {
$type_class = (new PhpGenerator\ClassType($type['name']))
->setType('class');
$fields = [];
foreach ($type['fields'] as $field) {
$type_class->addProperty($field['name'])
->setPublic()
->setType($field['type']);
$fields[] = [
'name' => $field['name'],
'type' => $field['type'],
'nullable' => true
];
}
$this->createSetters($type_class, $fields);
$this->types[$type['name']] = $this->addNamespace($type_class, 'Types');
}
foreach ($scheme['methods'] as $method) {
$method_class = (new PhpGenerator\ClassType(ucfirst($method['name'])))
->setType('class')
->addExtend('DefaultMethod');
$method_class->addConstant('METHOD_NAME', $method['name'])
->setPrivate();
$return_type = $method['return_types'][0];
$multiple_results = false;
if (substr($return_type, -2) == '[]') {
$return_type = substr($return_type, 0, -2);
$multiple_results = true;
}
$method_class->addConstant('RESULT_TYPE', $return_type)
->setPrivate();
$method_class->addConstant('MULTIPLE_RESULTS', $multiple_results)
->setPrivate();
$fields = [];
$get_params = $method_class->addMethod('getParams')
->setPublic()
->setReturnType(Type::ARRAY)
->addBody('return [');
$last_index = array_key_last($method['fields']);
foreach ($method['fields'] as $index => $field) {
$field_type = count($field['types']) == 0 ? $field['types'][0] : null; //maybe there will be a better implementation
$method_class->addProperty($field['name'])
->setPublic()
->setType($field_type);
$comma = $index != $last_index ? ',' : '';
$get_params->addBody(' \'' . $field['name'] . '\' => $this->' . $field['name'] . $comma);
$fields[] = [
'name' => $field['name'],
'type' => $field_type,
'nullable' => true
];
}
$get_params->addBody('];');
$this->createConstructor($method_class, $fields);
$this->methods[ucfirst($method['name'])] = $this->addNamespace($method_class, 'Methods');
}
return [
'methods' => $this->methods,
'types' => $this->types
];
}
private function createConstructor(PhpGenerator\ClassType $class, array $properties): void
{
$method = $class->addMethod('__construct');
foreach ($properties as $property) {
$method->addBody('$this->' . $property['name'] . ' = $' . $property['name'] . ';')
->addParameter($property['name'])
->setType($property['type'])
->setNullable($property['nullable']);
}
return;
}
}