Skip to content

Commit

Permalink
feature #53749 [Validator] Add Yaml constraint for validating YAML …
Browse files Browse the repository at this point in the history
…content (symfonyaml)

This PR was squashed before being merged into the 7.2 branch.

Discussion
----------

[Validator] Add `Yaml` constraint for validating YAML content

| Q             | A
| ------------- | ---
| Branch?       | 7.2
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Ticket? | no
| License       | MIT

## Purpose
Inspired by the [Json constraint](https://symfony.com/doc/current/reference/constraints/Json.html), I've added a new feature to the Validator component for validating YAML content with a dedicated constraint.

**Real world use case**: Having configuration settings stored in YAML format within a database. With this new feature, you can validate the integrity of these configurations, ensuring the YAML syntax is OK.

## Options
I've added a `flags` option to this constraint, aligning with the [Yaml parser flags](https://symfony.com/doc/current/components/yaml.html#advanced-usage-flags).

## Exemple
```php
namespace App\Entity;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Yaml\Yaml;

class Configuration
{
    #[Assert\Yaml(flags: Yaml::PARSE_DATETIME)]
    private string $content;
}
```

Commits
-------

023d48c [Validator] Add `Yaml` constraint for validating YAML content
  • Loading branch information
fabpot committed Jun 14, 2024
2 parents 678abb4 + 023d48c commit e0ad00c
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

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

7.1
---
Expand Down
44 changes: 44 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Yaml.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\LogicException;
use Symfony\Component\Yaml\Parser;

/**
* @author Kev <https://github.com/symfonyaml>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Yaml extends Constraint
{
public const INVALID_YAML_ERROR = '63313a31-837c-42bb-99eb-542c76aacc48';

protected const ERROR_NAMES = [
self::INVALID_YAML_ERROR => 'INVALID_YAML_ERROR',
];

#[HasNamedArguments]
public function __construct(
public string $message = 'This value is not valid YAML.',
public int $flags = 0,
?array $groups = null,
mixed $payload = null,
) {
if (!class_exists(Parser::class)) {
throw new LogicException('The Yaml component is required to use the Yaml constraint. Try running "composer require symfony/yaml".');
}

parent::__construct(null, $groups, $payload);
}
}
63 changes: 63 additions & 0 deletions src/Symfony/Component/Validator/Constraints/YamlValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;

/**
* @author Kev <https://github.com/symfonyaml>
*/
class YamlValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof Yaml) {
throw new UnexpectedTypeException($constraint, Yaml::class);
}

if (null === $value || '' === $value) {
return;
}

if (!\is_scalar($value) && !$value instanceof \Stringable) {
throw new UnexpectedValueException($value, 'string');
}

$value = (string) $value;

/** @see \Symfony\Component\Yaml\Command\LintCommand::validate() */
$prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) {
if (\E_USER_DEPRECATED === $level) {
throw new ParseException($message, $this->getParser()->getRealCurrentLineNb() + 1);
}

return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
});

try {
(new Parser())->parse($value, $constraint->flags);
} catch (ParseException $e) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ error }}', $e->getMessage())
->setParameter('{{ line }}', $e->getParsedLine())
->setCode(Yaml::INVALID_YAML_ERROR)
->addViolation();
} finally {
restore_error_handler();
}
}
}
57 changes: 57 additions & 0 deletions src/Symfony/Component/Validator/Tests/Constraints/YamlTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Tests\Constraints;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\Yaml;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
use Symfony\Component\Yaml\Yaml as YamlParser;

/**
* @author Kev <https://github.com/symfonyaml>
*/
class YamlTest extends TestCase
{
public function testAttributes()
{
$metadata = new ClassMetadata(YamlDummy::class);
$loader = new AttributeLoader();
self::assertTrue($loader->loadClassMetadata($metadata));

[$bConstraint] = $metadata->properties['b']->getConstraints();
self::assertSame('myMessage', $bConstraint->message);
self::assertSame(['Default', 'YamlDummy'], $bConstraint->groups);

[$cConstraint] = $metadata->properties['c']->getConstraints();
self::assertSame(['my_group'], $cConstraint->groups);
self::assertSame('some attached data', $cConstraint->payload);

[$cConstraint] = $metadata->properties['d']->getConstraints();
self::assertSame(YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS, $cConstraint->flags);
}
}

class YamlDummy
{
#[Yaml]
private $a;

#[Yaml(message: 'myMessage')]
private $b;

#[Yaml(groups: ['my_group'], payload: 'some attached data')]
private $c;

#[Yaml(flags: YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS)]
private $d;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Tests\Constraints;

use Symfony\Component\Validator\Constraints\Yaml;
use Symfony\Component\Validator\Constraints\YamlValidator;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
use Symfony\Component\Yaml\Yaml as YamlParser;

/**
* @author Kev <https://github.com/symfonyaml>
*/
class YamlValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator(): YamlValidator
{
return new YamlValidator();
}

/**
* @dataProvider getValidValues
*/
public function testYamlIsValid($value)
{
$this->validator->validate($value, new Yaml());

$this->assertNoViolation();
}

public function testYamlWithFlags()
{
$this->validator->validate('date: 2023-01-01', new Yaml(flags: YamlParser::PARSE_DATETIME));
$this->assertNoViolation();
}

/**
* @dataProvider getInvalidValues
*/
public function testInvalidValues($value, $message, $line)
{
$constraint = new Yaml(
message: 'myMessageTest',
);

$this->validator->validate($value, $constraint);

$this->buildViolation('myMessageTest')
->setParameter('{{ error }}', $message)
->setParameter('{{ line }}', $line)
->setCode(Yaml::INVALID_YAML_ERROR)
->assertRaised();
}

public function testInvalidFlags()
{
$value = 'tags: [!tagged app.myclass]';
$this->validator->validate($value, new Yaml());
$this->buildViolation('This value is not valid YAML.')
->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]").')
->setParameter('{{ line }}', 1)
->setCode(Yaml::INVALID_YAML_ERROR)
->assertRaised();
}

public static function getValidValues()
{
return [
['planet_diameters: {earth: 12742, mars: 6779, saturn: 116460, mercury: 4879}'],
["key:\n value"],
[null],
[''],
['"null"'],
['null'],
['"string"'],
['1'],
['true'],
[1],
];
}

public static function getInvalidValues(): array
{
return [
['{:INVALID]', 'Malformed unquoted YAML string at line 1 (near "{:INVALID]").', 1],
["key:\nvalue", 'Unable to parse at line 2 (near "value").', 2],
];
}
}

0 comments on commit e0ad00c

Please sign in to comment.