Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.34% covered (success)
98.34%
237 / 241
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileFactory
98.34% covered (success)
98.34%
237 / 241
87.50% covered (warning)
87.50%
7 / 8
33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createEntityDeclaration
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 createBoundedContextDeclaration
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
1
 createBoundedContextHashmapDeclaration
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 createDefinitionName
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 buildTypeDeclaration
95.79% covered (success)
95.79%
91 / 95
0.00% covered (danger)
0.00%
0 / 1
20
 buildTypeDeclarations
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 create
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2namespace Apie\TypescriptClientBuilder\CodeGenerators;
3
4use Apie\Core\BoundedContext\BoundedContext;
5use Apie\Core\BoundedContext\BoundedContextHashmap;
6use Apie\Core\Context\ApieContext;
7use Apie\Core\Dto\ValueOption;
8use Apie\Core\Enums\ScalarType;
9use Apie\Core\Metadata\MetadataFactory;
10use Apie\Core\Metadata\MetadataInterface;
11use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException;
12use Apie\Core\ValueObjects\Utils;
13use Apie\TypeConverter\ReflectionTypeFactory;
14use Apie\TypescriptClientBuilder\Dto\TypeDeclarationResult;
15use Apie\TypescriptCodeBuilder\Dto\Expressions\FunctionCallExpression;
16use Apie\TypescriptCodeBuilder\Dto\Expressions\IdentifierExpression;
17use Apie\TypescriptCodeBuilder\Dto\Expressions\JsonLiteralExpression;
18use Apie\TypescriptCodeBuilder\Dto\Expressions\StringLiteralExpression;
19use Apie\TypescriptCodeBuilder\Dto\File;
20use Apie\TypescriptCodeBuilder\Dto\FunctionArgument;
21use Apie\TypescriptCodeBuilder\Dto\ImportStatement;
22use Apie\TypescriptCodeBuilder\Dto\RawJavascript;
23use Apie\TypescriptCodeBuilder\Dto\Typehints\ArrayTypeDefinition;
24use Apie\TypescriptCodeBuilder\Dto\Typehints\IdentifierTypeDefinition;
25use Apie\TypescriptCodeBuilder\Dto\Typehints\InterfaceDefinition;
26use Apie\TypescriptCodeBuilder\Dto\Typehints\LiteralTypeDefinition;
27use Apie\TypescriptCodeBuilder\Dto\Typehints\ObjectTypeDefinition;
28use Apie\TypescriptCodeBuilder\Dto\Typehints\RawTypeDefinition;
29use Apie\TypescriptCodeBuilder\Dto\Typehints\UnionTypeDefinition;
30use Apie\TypescriptCodeBuilder\Dto\TypescriptDeclaration;
31use Apie\TypescriptCodeBuilder\Dto\VariableAssignment;
32use Apie\TypescriptCodeBuilder\Enums\TypescriptType;
33use Apie\TypescriptCodeBuilder\Enums\VariableDeclarationKind;
34use Apie\TypescriptCodeBuilder\Lists\ArgumentList;
35use Apie\TypescriptCodeBuilder\Lists\CodeList;
36use Apie\TypescriptCodeBuilder\Lists\ExpressionList;
37use Apie\TypescriptCodeBuilder\Lists\JavascriptIdentifierList;
38use Apie\TypescriptCodeBuilder\Lists\TypescriptDeclarationList;
39use Apie\TypescriptCodeBuilder\ValueObjects\JavascriptIdentifier;
40use Apie\TypescriptCodeBuilder\ValueObjects\JavascriptIdentifierKey;
41use ReflectionClass;
42use ReflectionType;
43
44class FileFactory
45{
46    public function __construct(
47        private readonly EntityListFactory $entityListFactory,
48        private readonly string $apieClassPrefix = 'ApieDefinition',
49    ) {
50    }
51
52    /**
53     * Creates a entity declaration
54     */
55    private function createEntityDeclaration(BoundedContext $boundedContext, ReflectionClass $entityClass): TypescriptDeclaration
56    {
57        $id = new JavascriptIdentifier(
58            $this->apieClassPrefix . 'BoundedContext' . ucfirst($boundedContext->getId()->toNative()) . $entityClass->getShortName()
59        );
60        return new TypescriptDeclaration(
61            $id,
62            new UnionTypeDefinition(
63                new TypescriptDeclarationList([
64                    new IdentifierTypeDefinition($this->createDefinitionName($entityClass, '')),
65                    new IdentifierTypeDefinition($this->createDefinitionName($entityClass, 'Modify')),
66                    new IdentifierTypeDefinition($this->createDefinitionName($entityClass, 'Result'))
67                ])
68            )
69        );
70    }
71
72    /**
73     * Creates an interface for a bounded context.
74     */
75    private function createBoundedContextDeclaration(BoundedContext $boundedContext): InterfaceDefinition
76    {
77        $id = new JavascriptIdentifier($this->apieClassPrefix . 'BoundedContext' . ucfirst($boundedContext->getId()->toNative()));
78        $performanceMethod = new RawTypeDefinition(
79            '(entity: Entity) => Promise<Entity>',
80            needsDefinition: ['Entity']
81        );
82        return new InterfaceDefinition(
83            $id,
84            new ArgumentList([
85                new FunctionArgument(
86                    new JavascriptIdentifierKey('entities'),
87                    new ObjectTypeDefinition(
88                        new ArgumentList(
89                            array_map(
90                                function (ReflectionClass $entityClass) use ($boundedContext) {
91                                    $id = new JavascriptIdentifier(
92                                        $this->apieClassPrefix
93                                        . 'BoundedContext'
94                                        . ucfirst($boundedContext->getId()->toNative())
95                                        . $entityClass->getShortName()
96                                    );
97                                    return new FunctionArgument(
98                                        new JavascriptIdentifierKey(Utils::getDisplayNameForValueObject($entityClass)),
99                                        new IdentifierTypeDefinition($id)
100                                    );
101                                },
102                                $boundedContext->resources->toArray()
103                            )
104                        )
105                    )
106                ),
107                new FunctionArgument(
108                    new JavascriptIdentifierKey('persist'),
109                    $performanceMethod
110                ),
111                new FunctionArgument(
112                    new JavascriptIdentifierKey('delete'),
113                    $performanceMethod
114                )
115            ]),
116            new JavascriptIdentifierList([
117                'BoundedContext'
118            ])
119        );
120    }
121
122    /**
123     * Creates an intterface for a bounded context hashmap.
124     */
125    private function createBoundedContextHashmapDeclaration(BoundedContextHashmap $hashmap): InterfaceDefinition
126    {
127        $list = [];
128        foreach ($hashmap as $key => $boundedContext) {
129            $list[] = new FunctionArgument(
130                JavascriptIdentifierKey::createFromText($key),
131                new IdentifierTypeDefinition(
132                    new JavascriptIdentifier($this->apieClassPrefix . 'BoundedContext' . ucfirst($boundedContext->getId()->toNative()))
133                )
134            );
135        }
136        return new InterfaceDefinition(
137            new JavascriptIdentifier($this->apieClassPrefix . 'BoundedContextHashmap'),
138            new ArgumentList($list)
139        );
140    }
141
142    private function createDefinitionName(
143        ReflectionClass|ReflectionType $class,
144        string $suffix
145    ): JavascriptIdentifier {
146        if ($class instanceof ReflectionClass) {
147            return JavascriptIdentifier::createFromText(
148                Utils::getDisplayNameForValueObject($class) . $suffix
149            );
150        }
151        return JavascriptIdentifier::createFromText(
152            ((string) $class) . $suffix
153        );
154    }
155
156    private function buildTypeDeclaration(
157        ReflectionClass|ReflectionType $class,
158        MetadataInterface $metadata,
159        string $suffix = ''
160    ): TypeDeclarationResult {
161        $definitionName = $this->createDefinitionName($class, $suffix);
162        $valueOptions = $metadata->getValueOptions(new ApieContext());
163        if (!empty($valueOptions?->toArray())) {
164            return new TypeDeclarationResult(
165                $definitionName,
166                new UnionTypeDefinition(
167                    new TypescriptDeclarationList(
168                        array_map(
169                            function (ValueOption $option) {
170                                $value = $option->value;
171                                return new LiteralTypeDefinition(is_object($value) ? Utils::toString($value) : $value);
172                            },
173                            $valueOptions->toArray()
174                        )
175                    )
176                ),
177                []
178            );
179        }
180        $scalar = $metadata->toScalarType(true);
181        $typedefinition = TypescriptType::Unknown;
182        $todoList = [];
183        switch ($scalar) {
184            case ScalarType::ARRAY:
185                $arrayItemMeta = $metadata->getArrayItemType();
186                $arrayType = TypescriptType::Unknown;
187                if ($arrayItemMeta) {
188                    array_push(
189                        $todoList,
190                        [
191                            $arrayItemMeta->toClass() ?? $arrayItemMeta->toScalarType()->toReflectionType(),
192                            $arrayItemMeta,
193                            $suffix === 'Result' ? 'Result' : ''
194                        ],
195                    );
196                    $arrayType = new IdentifierTypeDefinition(
197                        $this->createDefinitionName(
198                            $arrayItemMeta->toClass() ?? $arrayItemMeta->toScalarType()->toReflectionType(),
199                            $suffix === 'Result' ? 'Result' : ''
200                        )
201                    );
202                }
203                $typedefinition = new ArrayTypeDefinition($arrayType);
204                break;
205            case ScalarType::BOOLEAN:
206                $typedefinition = TypescriptType::Boolean;
207                break;
208            case ScalarType::FLOAT:
209            case ScalarType::INTEGER:
210                $typedefinition = TypescriptType::Number;
211                break;
212            case ScalarType::NULLVALUE:
213                $typedefinition = TypescriptType::NullValue;
214                break;
215            case ScalarType::MIXED:
216            case ScalarType::STDCLASS:
217                $args = [];
218                $context = new ApieContext([]);
219                foreach ($metadata->getHashmap() as $name => $field) {
220                    if (!$field->isField()) {
221                        continue;
222                    }
223                    $type = $field->getTypehint() ?? ReflectionTypeFactory::createReflectionType('mixed');
224                    $meta = $suffix === 'Result'
225                            ? MetadataFactory::getResultMetadata($type, $context)
226                            : MetadataFactory::getCreationMetadata($type, $context);
227                    $addSuffix = $suffix === 'Result' ? 'Result' : '';
228                    $typeDeclaration = new IdentifierTypeDefinition(
229                        $this->createDefinitionName($type, $addSuffix)
230                    );
231                    if ($type->allowsNull()) {
232                        $typeDeclaration = new UnionTypeDefinition(
233                            new TypescriptDeclarationList([
234                                $typeDeclaration,
235                                new LiteralTypeDefinition(null)
236                            ])
237                        );
238                    }
239                    $todoList[] = [
240                        $type,
241                        $meta,
242                        $addSuffix
243                    ];
244                    try {
245                        new JavascriptIdentifier($name);
246                    } catch (InvalidStringForValueObjectException) {
247                        continue;
248                    }
249                    $args[] = new FunctionArgument(
250                        new JavascriptIdentifierKey($name),
251                        $typeDeclaration,
252                        optional: !$field->isRequired(),
253                    );
254                }
255                $typedefinition = new ObjectTypeDefinition(new ArgumentList($args));
256                break;
257            case ScalarType::STRING:
258                $typedefinition = TypescriptType::String;
259        }
260        return new TypeDeclarationResult(
261            $definitionName,
262            $typedefinition,
263            $todoList
264        );
265    }
266
267    private function buildTypeDeclarations(array $todoList, array& $completed): array
268    {
269        $result = [];
270        while (!empty($todoList)) {
271            $todoItem = array_shift($todoList);
272            $declaration = $this->buildTypeDeclaration(...$todoItem);
273            $key = $declaration->definitionName->toNative();
274            if (!isset($completed[$key])) {
275                array_push($todoList, ...$declaration->todoList);
276                $result[] = new TypescriptDeclaration(
277                    $declaration->definitionName,
278                    $declaration->declaration
279                );
280                $completed[$key] = $declaration;
281            }
282        }
283        return $result;
284    }
285
286    public function create(BoundedContextHashmap $boundedContextHashmap, string $apiEndpoint): File
287    {
288        $boundedContextDeclarations = [];
289        $types = [];
290        $context = new ApieContext([]);
291        foreach ($boundedContextHashmap as $boundedContext) {
292            foreach ($boundedContext->resources as $resource) {
293                $boundedContextDeclarations[] = $this->createEntityDeclaration($boundedContext, $resource);
294                $types[] = [$resource, MetadataFactory::getCreationMetadata($resource, $context)];
295                $types[] = [$resource, MetadataFactory::getResultMetadata($resource, $context), 'Result'];
296                $types[] = [$resource, MetadataFactory::getModificationMetadata($resource, $context), 'Modify'];
297            }
298            $boundedContextDeclarations[] = $this->createBoundedContextDeclaration($boundedContext);
299        }
300        $completed = [];
301        $typeDeclarations = $this->buildTypeDeclarations($types, $completed);
302        $hashmapDeclaration = $this->createBoundedContextHashmapDeclaration($boundedContextHashmap);
303
304        $codeList = (new CodeList([
305                new ImportStatement(
306                    './contents/es6/index',
307                    new JavascriptIdentifierList(['createForApi'])
308                ),
309                new ImportStatement(
310                    './contents/es6/build-script',
311                    new JavascriptIdentifierList(['BoundedContext', 'Entity']),
312                    true
313                ),
314                ...$boundedContextDeclarations,
315                ...$typeDeclarations,
316                $hashmapDeclaration,
317                new VariableAssignment(
318                    VariableDeclarationKind::Const,
319                    new JavascriptIdentifier('apiUrl'),
320                    new StringLiteralExpression($apiEndpoint),
321                    TypescriptType::String
322                ),
323                new VariableAssignment(
324                    VariableDeclarationKind::Const,
325                    new JavascriptIdentifier('resourceDefinition'),
326                    new JsonLiteralExpression(
327                        $this->entityListFactory->createTodolistPerBoundedContext($boundedContextHashmap),
328                        prettified: true
329                    )
330                ),
331                new VariableAssignment(
332                    VariableDeclarationKind::Const,
333                    new JavascriptIdentifier('ApieLayer'),
334                    new FunctionCallExpression(
335                        new IdentifierExpression(new JavascriptIdentifier('createForApi')),
336                        new ExpressionList([
337                            new IdentifierExpression(new JavascriptIdentifier('apiUrl')),
338                            new IdentifierExpression(new JavascriptIdentifier('resourceDefinition')),
339                        ])
340                    ),
341                    new IdentifierTypeDefinition($hashmapDeclaration->name)
342                ),
343                new RawJavascript('export { ApieLayer }', needsDefinition: ['ApieLayer'])
344            ]))->sort();
345
346        return new File(
347            $codeList
348        );
349    }
350}