Skip to content

Commit 478b0e9

Browse files
committed
initial commit
0 parents  commit 478b0e9

16 files changed

+865
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
## ide
2+
.idea
3+
4+
## composer
5+
vendor
6+
composer.lock

bin/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## composer
2+
composer.lock
3+
vendor

bin/composer.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"require": {
3+
"php": ">=8.0",
4+
"nette/neon": "^3.3",
5+
"nette/php-generator": "^3.6",
6+
"nette/schema": "^1.2"
7+
}
8+
}

bin/generate.php

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
<?php declare(strict_types = 1);
2+
3+
use Nette\Neon\Neon;
4+
use Nette\PhpGenerator\ClassType;
5+
use Nette\PhpGenerator\Helpers;
6+
use Nette\PhpGenerator\PhpFile;
7+
use Nette\PhpGenerator\PhpNamespace;
8+
use Nette\PhpGenerator\Printer;
9+
use Nette\Schema\Expect;
10+
use Nette\Schema\Processor;
11+
use Nette\Utils\FileSystem;
12+
use Nette\Utils\Strings;
13+
use Nette\Utils\Type;
14+
use Utilitte\Asserts\Exceptions\AssertionFailedException;
15+
use Utilitte\Asserts\TypeAssert;
16+
17+
require __DIR__ . '/vendor/autoload.php';
18+
19+
class DefaultPrinter extends Printer
20+
{
21+
22+
public function __construct()
23+
{
24+
parent::__construct();
25+
26+
$this->linesBetweenMethods = 1;
27+
$this->linesBetweenProperties = 1;
28+
}
29+
30+
public function printFile(PhpFile $file): string
31+
{
32+
$namespaces = [];
33+
foreach ($file->getNamespaces() as $namespace) {
34+
$namespaces[] = $this->printNamespace($namespace);
35+
}
36+
37+
return Strings::normalize(
38+
"<?php"
39+
. ($file->hasStrictTypes() ? " declare(strict_types = 1);\n" : '')
40+
. ($file->getComment() ? "\n" . Helpers::formatDocComment($file->getComment() . "\n") : '')
41+
. "\n"
42+
. implode("\n\n", $namespaces)
43+
) . "\n";
44+
}
45+
46+
public function printClass(ClassType $class, PhpNamespace $namespace = null): string
47+
{
48+
$lines = explode("\n", parent::printClass($class, $namespace));
49+
foreach ($lines as $i => $line) {
50+
if (preg_match('#^\s*(final|abstract)?\s*(class|interface|trait)#', $line)) {
51+
array_splice($lines, $i + 2, 0, '');
52+
53+
break;
54+
}
55+
}
56+
57+
array_splice($lines, -2, 0, '');
58+
59+
return implode("\n", $lines);
60+
}
61+
62+
}
63+
64+
class TypeAssertionGenerator
65+
{
66+
67+
public function __construct(
68+
private string $arrayTypeAssertTraitName,
69+
private string $typeAssertTraitName,
70+
private string $typeAssertClassName,
71+
private string $typeAssertionException,
72+
private array $types,
73+
)
74+
{
75+
}
76+
77+
public function runArray(array $types): string
78+
{
79+
$file = new PhpFile();
80+
$file->setStrictTypes();
81+
$namespace = $file->addNamespace(Helpers::extractNamespace($this->arrayTypeAssertTraitName));
82+
$namespace->addUse($this->typeAssertClassName);
83+
$class = $namespace->addTrait(Helpers::extractShortName($this->arrayTypeAssertTraitName));
84+
$class->addComment('@internal');
85+
86+
foreach ($types as $type) {
87+
$type = Type::fromString($type);
88+
89+
if ($type->isIntersection()) {
90+
throw new LogicException('Intersection is not supported.');
91+
}
92+
93+
$this->generateArray($type, $class, $namespace);
94+
}
95+
96+
return (new DefaultPrinter())->printFile($file);
97+
}
98+
99+
public function run(array $types): string
100+
{
101+
$file = new PhpFile();
102+
$file->setStrictTypes();
103+
$namespace = $file->addNamespace(Helpers::extractNamespace($this->typeAssertTraitName));
104+
$namespace->addUse($this->typeAssertionException);
105+
$class = $namespace->addTrait(Helpers::extractShortName($this->typeAssertTraitName));
106+
$class->addComment('@internal');
107+
108+
foreach ($types as $type) {
109+
$type = Type::fromString($type);
110+
111+
if ($type->isIntersection()) {
112+
throw new LogicException('Intersection is not supported.');
113+
}
114+
115+
$this->generate($type, $class, $namespace);
116+
}
117+
118+
return (new DefaultPrinter())->printFile($file);
119+
}
120+
121+
private function generateArray(Type $type, ClassType $class, PhpNamespace $namespace): void
122+
{
123+
$method = $class->addMethod($methodName = $this->generateName($type));
124+
$method->addParameter('array')
125+
->setType('mixed');
126+
$method->addParameter('key')
127+
->setType('int|string');
128+
$method->setReturnType($this->returnType($type))
129+
->setStatic();
130+
131+
$method->addBody(sprintf('return %s::%s(self::get($array, $key));', $namespace->simplifyType($this->typeAssertClassName), $methodName));
132+
}
133+
134+
private function generate(Type $type, ClassType $class, PhpNamespace $namespace): void
135+
{
136+
$method = $class->addMethod($this->generateName($type));
137+
$method->addParameter('value')
138+
->setType('mixed');
139+
$method->setReturnType($this->returnType($type))
140+
->setStatic();
141+
142+
$expandedType = $this->typeToStringExpanded($type);
143+
144+
$assertions = [];
145+
$epilogs = [];
146+
$prologs = [];
147+
foreach ($type->getTypes() as $singleType) {
148+
$struct = $this->types[$singleType->getSingleName()];
149+
$assertions = array_merge($assertions, $struct->assertions);
150+
if ($struct->prolog) {
151+
$prologs[] = $struct->prolog;
152+
}
153+
if ($struct->epilog) {
154+
$epilogs[] = $struct->epilogs;
155+
}
156+
}
157+
158+
if ($prologs) {
159+
$method->addBody(implode("\n", $prologs));
160+
$method->addBody('');
161+
}
162+
163+
$method->addBody(
164+
sprintf('if (%s) {', $this->generateCondition($assertions))
165+
);
166+
$method->addBody(
167+
sprintf(
168+
"\tthrow new %s(self::createErrorMessage(\$value, ?));",
169+
$namespace->simplifyName($this->typeAssertionException)
170+
),
171+
[$expandedType]
172+
);
173+
$method->addBody('}');
174+
175+
if ($epilogs) {
176+
$method->addBody('');
177+
$method->addBody(implode("\n", $epilogs));
178+
}
179+
180+
$method->addBody('');
181+
$method->addBody('return $value;');
182+
}
183+
184+
private function typeToStringExpanded(Type $type): string
185+
{
186+
$types = [];
187+
foreach ($type->getTypes() as $type) {
188+
$types[] = $type->getSingleName();
189+
}
190+
191+
return implode('|', $types);
192+
}
193+
194+
private function generateName(Type $type): string
195+
{
196+
$methodName = '';
197+
foreach ($type->getNames() as $name) {
198+
$methodName .= ucfirst($name) . 'Or';
199+
}
200+
201+
return lcfirst(substr($methodName, 0, -2));
202+
}
203+
204+
private function generateCondition(array $validators): string
205+
{
206+
return implode(' && ', $validators);
207+
}
208+
209+
private function returnType(Type $type): string
210+
{
211+
$returnType = '';
212+
foreach ($type->getTypes() as $type) {
213+
if ($type->isBuiltin()) {
214+
$returnType .= $type->getSingleName() . '|';
215+
} else {
216+
$struct = $this->types[$type->getSingleName()] ?? throw new LogicException(
217+
sprintf('Return type for type "%s" does not exist.', $type->getSingleName())
218+
);
219+
220+
foreach ($struct->returns as $item) {
221+
$returnType .= $item . '|';
222+
}
223+
}
224+
}
225+
226+
return (string) Type::fromString(substr($returnType, 0, -1));
227+
}
228+
229+
}
230+
231+
$types = [
232+
'array',
233+
'array|null',
234+
'object',
235+
'object|null',
236+
'string',
237+
'string|null',
238+
'int',
239+
'int|null',
240+
'float',
241+
'float|null',
242+
'int|float',
243+
'int|float|null',
244+
'numeric',
245+
'numericInt',
246+
'numericInt|null',
247+
'numericFloat',
248+
'numericFloat|null',
249+
];
250+
251+
$data = (new Processor())->process(Expect::structure([
252+
'types' => Expect::arrayOf(Expect::structure([
253+
'assertions' => Expect::anyOf(Expect::string(), Expect::arrayOf('string'))->castTo('array')->default([]),
254+
'returns' => Expect::anyOf(Expect::string(), Expect::arrayOf('string'))->castTo('array')->default([]),
255+
'prolog' => Expect::string()->default(null),
256+
'epilog' => Expect::string()->default(null),
257+
])),
258+
]), Neon::decode(FileSystem::read(__DIR__ . '/methods.neon')));
259+
260+
$generator = new TypeAssertionGenerator(
261+
'Utilitte\Asserts\Mixins\ArrayTypeAssertTrait',
262+
'Utilitte\Asserts\Mixins\TypeAssertTrait',
263+
TypeAssert::class,
264+
AssertionFailedException::class,
265+
$data->types,
266+
);
267+
268+
FileSystem::write(__DIR__ . '/../src/Mixins/TypeAssertTrait.php', $generator->run($types));
269+
FileSystem::write(__DIR__ . '/../src/Mixins/ArrayTypeAssertTrait.php', $generator->runArray($types));

