Skip to content

Commit 9023d06

Browse files
Merge pull request #26 from kirschbaum-development/feature/escape-string-values
Escape string values and method outputs in generated enums
2 parents b42d0ad + 0206f09 commit 9023d06

7 files changed

Lines changed: 78 additions & 18 deletions

File tree

.github/workflows/run-tests.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,9 @@ jobs:
1111
strategy:
1212
fail-fast: true
1313
matrix:
14-
php: [ 8.1, 8.2, 8.3, 8.4 ]
14+
php: [ 8.2, 8.3, 8.4 ]
1515
laravel: [ 10.*, 11.*, 12.* ]
1616
include:
17-
- php: 8.1
18-
laravel: 10.*
19-
pest: 2.*
20-
testbench: 8.*
21-
larastan: 2.*
2217
- php: 8.2
2318
laravel: 10.*
2419
pest: 2.*
@@ -64,11 +59,6 @@ jobs:
6459
pest: 3.*
6560
testbench: 10.*
6661
larastan: 3.*
67-
exclude:
68-
- php: 8.1
69-
laravel: 11.*
70-
- php: 8.1
71-
laravel: 12.*
7262

7363
name: Paragon Tests - PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}
7464

src/Concerns/Builders/EnumJsBuilder.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public function fileExtension(): string
3535

3636
/**
3737
* Prepare the method and its respective values so it can get injected into the case object.
38+
*
39+
* @throws \JsonException
3840
*/
3941
public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase|ReflectionEnumBackedCase $case): string
4042
{
@@ -46,7 +48,7 @@ public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase|Refl
4648
$value instanceof BackedEnum => "=> {$class}.{$value->name}",
4749
is_numeric($value) => "=> {$value}",
4850
is_null($value) => '=> null',
49-
default => "=> '{$value}'"
51+
default => '=> ' . json_encode($value, JSON_THROW_ON_ERROR)
5052
})
5153
->append(',');
5254
}

src/Concerns/Builders/EnumTsBuilder.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public function fileExtension(): string
3535

3636
/**
3737
* Prepare the method and its respective values so it can get injected into the case object.
38+
*
39+
* @throws \JsonException
3840
*/
3941
public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase|ReflectionEnumBackedCase $case): string
4042
{
@@ -46,7 +48,7 @@ public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase|Refl
4648
$value instanceof BackedEnum => "object => {$class}.{$value->name}",
4749
is_numeric($value) => "number => {$value}",
4850
is_null($value) => 'null => null',
49-
default => "string => '{$value}'"
51+
default => 'string => ' . json_encode($value, JSON_THROW_ON_ERROR)
5052
})
5153
->append(',');
5254
}

src/Generators/EnumGenerator.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ protected function buildCases(Collection $cases): string
216216

217217
/**
218218
* Prepare the value of the enum case object if it is a backed enum.
219+
*
220+
* @throws \JsonException
219221
*/
220222
protected function caseValueProperty(ReflectionEnumUnitCase|ReflectionEnumBackedCase $case): string
221223
{
@@ -228,7 +230,7 @@ protected function caseValueProperty(ReflectionEnumUnitCase|ReflectionEnumBacked
228230
? $string->append("{$case->getValue()->value}")
229231
: $string,
230232
fn ($string) => $case->getValue() instanceof BackedEnum
231-
? $string->append("'{$case->getValue()->value}'")
233+
? $string->append(json_encode($case->getValue()->value, JSON_THROW_ON_ERROR))
232234
: $string,
233235
)
234236
->append(',');
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace App\Enums;
4+
5+
enum EscapedStringBacked: string
6+
{
7+
case Quote = 'it\'s a "test"';
8+
9+
public function label(): string
10+
{
11+
return 'can\'t "stop"';
12+
}
13+
}

tests/Unit/Commands/GenerateEnumsCommandTest.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Enums\EscapedStringBacked;
34
use App\Enums\IntegerBacked;
45
use App\Enums\NonBacked;
56
use App\Enums\StringBacked;
@@ -20,7 +21,7 @@
2021
->toContain('class StringBacked extends Enum')
2122
->toContain(StringBacked::Active->name . ': Object.freeze({')
2223
->toContain("name: '" . StringBacked::Active->name . "',")
23-
->toContain("value: '" . StringBacked::Active->value . "',")
24+
->toContain('value: ' . json_encode(StringBacked::Active->value) . ',')
2425
->toContain('export default StringBacked;');
2526
});
2627

