Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
98.34% |
237 / 241 |
|
87.50% |
7 / 8 |
CRAP | |
0.00% |
0 / 1 |
| FileFactory | |
98.34% |
237 / 241 |
|
87.50% |
7 / 8 |
33 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| createEntityDeclaration | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
| createBoundedContextDeclaration | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
1 | |||
| createBoundedContextHashmapDeclaration | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
| createDefinitionName | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| buildTypeDeclaration | |
95.79% |
91 / 95 |
|
0.00% |
0 / 1 |
20 | |||
| buildTypeDeclarations | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
| create | |
100.00% |
57 / 57 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | namespace Apie\TypescriptClientBuilder\CodeGenerators; |
| 3 | |
| 4 | use Apie\Core\BoundedContext\BoundedContext; |
| 5 | use Apie\Core\BoundedContext\BoundedContextHashmap; |
| 6 | use Apie\Core\Context\ApieContext; |
| 7 | use Apie\Core\Dto\ValueOption; |
| 8 | use Apie\Core\Enums\ScalarType; |
| 9 | use Apie\Core\Metadata\MetadataFactory; |
| 10 | use Apie\Core\Metadata\MetadataInterface; |
| 11 | use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException; |
| 12 | use Apie\Core\ValueObjects\Utils; |
| 13 | use Apie\TypeConverter\ReflectionTypeFactory; |
| 14 | use Apie\TypescriptClientBuilder\Dto\TypeDeclarationResult; |
| 15 | use Apie\TypescriptCodeBuilder\Dto\Expressions\FunctionCallExpression; |
| 16 | use Apie\TypescriptCodeBuilder\Dto\Expressions\IdentifierExpression; |
| 17 | use Apie\TypescriptCodeBuilder\Dto\Expressions\JsonLiteralExpression; |
| 18 | use Apie\TypescriptCodeBuilder\Dto\Expressions\StringLiteralExpression; |
| 19 | use Apie\TypescriptCodeBuilder\Dto\File; |
| 20 | use Apie\TypescriptCodeBuilder\Dto\FunctionArgument; |
| 21 | use Apie\TypescriptCodeBuilder\Dto\ImportStatement; |
| 22 | use Apie\TypescriptCodeBuilder\Dto\RawJavascript; |
| 23 | use Apie\TypescriptCodeBuilder\Dto\Typehints\ArrayTypeDefinition; |
| 24 | use Apie\TypescriptCodeBuilder\Dto\Typehints\IdentifierTypeDefinition; |
| 25 | use Apie\TypescriptCodeBuilder\Dto\Typehints\InterfaceDefinition; |
| 26 | use Apie\TypescriptCodeBuilder\Dto\Typehints\LiteralTypeDefinition; |
| 27 | use Apie\TypescriptCodeBuilder\Dto\Typehints\ObjectTypeDefinition; |
| 28 | use Apie\TypescriptCodeBuilder\Dto\Typehints\RawTypeDefinition; |
| 29 | use Apie\TypescriptCodeBuilder\Dto\Typehints\UnionTypeDefinition; |
| 30 | use Apie\TypescriptCodeBuilder\Dto\TypescriptDeclaration; |
| 31 | use Apie\TypescriptCodeBuilder\Dto\VariableAssignment; |
| 32 | use Apie\TypescriptCodeBuilder\Enums\TypescriptType; |
| 33 | use Apie\TypescriptCodeBuilder\Enums\VariableDeclarationKind; |
| 34 | use Apie\TypescriptCodeBuilder\Lists\ArgumentList; |
| 35 | use Apie\TypescriptCodeBuilder\Lists\CodeList; |
| 36 | use Apie\TypescriptCodeBuilder\Lists\ExpressionList; |
| 37 | use Apie\TypescriptCodeBuilder\Lists\JavascriptIdentifierList; |
| 38 | use Apie\TypescriptCodeBuilder\Lists\TypescriptDeclarationList; |
| 39 | use Apie\TypescriptCodeBuilder\ValueObjects\JavascriptIdentifier; |
| 40 | use Apie\TypescriptCodeBuilder\ValueObjects\JavascriptIdentifierKey; |
| 41 | use ReflectionClass; |
| 42 | use ReflectionType; |
| 43 | |
| 44 | class 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 | } |