bin/methods.neon

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
types:
2+
int:
3+
assertions: '!is_int($value)'
4+
float:
5+
assertions: '!is_float($value)'
6+
array:
7+
assertions: '!is_array($value)'
8+
object:
9+
assertions: '!is_object($value)'
10+
string:
11+
assertions: '!is_string($value)'
12+
null:
13+
assertions: '$value !== null'
14+
15+
numeric:
16+
assertions: '!is_float($value) && !is_int($value)'
17+
returns: [float, int]
18+
prolog: """
19+
if (is_string($value) && is_numeric($value)) {
20+
$value = str_contains($value, '.') ? (float) $value : (int) $value;
21+
}
22+
"""
23+
numericFloat:
24+
assertions: '!is_float($value)'
25+
returns: float
26+
prolog: """
27+
if (is_string($value) && is_numeric($value)) {
28+
$value = (float) preg_replace('#\\.0*$#D', '', $value);
29+
}
30+
"""
31+
numericInt:
32+
assertions: '!is_int($value)'
33+
returns: int
34+
prolog: """
35+
if (is_string($value) && is_numeric($value) && preg_match('#^[0-9]+$#D', $value)) {
36+
$value = (int) preg_replace('#\\.0*$#D', '', $value);
37+
}
38+
"""

composer.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "utilitte/asserts",
3+
"autoload": {
4+
"psr-4": {
5+
"Utilitte\\Asserts\\": "src"
6+
}
7+
},
8+
"require": {
9+
"php": ">= 8.0",
10+
"nette/tester": "^2.4",
11+
"nette/utils": "^3.2"
12+
}
13+
}

0 commit comments

Comments
 (0)