@@ -62,6 +63,41 @@
6263
->toContain('export default NonBacked;');
6364
});
6465

66+
it('escapes string values and method return values that need it', function () {
67+
// Act.
68+
$this->artisan(GenerateEnumsCommand::class);
69+
70+
$path = resource_path(config('paragon.enums.paths.generated') . DIRECTORY_SEPARATOR . 'EscapedStringBacked.ts');
71+
$file = file_get_contents($path);
72+
73+
// Assert.
74+
expect($path)->toBeFile()
75+
->and($file)
76+
->toContain('value: ' . json_encode(EscapedStringBacked::Quote->value) . ',')
77+
->toContain('label: (): string => ' . json_encode(EscapedStringBacked::Quote->label()) . ',');
78+
});
79+
80+
it('throws instead of writing broken output for a value that cannot be encoded as JSON', function () {
81+
// Arrange.
82+
// Written directly into app_path() rather than tests/Fixtures: GenerateEnumsCommand
83+
// processes every enum under that path in a single pass, so a fixture there that
84+
// intentionally fails generation would break every test that runs the command.
85+
file_put_contents(app_path('Enums/InvalidUtf8Backed.php'), <<<'PHP'
86+
<?php
87+
88+
namespace App\Enums;
89+
90+
enum InvalidUtf8Backed: string
91+
{
92+
case Bad = "\xB1\x31";
93+
}
94+
95+
PHP);
96+
97+
// Act.
98+
$this->artisan(GenerateEnumsCommand::class);
99+
})->throws(JsonException::class);
100+
65101
it('generates enums recursively', function () {
66102
// Act.
67103
$this->artisan(GenerateEnumsCommand::class);
@@ -102,7 +138,7 @@
102138
// type definition
103139
->toContain('label();')
104140
// items objects
105-
->toContain("label: (): string => '" . StringBacked::Active->label() . "',");
141+
->toContain('label: (): string => ' . json_encode(StringBacked::Active->label()) . ',');
106142
});
107143

108144
it('ignores methods with \'IgnoreParagon\' attribute', function () {

tests/Unit/Commands/GenerateJsEnumsCommandTest.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Enums\EscapedStringBacked;
34
use App\Enums\IntegerBacked;
45
use App\Enums\NonBacked;
56
use App\Enums\StringBacked;
@@ -20,7 +21,7 @@
2021
->toContain('class StringBacked extends Enum')
2122
->toContain(StringBacked::Active->name . ': Object.freeze({')
2223
->toContain("name: '" . StringBacked::Active->name . "',")
23-
->toContain("value: '" . StringBacked::Active->value . "',")
24+
->toContain('value: ' . json_encode(StringBacked::Active->value) . ',')
2425
->toContain('export default StringBacked;');
2526
});
2627

@@ -62,6 +63,20 @@
6263
->toContain('export default NonBacked;');
6364
});
6465

66+
it('escapes string values and method return values that need it', function () {
67+
// Act.
68+
$this->artisan(GenerateEnumsCommand::class, ['--javascript' => true]);
69+
70+
$path = resource_path(config('paragon.enums.paths.generated') . DIRECTORY_SEPARATOR . 'EscapedStringBacked.js');
71+
$file = file_get_contents($path);
72+
73+
// Assert.
74+
expect($path)->toBeFile()
75+
->and($file)
76+
->toContain('value: ' . json_encode(EscapedStringBacked::Quote->value) . ',')
77+
->toContain('label: () => ' . json_encode(EscapedStringBacked::Quote->label()) . ',');
78+
});
79+
6580
it('generates enums recursively', function () {
6681
// Act.
6782
$this->artisan(GenerateEnumsCommand::class, ['--javascript' => true]);
@@ -102,7 +117,7 @@
102117
// type definition
103118
->not->toContain('label();')
104119
// items objects
105-
->toContain("label: () => '" . StringBacked::Active->label() . "',");
120+
->toContain('label: () => ' . json_encode(StringBacked::Active->label()) . ',');
106121
});
107122

108123
it('ignores methods with \'IgnoreParagon\' attribute', function () {

0 commit comments

Comments
 (0)