Skip to content

Commit d086aa4

Browse files
Merge pull request #102 from benbjurstrom/generate-typescript-definitions
Generate typescript definitions
2 parents 3e62270 + 5e6ccb8 commit d086aa4

File tree

5 files changed

+353
-1
lines changed

5 files changed

+353
-1
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"laravel/pint": "^1.21",
4242
"orchestra/testbench": "^9.0|^10.0",
4343
"pestphp/pest": "^2.0|^3.0",
44-
"pestphp/pest-plugin-faker": "^2.0|^3.0"
44+
"pestphp/pest-plugin-faker": "^2.0|^3.0",
45+
"spatie/typescript-transformer": "^2.4"
4546
},
4647
"scripts": {
4748
"lint": "pint",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\ValidatedDTO\Support;
6+
7+
use ReflectionClass;
8+
use Spatie\TypeScriptTransformer\Collectors\DefaultCollector;
9+
use Spatie\TypeScriptTransformer\Structures\TransformedType;
10+
use Spatie\TypeScriptTransformer\TypeReflectors\ClassTypeReflector;
11+
use WendellAdriel\ValidatedDTO\SimpleDTO;
12+
13+
class TypeScriptCollector extends DefaultCollector
14+
{
15+
public function getTransformedType(ReflectionClass $class): ?TransformedType
16+
{
17+
if (! $this->shouldCollect($class)) {
18+
return null;
19+
}
20+
21+
$reflector = ClassTypeReflector::create($class);
22+
23+
// Always use our ValidatedDtoTransformer
24+
$transformer = $this->config->buildTransformer(TypeScriptTransformer::class);
25+
26+
return $transformer->transform(
27+
$reflector->getReflectionClass(),
28+
$reflector->getName()
29+
);
30+
}
31+
32+
protected function shouldCollect(ReflectionClass $class): bool
33+
{
34+
// Only collect classes that extend ValidatedDTO
35+
if (! $class->isSubclassOf(SimpleDTO::class)) {
36+
return false;
37+
}
38+
39+
return true;
40+
}
41+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\ValidatedDTO\Support;
6+
7+
use ReflectionClass;
8+
use ReflectionProperty;
9+
use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection;
10+
use Spatie\TypeScriptTransformer\Structures\TransformedType;
11+
use Spatie\TypeScriptTransformer\Transformers\Transformer;
12+
use Spatie\TypeScriptTransformer\Transformers\TransformsTypes;
13+
use Spatie\TypeScriptTransformer\TypeProcessors\ReplaceDefaultsTypeProcessor;
14+
use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig;
15+
use WendellAdriel\ValidatedDTO\SimpleDTO;
16+
17+
class TypeScriptTransformer implements Transformer
18+
{
19+
use TransformsTypes;
20+
21+
protected TypeScriptTransformerConfig $config;
22+
23+
/**
24+
* Properties to exclude from the TypeScript output
25+
*/
26+
protected array $excludedProperties = [
27+
'lazyValidation',
28+
];
29+
30+
public function __construct(TypeScriptTransformerConfig $config)
31+
{
32+
$this->config = $config;
33+
}
34+
35+
public function transform(ReflectionClass $class, string $name): ?TransformedType
36+
{
37+
if (! $this->canTransform($class)) {
38+
return null;
39+
}
40+
41+
$missingSymbols = new MissingSymbolsCollection();
42+
$properties = $this->transformProperties($class, $missingSymbols);
43+
44+
return TransformedType::create(
45+
$class,
46+
$name,
47+
'{' . PHP_EOL . $properties . '}',
48+
$missingSymbols
49+
);
50+
}
51+
52+
protected function canTransform(ReflectionClass $class): bool
53+
{
54+
return $class->isSubclassOf(SimpleDTO::class);
55+
}
56+
57+
protected function transformProperties(
58+
ReflectionClass $class,
59+
MissingSymbolsCollection $missingSymbols
60+
): string {
61+
$properties = array_filter(
62+
$class->getProperties(ReflectionProperty::IS_PUBLIC),
63+
function (ReflectionProperty $property) {
64+
// Exclude static properties
65+
if ($property->isStatic()) {
66+
return false;
67+
}
68+
69+
// Exclude specific properties by name
70+
if (in_array($property->getName(), $this->excludedProperties)) {
71+
return false;
72+
}
73+
74+
return true;
75+
}
76+
);
77+
78+
return array_reduce(
79+
$properties,
80+
function (string $carry, ReflectionProperty $property) use ($missingSymbols) {
81+
$transformed = $this->reflectionToTypeScript(
82+
$property,
83+
$missingSymbols,
84+
false,
85+
new ReplaceDefaultsTypeProcessor($this->config->getDefaultTypeReplacements())
86+
);
87+
88+
if ($transformed === null) {
89+
return $carry;
90+
}
91+
92+
$propertyName = $property->getName();
93+
94+
return "{$carry}{$propertyName}: {$transformed};" . PHP_EOL;
95+
},
96+
''
97+
);
98+
}
99+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig;
6+
use WendellAdriel\ValidatedDTO\Support\TypeScriptCollector;
7+
8+
it('returns null when class does not extend SimpleDTO', function () {
9+
$class = new class() {};
10+
11+
$reflection = new ReflectionClass($class);
12+
$collector = new TypeScriptCollector(TypeScriptTransformerConfig::create());
13+
14+
$type = $collector->getTransformedType($reflection);
15+
16+
expect($type)->toBeNull();
17+
});
18+
19+
it('uses the TypeScriptTransformer for an eligible class', function () {
20+
eval('
21+
namespace App\Data {
22+
use WendellAdriel\ValidatedDTO\SimpleDTO;
23+
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
24+
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
25+
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
26+
class TransformerTestDTO1 extends SimpleDTO {
27+
use EmptyRules, EmptyCasts, EmptyDefaults;
28+
29+
public string $name;
30+
}
31+
}
32+
');
33+
34+
$reflection = new ReflectionClass(\App\Data\TransformerTestDTO1::class);
35+
36+
// Provide a config with no other conflicting transformers
37+
$config = TypeScriptTransformerConfig::create()
38+
->transformers([\WendellAdriel\ValidatedDTO\Support\TypeScriptTransformer::class]);
39+
40+
$collector = new TypeScriptCollector($config);
41+
42+
$type = $collector->getTransformedType($reflection);
43+
44+
expect($type)->not->toBeNull()
45+
->and($type->getTypeScriptName())->toBe('App.Data.TransformerTestDTO1');
46+
});
47+
48+
it('uses the TypeScriptTransformer for ResourceDTO', function () {
49+
eval('
50+
namespace App\Data {
51+
use WendellAdriel\ValidatedDTO\ResourceDTO;
52+
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
53+
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
54+
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
55+
class TransformerTestDTO2 extends ResourceDTO {
56+
use EmptyRules, EmptyCasts, EmptyDefaults;
57+
58+
public string $name;
59+
}
60+
}
61+
');
62+
63+
$reflection = new ReflectionClass(\App\Data\TransformerTestDTO2::class);
64+
65+
// Provide a config with no other conflicting transformers
66+
$config = TypeScriptTransformerConfig::create()
67+
->transformers([\WendellAdriel\ValidatedDTO\Support\TypeScriptTransformer::class]);
68+
69+
$collector = new TypeScriptCollector($config);
70+
71+
$type = $collector->getTransformedType($reflection);
72+
73+
expect($type)->not->toBeNull()
74+
->and($type->getTypeScriptName())->toBe('App.Data.TransformerTestDTO2');
75+
});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Spatie\TypeScriptTransformer\Structures\TransformedType;
6+
use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig;
7+
use WendellAdriel\ValidatedDTO\Support\TypeScriptTransformer;
8+
9+
it('returns null when class does not extend SimpleDTO', function () {
10+
$class = new class()
11+
{
12+
public string $name;
13+
};
14+
15+
$reflection = new ReflectionClass($class);
16+
17+
$transformer = new TypeScriptTransformer(TypeScriptTransformerConfig::create());
18+
$type = $transformer->transform($reflection, 'IrrelevantName');
19+
20+
expect($type)->toBeNull();
21+
});
22+
23+
it('transforms a SimpleDTO with public properties into a TransformedType', function () {
24+
eval('
25+
namespace App\Data {
26+
use WendellAdriel\ValidatedDTO\SimpleDTO;
27+
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
28+
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
29+
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
30+
class TestTransformerDTO extends SimpleDTO {
31+
use EmptyRules, EmptyCasts, EmptyDefaults;
32+
33+
public string $name;
34+
public int $age;
35+
public static string $shouldNotAppear = "excluded";
36+
protected string $invisible = "excluded";
37+
}
38+
}
39+
');
40+
41+
$reflection = new ReflectionClass(\App\Data\TestTransformerDTO::class);
42+
43+
$transformer = new TypeScriptTransformer(TypeScriptTransformerConfig::create());
44+
$type = $transformer->transform($reflection, 'TransformedDTO');
45+
46+
// Should only include public, non-static properties
47+
expect($type)->toBeInstanceOf(TransformedType::class)
48+
->and($type->name)->toBe('TransformedDTO')
49+
->and($type->transformed)->toContain('name: string;')
50+
->and($type->transformed)->toContain('age: number;')
51+
->and($type->transformed)->not->toContain('shouldNotAppear')
52+
->and($type->transformed)->not->toContain('invisible');
53+
});
54+
55+
it('excludes properties listed in excludedProperties', function () {
56+
eval('
57+
namespace App\Data {
58+
use WendellAdriel\ValidatedDTO\ValidatedDTO;
59+
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
60+
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
61+
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
62+
class ExcludedPropertyDTO extends ValidatedDTO {
63+
use EmptyRules, EmptyCasts, EmptyDefaults;
64+
65+
public bool $lazyValidation = true; // excluded by default
66+
public string $title;
67+
}
68+
}
69+
');
70+
71+
$reflection = new ReflectionClass(\App\Data\ExcludedPropertyDTO::class);
72+
73+
$transformer = new TypeScriptTransformer(TypeScriptTransformerConfig::create());
74+
$type = $transformer->transform($reflection, 'ExcludedProps');
75+
76+
expect($type->transformed)->not->toContain('lazyValidation:')
77+
->and($type->transformed)->toContain('title: string;')
78+
->and($type->getTypeScriptName())->toBe('App.Data.ExcludedProps');
79+
});
80+
81+
it('transforms a ValidatedDTO with nested DTO and enum property', function () {
82+
eval('
83+
namespace App\Enums {
84+
enum FakeStatusEnum: string {
85+
case FIRST = "first";
86+
case SECOND = "second";
87+
}
88+
}
89+
');
90+
91+
eval('
92+
namespace App\Data {
93+
use WendellAdriel\ValidatedDTO\ValidatedDTO;
94+
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
95+
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
96+
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
97+
class ChildDTO extends ValidatedDTO {
98+
use EmptyRules, EmptyCasts, EmptyDefaults;
99+
100+
public string $childField;
101+
}
102+
}
103+
');
104+
105+
eval('
106+
namespace App\Data {
107+
use WendellAdriel\ValidatedDTO\ValidatedDTO;
108+
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
109+
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
110+
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
111+
use App\Enums\FakeStatusEnum;
112+
113+
class ParentDTO extends ValidatedDTO {
114+
use EmptyRules, EmptyCasts, EmptyDefaults;
115+
116+
public FakeStatusEnum $status;
117+
public ChildDTO $child;
118+
}
119+
}
120+
');
121+
122+
$reflection = new ReflectionClass(\App\Data\ParentDTO::class);
123+
$transformer = new TypeScriptTransformer(TypeScriptTransformerConfig::create());
124+
$type = $transformer->transform($reflection, 'ComplexDTO');
125+
126+
expect($type)->toBeInstanceOf(TransformedType::class)
127+
->and($type->name)->toBe('ComplexDTO')
128+
->and($type->transformed)->toContain('status: {%App\Enums\FakeStatusEnum%};')
129+
->and($type->transformed)->toContain('child: {%App\Data\ChildDTO%};')
130+
->and($type->missingSymbols->all())
131+
// Missing Symbols contain references to other types. Once all types are
132+
// transformed, the package will replace these references with their
133+
// TypeScript types. When no type is found the type will default to any.
134+
->toContain(\App\Enums\FakeStatusEnum::class)
135+
->and($type->missingSymbols->all())->toContain(\App\Data\ChildDTO::class);
136+
});

0 commit comments

Comments
 (0)