8000 [Validator] Add `Yaml` constraint for validating YAML content · symfony/symfony@023d48c · GitHub
[go: up one dir, main page]

Skip to content

Commit 023d48c

Browse files
symfonyamlfabpot
symfonyaml
authored andcommitted
[Validator] Add Yaml constraint for validating YAML content
1 parent 678abb4 commit 023d48c

File tree

5 files changed

+262
-0
lines changed

5 files changed

+262
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Make `PasswordStrengthValidator::estimateStrength()` public
8+
* Add the `Yaml` constraint for validating YAML content
89

910
7.1
1011
---
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Attribute\HasNamedArguments;
15+
use Symfony\Component\Validator\Constraint;
16+
use Symfony\Component\Validator\Exception\LogicException;
17+
use Symfony\Component\Yaml\Parser;
18+
19+
/**
20+
* @author Kev <https://github.com/symfonyaml>
21+
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
23+
class Yaml extends Constraint
24+
{
25+
public const INVALID_YAML_ERROR = '63313a31-837c-42bb-99eb-542c76aacc48';
26+
27+
protected const ERROR_NAMES = [
28+
self::INVALID_YAML_ERROR => 'INVALID_YAML_ERROR',
29+
];
30+
31+
#[HasNamedArguments]
32+
public function __construct(
33+
public string $message = 'This value is not valid YAML.',
34+
public int $flags = 0,
35+
?array $groups = null,
36+
mixed $payload = null,
37+
) {
38+
if (!class_exists(Parser::class)) {
39+
throw new LogicException('The Yaml component is required to use the Yaml constraint. Try running "composer require symfony/yaml".');
40+
}
41+
42+
parent::__construct(null, $groups, $payload);
43+
}
44+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\ConstraintValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
use Symfony\Component\Yaml\Exception\ParseException;
19+
use Symfony\Component\Yaml\Parser;
20+
21+
/**
22+
* @author Kev <https://github.com/symfonyaml>
23+
*/
24+
class YamlValidator extends ConstraintValidator
25+
{
26+
public function validate(mixed $value, Constraint $constraint): void
27+
{
28+
if (!$constraint instanceof Yaml) {
29+
throw new UnexpectedTypeException($constraint, Yaml::class);
30+
}
31+
32+
if (null === $value || '' === $value) {
33+
return;
34+
}
35+
36+
if (!\is_scalar($value) && !$value instanceof \Stringable) {
37+
throw new UnexpectedValueException($value, 'string');
38+
}
39+
40+
$value = (string) $value;
41+
42+
/** @see \Symfony\Component\Yaml\Command\LintCommand::validate() */
43+
$prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) {
44+
if (\E_USER_DEPRECATED === $level) {
45+
throw new ParseException($message, $this->getParser()->getRealCurrentLineNb() + 1);
46+
}
47+
48+
return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
49+
});
50+
51+
try {
52+
(new Parser())->parse($value, $constraint->flags);
53+
} catch (ParseException $e) {
54+
$this->context->buildViolation($constraint->message)
55+
->setParameter('{{ error }}', $e->getMessage())
56+
->setParameter('{{ line }}', $e->getParsedLine())
57+
->setCode(Yaml::INVALID_YAML_ERROR)
58+
->addViolation();
59+
} finally {
60+
restore_error_handler();
61+
}
62+
}
63+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Validator\Constraints\Yaml;
16+
use Symfony\Component\Validator\Mapping\ClassMetadata;
17+
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
18+
use Symfony\Component\Yaml\Yaml as YamlParser;
19+
20+
/**
21+
* @author Kev <https://github.com/symfonyaml>
22+
*/
23+
class YamlTest extends TestCase
24+
{
25+
public function testAttributes()
26+
{
27+
$metadata = new ClassMetadata(YamlDummy::class);
28+
$loader = new AttributeLoader();
29+
self::assertTrue($loader->loadClassMetadata($metadata));
30+
31+
[$bConstraint] = $metadata->properties['b']->getConstraints();
32+
self::assertSame('myMessage', $bConstraint->message);
33+
self::assertSame(['Default', 'YamlDummy'], $bConstraint->groups);
34+
35+
[$cConstraint] = $metadata->properties['c']->getConstraints();
36+
self::assertSame(['my_group'], $cConstraint->groups);
37+
self::assertSame('some attached data', $cConstraint->payload);
38+
39+
[$cConstraint] = $metadata->properties['d']->getConstraints();
40+
self::assertSame(YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS, $cConstraint->flags);
41+
}
42+
}
43+
44+
class YamlDummy
45+
{
46+
#[Yaml]
47+
private $a;
48+
49+
#[Yaml(message: 'myMessage')]
50+
private $b;
51+
52+
#[Yaml(groups: ['my_group'], payload: 'some attached data')]
53+
private $c;
54+
55+
#[Yaml(flags: YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS)]
56+
private $d;
57+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use Symfony\Component\Validator\Constraints\Yaml;
15+
use Symfony\Component\Validator\Constraints\YamlValidator;
16+
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
17+
use Symfony\Component\Yaml\Yaml as YamlParser;
18+
19+
/**
20+
* @author Kev <https://github.com/symfonyaml>
21+
*/
22+
class YamlValidatorTest extends ConstraintValidatorTestCase
23+
{
24+
protected function createValidator(): YamlValidator
25+
{
26+
return new YamlValidator();
27+
}
28+
29+
/**
30+
* @dataProvider getValidValues
31+
*/
32+
public function testYamlIsValid($value)
33+
{
34+
$this->validator->validate($value, new Yaml());
35+
36+
$this->assertNoViolation();
37+
}
38+
39+
public function testYamlWithFlags()
40+
{
41+
$this->validator->validate('date: 2023-01-01', new Yaml(flags: YamlParser::PARSE_DATETIME));
42+
$this->assertNoViolation();
43+
}
44+
45+
/**
46+
* @dataProvider getInvalidValues
47+
*/
48+
public function testInvalidValues($value, $message, $line)
49+
{
50+
$constraint = new Yaml(
51+
message: 'myMessageTest',
52+
);
53+
54+
$this->validator->validate($value, $constraint);
55+
56+
$this->buildViolation('myMessageTest')
57+
->setParameter('{{ error }}', $message)
58+
->setParameter('{{ line }}', $line)
59+
->setCode(Yaml::INVALID_YAML_ERROR)
60+
->assertRaised();
61+
}
62+
63+
public function testInvalidFlags()
64+
{
65+
$value = 'tags: [!tagged app.myclass]';
66+
$this->validator->validate($value, new Yaml());
67+
$this->buildViolation('This value is not valid YAML.')
68+
->setParameter('{{ error }}', 'Tags support is not enabled. Enable the "Yaml::PARSE_CUSTOM_TAGS" flag to use "!tagged" at line 1 (near "tags: [!tagged app.myclass]").')
69+
->setParameter('{{ line }}', 1)
70+
->setCode(Yaml::INVALID_YAML_ERROR)
71+
->assertRaised();
72+
}
73+
74+
public static function getValidValues()
75+
{
76+
return [
77+
['planet_diameters: {earth: 12742, mars: 6779, saturn: 116460, mercury: 4879}'],
78+
["key:\n value"],
79+
[null],
80+
[''],
81+
['"null"'],
82+
['null'],
83+
['"string"'],
84+
['1'],
85+
['true'],
86+
[1],
87+
];
88+
}
89+
90+
public static function getInvalidValues(): array
91+
{
92+
return [
93+
['{:INVALID]', 'Malformed unquoted YAML string at line 1 (near "{:INVALID]").', 1],
94+
["key:\nvalue", 'Unable to parse at line 2 (near "value").', 2],
95+
];
96+
}
97+
}

0 commit comments

Comments
 (0)
0