SchemaParser::printError()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AurimasNiekis\TdLibSchema\Generator\Parser;
6
7
use Psr\Log\LoggerInterface;
8
use Psr\Log\NullLogger;
9
10
/**
11
 * @author  Aurimas Niekis <[email protected]>
12
 */
13
class SchemaParser
14
{
15
    public const MASTER_TL_API_SOURCE_URL = 'https://raw.githubusercontent.com/tdlib/td/master/td/generate/scheme/td_api.tl';
16
17
    private LoggerInterface $logger;
18
19
    private string          $schemaFile;
20
    private string          $rawSchema;
21
    private string          $currentLine;
22
    private int             $currentLineNr;
23
    private array           $documentation;
24
    private array           $classes;
25
26
    public function __construct(LoggerInterface $logger = null, string $schemaFile = null)
27
    {
28
        $this->logger        = $logger ?? new NullLogger();
29
        $this->schemaFile    = $schemaFile ?? static::MASTER_TL_API_SOURCE_URL;
30
        $this->rawSchema     = file_get_contents($this->schemaFile);
31
        $this->documentation = [];
32
        $this->classes       = [];
33
    }
34
35
    /**
36
     * @return ClassDefinition[]
37
     */
38
    public function parse(): array
39
    {
40
        $lines = explode(PHP_EOL, $this->rawSchema);
41
42
        $description          = '';
43
        $currentClassName     = '';
44
        $currentClass         = new ClassDefinition();
45
        $isFunction           = false;
46
        $needClassDescription = false;
47
        $this->currentLineNr  = 0;
48
49
        foreach ($lines as $line) {
50
            $this->logger->debug('Current Line', ['line' => $line]);
51
52
            $this->currentLine = $line;
53
            $this->currentLineNr++;
54
55
            if ('---types---' === $line) {
56
                $isFunction = false;
57
            } elseif ('---functions---' === $line) {
58
                $isFunction           = true;
59
                $currentClassName     = '';
60
                $currentClass         = new ClassDefinition();
61
                $needClassDescription = false;
62
            } elseif (($line[0] ?? '') === '/') {
63
                if (($line[1] ?? '') !== '/') {
64
                    $this->printError('Wrong comment');
65
66
                    continue;
67
                }
68
69
                if (($line[2] ?? '') === '@' || ($line[2] ?? '') === '-') {
70
                    $description .= trim(substr($line, 2 + intval('-' === $line[2]))) . ' ';
71
                } else {
72
                    $this->printError('Unexpected comment');
73
                }
74
            } elseif (strpos($line, '? =') || strpos($line, ' = Vector t;') || 'boolFalse = Bool;' === $line ||
75
                'boolTrue = Bool;' === $line || 'bytes = Bytes;' === $line || 'int32 = Int32;' === $line ||
76
                'int53 = Int53;' === $line || 'int64 = Int64;' === $line) {
77
                $this->printDebug('skip built-in types');
78
79
                continue;
80
            } else {
81
                $description = trim($description);
82
83
                if ('' === $description) {
84
//                    $this->printError('Empty description', ['description' => $description]);
85
                }
86
87
                if (($description[0] ?? '') !== '@') {
88
//                    $this->printError('Wrong description begin', ['description' => $description]);
89
                }
90
91
                $docs = explode('@', $description);
92
                array_shift($docs);
93
94
                $info = [];
95
                foreach ($docs as $doc) {
96
                    [$key, $value] = explode(' ', $doc, 2);
97
                    $value         = trim($value);
98
99
                    if ($needClassDescription) {
100
                        if ('description' === $key) {
101
                            $needClassDescription = false;
102
103
                            $currentClass->classDocs   = $value;
104
                            $currentClass->parentClass = 'Object';
105
                            $currentClass->typeName    = $currentClass->className;
106
107
                            $this->classes[$value] = $currentClass;
108
                            $currentClass          = new ClassDefinition();
109
                            continue;
110
                        } else {
111
                            $this->printError('Expected abstract class description', ['description' => $description]);
112
                        }
113
                    }
114
115
                    if ('class' === $key) {
116
                        $currentClassName        = $this->getClassName($value);
117
                        $currentClass->className = $currentClassName;
118
119
                        $needClassDescription = true;
120
121
                        if ($isFunction) {
122
                            $this->printError('Unexpected class definition');
123
                        }
124
                    } else {
125
                        if (isset($info[$key])) {
126
//                            $this->printError("Duplicate info about `$key`");
127
                        }
128
                        $info[$key] = trim($value);
129
                    }
130
                }
131
132
                if (1 !== substr_count($line, '=')) {
133
//                    $this->printError("Wrong '=' count");
134
                    continue;
135
                }
136
137
                [$fields, $type] = explode('=', $line);
138
                $type            = $this->getClassName($type);
139
                $fields          = explode(' ', trim($fields));
140
                $typeName        = array_shift($fields);
141
                $className       = $this->getClassName($typeName);
142
143
                if ($type !== $currentClassName) {
144
                    $currentClassName     = '';
145
                    $currentClass         = new ClassDefinition();
146
                    $needClassDescription = false;
147
                }
148
149
                if (!$isFunction) {
150
                    $typeLower      = strtolower($type);
151
                    $classNameLower = strtolower($className);
152
153
                    if (empty($currentClassName) === ($typeLower !== $classNameLower)) {
154
                        $this->printError('Wrong constructor name');
155
                    }
156
157
                    if (0 !== strpos($classNameLower, $typeLower)) {
158
                        // $this->printError('Wrong constructor name');
159
                    }
160
                }
161
162
                $knownFields = [];
163
                foreach ($fields as $field) {
164
                    [$fieldName, $fieldType] = explode(':', $field);
165
166
                    if (isset($info['param_' . $fieldName])) {
167
                        $knownFields['param_' . $fieldName] = $fieldType;
168
169
                        continue;
170
                    }
171
172
                    if (isset($info[$fieldName])) {
173
                        $knownFields[$fieldName] = $fieldType;
174
175
                        continue;
176
                    }
177
178
                    $this->printError("Have no info about field `$fieldName`");
179
                }
180
181
                foreach ($info as $name => $value) {
182
                    if (!$value) {
183
                        $this->printError("info[$name] for $className is empty");
184
                    } elseif (($value[0] < 'A' || $value[0] > 'Z') && ($value[0] < '0' || $value[0] > '9')) {
185
                        $this->printError("info[$name] for $className doesn't begins with capital letter");
186
                    }
187
                }
188
189
                foreach (array_diff_key($info, $knownFields) as $fieldName => $fieldInfo) {
190
                    if ('description' !== $fieldName) {
191
                        $this->printError("Have info about unexisted field `$fieldName`");
192
                    }
193
                }
194
195
                if (!isset($info['description'])) {
196
                    $this->printError("Have no description for class `$className`");
197
                }
198
199
                $baseClassName    = $currentClassName ?: $this->getBaseClassName($isFunction);
200
                $classDescription = $info['description'];
201
202
                if ($isFunction) {
203
                    $currentClass->returnType = $this->getTypeName($type);
204
                }
205
206
                $currentClass->className   = $className;
207
                $currentClass->parentClass = $baseClassName;
208
                $currentClass->classDocs   = $classDescription;
209
                $currentClass->typeName    = $typeName;
210
211
                foreach ($knownFields as $name => $fieldType) {
212
                    $mayBeNull     = false !== stripos($info[$name], 'may be null');
213
                    $fieldName     = $this->getFieldName($name, $className);
214
                    $fieldTypeName = $this->getTypeName($fieldType);
215
216
                    $rawName = $name;
217
                    if ('param_' === substr($rawName, 0, 6)) {
218
                        $rawName = substr($rawName, 6);
219
                    }
220
221
                    $field            = $currentClass->getField($name);
222
                    $field->rawName   = $rawName;
223
                    $field->name      = $fieldName;
224
                    $field->type      = $fieldTypeName;
225
                    $field->doc       = $info[$name];
226
                    $field->mayBeNull = $mayBeNull;
227
                }
228
229
                $this->classes[$typeName] = $currentClass;
230
                $currentClass             = new ClassDefinition();
231
                $description              = '';
232
            }
233
        }
234
235
        return $this->classes;
236
    }
237
238
    private function printError(string $msg, array $args = []): void
239
    {
240
        $this->logger->error(
241
            $msg,
242
            array_merge(
243
                ['line' => $this->currentLine, 'line_nr' => $this->currentLineNr],
244
                $args
245
            )
246
        );
247
    }
248
249
    private function printDebug(string $msg, array $args = []): void
250
    {
251
        $this->logger->debug(
252
            $msg,
253
            array_merge(
254
                ['line' => $this->currentLine, 'line_nr' => $this->currentLineNr],
255
                $args
256
            )
257
        );
258
    }
259
260
    protected function getClassName($name): string
261
    {
262
        return implode(array_map('ucfirst', explode('.', trim($name, "\r\n ;"))));
263
    }
264
265
    protected function getBaseClassName($isFunction): string
266
    {
267
        return $isFunction ? 'Function' : 'Object';
268
    }
269
270
    protected function getTypeName($type): string
271
    {
272
        switch ($type) {
273
            case 'Bool':
274
                return 'bool';
275
            case 'int32':
276
            case 'int53':
277
                return 'int';
278
            case 'double':
279
                return 'float';
280
            case 'string':
281
            case 'bytes':
282
            case 'int64':
283
                return 'string';
284
            case 'bool':
285
            case 'int':
286
            case 'long':
287
            case 'Int':
288
            case 'Long':
289
            case 'Int32':
290
            case 'Int53':
291
            case 'Int64':
292
            case 'Double':
293
            case 'String':
294
            case 'Bytes':
295
                $this->printError("Wrong type $type");
296
297
                return '';
298
            default:
299
                if ('vector' === substr($type, 0, 6)) {
300
                    if ('<' !== $type[6] || '>' !== $type[strlen($type) - 1]) {
301
                        $this->printError("Wrong vector subtype in $type");
302
303
                        return '';
304
                    }
305
306
                    return $this->getTypeName(substr($type, 7, -1)) . '[]';
307
                }
308
309
                if (preg_match('/[^A-Za-z0-9.]/', $type)) {
310
                    $this->printError("Wrong type $type");
311
312
                    return '';
313
                }
314
315
                return $this->getClassName($type);
316
        }
317
    }
318
319
    protected function getFieldName($name, $className): string
320
    {
321
        if ('param_' === substr($name, 0, 6)) {
322
            $name = substr($name, 6);
323
        }
324
325
        return preg_replace_callback(
326
            '/_([A-Za-z])/',
327
            fn ($matches) => strtoupper($matches[1]),
328
            trim($name)
329
        );
330
    }
331
}
332