diff --git a/README.md b/README.md index 59dbe69..8712f2a 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,23 @@ Assert::that(['foo', 'bar']) ; ``` +### Json Expectations + +```php +use Zenstruck\Assert; + +Assert::that('[4, 5, 6]') + ->isJson() // json_decode's the current value and starts a new expectation with this + ->contains(5) +; + +Assert::that('5') + ->isJson() + ->is(5) + ->isGreaterThan(4) +; +``` + ## `AssertionFailed` Exception When triggering a failed assertion, it is important to provide a useful failure diff --git a/src/Assert/Expectation.php b/src/Assert/Expectation.php index 61152e4..ebd8fbf 100644 --- a/src/Assert/Expectation.php +++ b/src/Assert/Expectation.php @@ -357,4 +357,28 @@ public function throws($expectedException, ?string $expectedMessage = null): sel return $this; } + + /** + * Json decodes the current value and returns a new instance + * wrapping the decoded value. + * + * Fails if not a valid json string. + */ + public function isJson(): self + { + if (!\is_string($this->value)) { + Assert::fail('"{value}" is not a string.', ['value' => $this->value]); + } + + // TODO: use \JSON_THROW_ON_ERROR once min PHP >= 7.3 + $decoded = \json_decode($this->value, true); + + if (\JSON_ERROR_NONE !== \json_last_error()) { + Assert::fail('"{value}" is not a json string.', ['value' => $this->value]); + } + + Assert::pass(); + + return new self($decoded); + } } diff --git a/tests/ExpectationTest.php b/tests/ExpectationTest.php index f9d4ac9..ea0be4c 100644 --- a/tests/ExpectationTest.php +++ b/tests/ExpectationTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Zenstruck\Assert; +use Zenstruck\Assert\AssertionFailed; use Zenstruck\Assert\Tests\Fixture\CountableObject; use Zenstruck\Assert\Tests\Fixture\IterableObject; use Zenstruck\Assert\Type; @@ -736,6 +737,37 @@ public static function isTypeFailProvider(): iterable yield ['foo', Type::json()]; } + /** + * @test + */ + public function is_json(): void + { + $this->assertSuccess(6, function() { + Assert::that('6')->isJson()->is(Type::int())->isGreaterThan(5); + Assert::that('[6, 7]')->isJson()->contains(7)->doesNotContain(5); + }); + } + + /** + * @test + */ + public function is_json_failure(): void + { + $this->assertFails('"stdClass" is not a string.', function() { + try { + Assert::that(new \stdClass())->isJson(); + } catch (AssertionFailed $e) { + } + }); + + $this->assertFails('"foo" is not a json string.', function() { + try { + Assert::that('foo')->isJson(); + } catch (AssertionFailed $e) { + } + }); + } + private function assertSuccess(int $expectedCount, callable $what): self { $this->handler->reset();