Passed
Push — master ( e13e88...4bf53d )
by Sys
02:04
created

Generator::toYaml()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 3
dl 0
loc 4
rs 10
1
<?php
2
3
namespace TgScraper;
4
5
use Exception;
6
use JsonException;
7
use PHPHtmlParser\Dom;
8
use PHPHtmlParser\Exceptions\ChildNotFoundException;
9
use PHPHtmlParser\Exceptions\CircularException;
10
use PHPHtmlParser\Exceptions\ContentLengthException;
11
use PHPHtmlParser\Exceptions\LogicalException;
12
use PHPHtmlParser\Exceptions\NotLoadedException;
13
use PHPHtmlParser\Exceptions\ParentNotFoundException;
14
use PHPHtmlParser\Exceptions\StrictException;
15
use Psr\Http\Client\ClientExceptionInterface;
16
use Symfony\Component\Yaml\Yaml;
17
18
class Generator
19
{
20
21
    private const BOOL_RETURNS = [
22
        'answerShippingQuery',
23
        'answerPreCheckoutQuery'
24
    ];
25
26
    public const BOT_API_URL = 'https://core.telegram.org/bots/api';
27
28
    public function __construct(private string $url = self::BOT_API_URL)
29
    {
30
    }
31
32
    /**
33
     * @param string $directory
34
     * @param string $namespace
35
     * @param string|null $scheme
36
     * @return bool
37
     * @throws ClientExceptionInterface
38
     */
39
    public function toStubs(string $directory = '', string $namespace = '', string $scheme = null): bool
40
    {
41
        try {
42
            $directory = self::getTargetDirectory($directory);
43
        } catch (Exception $e) {
44
            echo 'Unable to use target directory:' . $e->getMessage() . PHP_EOL;
45
            return false;
46
        }
47
        mkdir($directory . '/Types', 0755);
48
        try {
49
            if (!empty($scheme)) {
50
                try {
51
                    $data = json_decode($scheme, true, flags: JSON_THROW_ON_ERROR);
52
                } /** @noinspection PhpRedundantCatchClauseInspection */ catch (JsonException) {
53
                    $data = null;
54
                }
55
            }
56
            $data = $data ?? $this->extractScheme();
57
            $creator = new StubCreator($data, $namespace);
58
            $code = $creator->generateCode();
59
            foreach ($code['types'] as $className => $type) {
60
                $filename = sprintf('%s/Types/%s.php', $directory, $className);
61
                file_put_contents($filename, $type);
62
            }
63
            file_put_contents($directory . '/API.php', $code['api']);
64
        } catch (Exception $e) {
65
            echo $e->getMessage() . PHP_EOL;
66
            return false;
67
        }
68
        return true;
69
    }
70
71
    /**
72
     * @param string $path
73
     * @return string
74
     * @throws Exception
75
     */
76
    private static function getTargetDirectory(string $path): string
77
    {
78
        $path = realpath($path);
79
        if (false === $path) {
80
            if (!mkdir($path)) {
81
                $path = __DIR__ . '/../generated';
82
                if (!file_exists($path)) {
83
                    mkdir($path, 0755);
84
                }
85
            }
86
        }
87
        if (realpath($path) === false) {
88
            throw new Exception('Could not create target directory');
89
        }
90
        return $path;
91
    }
92
93
    /**
94
     * @return array
95
     * @throws ChildNotFoundException
96
     * @throws CircularException
97
     * @throws ContentLengthException
98
     * @throws LogicalException
99
     * @throws NotLoadedException
100
     * @throws ParentNotFoundException
101
     * @throws StrictException
102
     * @throws ClientExceptionInterface
103
     */
104
    private function extractScheme(): array
105
    {
106
        $dom = new Dom;
107
        $dom->loadFromURL($this->url);
108
        $elements = $dom->find('h4');
109
        $data = [];
110
        /* @var Dom\Node\AbstractNode $element */
111
        foreach ($elements as $element) {
112
            if (!str_contains($name = $element->text, ' ')) {
113
                $isMethod = self::isMethod($name);
114
                $path = $isMethod ? 'methods' : 'types';
115
                $temp = $element;
116
                $description = '';
117
                $table = null;
118
                while (true) {
119
                    try {
120
                        $element = $element->nextSibling();
121
                    } catch (ChildNotFoundException) {
122
                        break;
123
                    }
124
                    $tag = $element->tag->name() ?? null;
125
                    if (empty($temp->text()) or empty($tag) or $tag == 'text') {
126
                        continue;
127
                    } elseif (str_starts_with($tag, 'h')) {
128
                        break;
129
                    } elseif ($tag == 'p') {
130
                        $description .= PHP_EOL . $element->innerHtml();
131
                    } elseif ($tag == 'table') {
132
                        $table = $element->find('tbody')->find('tr');
133
                        break;
134
                    }
135
                }
136
                /* @var Dom\Node\AbstractNode $element */
137
                $data[$path][] = self::generateElement(
138
                    $name,
139
                    trim($description),
140
                    $table,
141
                    $isMethod
142
                );
143
            }
144
        }
145
        return $data;
146
    }
147
148
    private static function isMethod(string $name): bool
149
    {
150
        return lcfirst($name) == $name;
151
    }
152
153
    /**
154
     * @param string $name
155
     * @param string $description
156
     * @param Dom\Node\Collection|null $unparsedFields
157
     * @param bool $isMethod
158
     * @return array
159
     * @throws ChildNotFoundException
160
     * @throws CircularException
161
     * @throws ContentLengthException
162
     * @throws LogicalException
163
     * @throws NotLoadedException
164
     * @throws StrictException
165
     */
166
    private static function generateElement(
167
        string $name,
168
        string $description,
169
        ?Dom\Node\Collection $unparsedFields,
170
        bool $isMethod
171
    ): array {
172
        $fields = self::parseFields($unparsedFields, $isMethod);
173
        if (!$isMethod) {
174
            return [
175
                'name' => $name,
176
                'description' => htmlspecialchars_decode(strip_tags($description), ENT_QUOTES),
177
                'fields' => $fields
178
            ];
179
        }
180
        $returnTypes = self::parseReturnTypes($description);
181
        if (empty($returnTypes) and in_array($name, self::BOOL_RETURNS)) {
182
            $returnTypes[] = 'bool';
183
        }
184
        return [
185
            'name' => $name,
186
            'description' => htmlspecialchars_decode(strip_tags($description), ENT_QUOTES),
187
            'fields' => $fields,
188
            'return_types' => $returnTypes
189
        ];
190
    }
191
192
    /**
193
     * @param Dom\Node\Collection|null $fields
194
     * @param bool $isMethod
195
     * @return array
196
     * @throws ChildNotFoundException
197
     * @throws NotLoadedException
198
     */
199
    private static function parseFields(?Dom\Node\Collection $fields, bool $isMethod): array
200
    {
201
        $parsedFields = [];
202
        $fields = $fields ?? [];
203
        foreach ($fields as $field) {
204
            /* @var Dom $field */
205
            $fieldData = $field->find('td');
206
            $name = $fieldData[0]->text;
207
            if (empty($name)) {
208
                continue;
209
            }
210
            $parsedData = [
211
                'name' => $name,
212
                'type' => strip_tags($fieldData[1]->innerHtml)
213
            ];
214
            $parsedData['types'] = self::parseFieldTypes($parsedData['type']);
215
            unset($parsedData['type']);
216
            if ($isMethod) {
217
                $parsedData['required'] = $fieldData[2]->text == 'Yes';
218
                $parsedData['description'] = htmlspecialchars_decode(
219
                    strip_tags($fieldData[3]->innerHtml ?? $fieldData[3]->text ?? ''),
220
                    ENT_QUOTES
221
                );
222
            } else {
223
                $description = htmlspecialchars_decode(strip_tags($fieldData[2]->innerHtml), ENT_QUOTES);
224
                $parsedData['optional'] = str_starts_with($description, 'Optional.');
225
                $parsedData['description'] = $description;
226
            }
227
            $parsedFields[] = $parsedData;
228
        }
229
        return $parsedFields;
230
    }
231
232
    /**
233
     * @param string $rawType
234
     * @return array
235
     */
236
    private static function parseFieldTypes(string $rawType): array
237
    {
238
        $types = [];
239
        foreach (explode(' or ', $rawType) as $rawOrType) {
240
            if (stripos($rawOrType, 'array') === 0) {
241
                $types[] = str_replace(' and', ',', $rawOrType);
242
                continue;
243
            }
244
            foreach (explode(' and ', $rawOrType) as $unparsedType) {
245
                $types[] = $unparsedType;
246
            }
247
        }
248
        $parsedTypes = [];
249
        foreach ($types as $type) {
250
            $type = trim(str_replace(['number', 'of'], '', $type));
251
            $multiplesCount = substr_count(strtolower($type), 'array');
252
            $parsedType = trim(
253
                str_replace(
254
                    ['Array', 'Integer', 'String', 'Boolean', 'Float', 'True'],
255
                    ['', 'int', 'string', 'bool', 'float', 'bool'],
256
                    $type
257
                )
258
            );
259
            for ($i = 0; $i < $multiplesCount; $i++) {
260
                $parsedType = sprintf('Array<%s>', $parsedType);
261
            }
262
            $parsedTypes[] = $parsedType;
263
        }
264
        return $parsedTypes;
265
    }
266
267
    /**
268
     * @param string $description
269
     * @return array
270
     * @throws ChildNotFoundException
271
     * @throws CircularException
272
     * @throws NotLoadedException
273
     * @throws StrictException
274
     * @throws ContentLengthException
275
     * @throws LogicalException
276
     * @noinspection PhpUndefinedFieldInspection
277
     */
278
    private static function parseReturnTypes(string $description): array
279
    {
280
        $returnTypes = [];
281
        $phrases = explode('.', $description);
282
        $phrases = array_filter(
283
            $phrases,
284
            function ($phrase) {
285
                return (false !== stripos($phrase, 'returns') or false !== stripos($phrase, 'is returned'));
286
            }
287
        );
288
        foreach ($phrases as $phrase) {
289
            $dom = new Dom;
290
            $dom->loadStr($phrase);
291
            $a = $dom->find('a');
292
            $em = $dom->find('em');
293
            foreach ($a as $element) {
294
                if ($element->text == 'Messages') {
295
                    $returnTypes[] = 'Array<Message>';
296
                    continue;
297
                }
298
299
                $multiplesCount = substr_count(strtolower($phrase), 'array');
300
                $returnType = $element->text;
301
                for ($i = 0; $i < $multiplesCount; $i++) {
302
                    $returnType = sprintf('Array<%s>', $returnType);
303
                }
304
                $returnTypes[] = $returnType;
305
            }
306
            foreach ($em as $element) {
307
                if (in_array($element->text, ['False', 'force', 'Array'])) {
308
                    continue;
309
                }
310
                $type = str_replace(['True', 'Int', 'String'], ['bool', 'int', 'string'], $element->text);
311
                $returnTypes[] = $type;
312
            }
313
        }
314
        return $returnTypes;
315
    }
316
317
    /**
318
     * @param int $options
319
     * @return string
320
     * @throws ChildNotFoundException
321
     * @throws CircularException
322
     * @throws ClientExceptionInterface
323
     * @throws ContentLengthException
324
     * @throws LogicalException
325
     * @throws NotLoadedException
326
     * @throws ParentNotFoundException
327
     * @throws StrictException
328
     */
329
    public function toJson(int $options = 0): string
330
    {
331
        $scheme = $this->extractScheme();
332
        return json_encode($scheme, $options);
333
    }
334
335
    /**
336
     * @throws ChildNotFoundException
337
     * @throws CircularException
338
     * @throws ParentNotFoundException
339
     * @throws StrictException
340
     * @throws ClientExceptionInterface
341
     * @throws NotLoadedException
342
     * @throws ContentLengthException
343
     * @throws LogicalException
344
     */
345
    public function toYaml(int $inline = 6, int $indent = 4, int $flags = 0): string
346
    {
347
        $scheme = $this->extractScheme();
348
        return Yaml::dump($scheme, $inline, $indent, $flags);
349
    }
350
351
}
352