Skip to content

Commit

Permalink
[feature] add TypeAssertion and type expectations (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
kbond committed Jul 11, 2022
1 parent ca27cac commit af570fd
Show file tree
Hide file tree
Showing 6 changed files with 454 additions and 2 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ Assert::that(6)->isNot(6); // fail
Assert::that('foo')->isNot('bar'); // pass
Assert::that(6)->isNot('6'); // pass

// instanceof
Assert::that($object)->isInstanceOf(Some::class);

Assert::that($object)->isNotInstanceOf(Some::class);

// greater than
Assert::that(2)->isGreaterThan(1); // pass
Assert::that(2)->isGreaterThan(1); // fail
Expand All @@ -201,6 +206,46 @@ Assert::that(3)->isLessThanOrEqualTo(2); // fail
Assert::that(3)->isLessThanOrEqualTo(3); // pass
```

### Type Expectations

```php
use Zenstruck\Assert;
use Zenstruck\Assert\Type;

Assert::that($something)->is(Type::bool());
Assert::that($something)->is(Type::int());
Assert::that($something)->is(Type::float());
Assert::that($something)->is(Type::numeric());
Assert::that($something)->is(Type::string());
Assert::that($something)->is(Type::callable());
Assert::that($something)->is(Type::iterable());
Assert::that($something)->is(Type::countable());
Assert::that($something)->is(Type::object());
Assert::that($something)->is(Type::resource());
Assert::that($something)->is(Type::array());
Assert::that($something)->is(Type::arrayList()); // [1, 2, 3] passes but ['foo' => 'bar'] does not
Assert::that($something)->is(Type::arrayAssoc()); // ['foo' => 'bar'] passes but [1, 2, 3] does not
Assert::that($something)->is(Type::arrayEmpty()); // [] passes but [1, 2, 3] does not
Assert::that($something)->is(Type::json()); // valid json string

// "Not's"
Assert::that($something)->isNot(Type::bool());
Assert::that($something)->isNot(Type::int());
Assert::that($something)->isNot(Type::float());
Assert::that($something)->isNot(Type::numeric());
Assert::that($something)->isNot(Type::string());
Assert::that($something)->isNot(Type::callable());
Assert::that($something)->isNot(Type::iterable());
Assert::that($something)->isNot(Type::countable());
Assert::that($something)->isNot(Type::object());
Assert::that($something)->isNot(Type::resource());
Assert::that($something)->isNot(Type::array());
Assert::that($something)->isNot(Type::arrayList());
Assert::that($something)->isNot(Type::arrayAssoc());
Assert::that($something)->isNot(Type::arrayEmpty());
Assert::that($something)->isNot(Type::json());
```

### Throws Expectation

This expectation provides a nice API for exceptions. It is an alternative to PHPUnit's
Expand Down
53 changes: 53 additions & 0 deletions src/Assert/Assertion/TypeAssertion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Zenstruck\Assert\Assertion;

use Zenstruck\Assert\Type;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class TypeAssertion extends EvaluableAssertion
{
/** @var mixed */
private $value;

/** @var Type */
private $expected;

/**
* @param mixed $value
* @param string|null $message Available context: {value}, {expected}, {actual}
*/
public function __construct($value, Type $expected, ?string $message = null, array $context = [])
{
$this->value = $value;
$this->expected = $expected;

parent::__construct($message, $context);
}

protected function evaluate(): bool
{
return ($this->expected)($this->value);
}

protected function defaultFailureMessage(): string
{
return 'Expected "{value}" to be of type {expected} but is {actual}.';
}

protected function defaultNotFailureMessage(): string
{
return 'Expected "{value}" to NOT be of type {expected}.';
}

protected function defaultContext(): array
{
return [
'value' => $this->value,
'expected' => (string) $this->expected,
'actual' => \get_debug_type($this->value),
];
}
}
21 changes: 19 additions & 2 deletions src/Assert/Expectation.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Zenstruck\Assert\Assertion\CountAssertion;
use Zenstruck\Assert\Assertion\EmptyAssertion;
use Zenstruck\Assert\Assertion\ThrowsAssertion;
use Zenstruck\Assert\Assertion\TypeAssertion;

/**
* @author Kevin Bond <kevinbond@gmail.com>
Expand Down Expand Up @@ -201,11 +202,19 @@ public function isNotEqualTo($expected, ?string $message = null, array $context
/**
* Asserts the expectation value and $expected are "the same" using "===".
*
* @param mixed $expected
* If a {@see Type} object is passed as expected, asserts the type matches.
*
* @param mixed|Type $expected
* @param string|null $message Available context: {expected}, {actual}
*/
public function is($expected, ?string $message = null, array $context = []): self
{
if ($expected instanceof Type) {
Assert::run(new TypeAssertion($this->value, $expected, $message, $context));

return $this;
}

Assert::run(ComparisonAssertion::same($this->value, $expected, $message, $context));

return $this;
Expand All @@ -214,11 +223,19 @@ public function is($expected, ?string $message = null, array $context = []): sel
/**
* Asserts the expectation value and $expected are NOT "the same" using "!==".
*
* @param mixed $expected
* If a {@see Type} object is passed as expected, asserts the type DOES NOT matche.
*
* @param mixed|Type $expected
* @param string|null $message Available context: {expected}, {actual}
*/
public function isNot($expected, ?string $message = null, array $context = []): self
{
if ($expected instanceof Type) {
Assert::not(new TypeAssertion($this->value, $expected, $message, $context));

return $this;
}

Assert::not(ComparisonAssertion::same($this->value, $expected, $message, $context));

return $this;
Expand Down
188 changes: 188 additions & 0 deletions src/Assert/Type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

namespace Zenstruck\Assert;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class Type
{
private const BOOL = 'bool';
private const STRING = 'string';
private const INT = 'int';
private const FLOAT = 'float';
private const NUMERIC = 'numeric';
private const CALLABLE = 'callable';
private const RESOURCE = 'resource';
private const ITERABLE = 'iterable';
private const COUNTABLE = 'countable';
private const OBJECT = 'object';
private const ARRAY = 'array';
private const ARRAY_LIST = 'array:list';
private const ARRAY_ASSOC = 'array:assoc';
private const ARRAY_EMPTY = 'array:empty';
private const STRING_JSON = 'string:json';

/** @var string */
private $value;

private function __construct(string $value)
{
$this->value = $value;
}

/**
* @internal
*
* @param mixed $value
*/
public function __invoke($value): bool
{
switch ($this->value) {
case self::BOOL:
return \is_bool($value);

case self::STRING:
return \is_string($value);

case self::INT:
return \is_int($value);

case self::FLOAT:
return \is_float($value);

case self::NUMERIC:
return \is_numeric($value);

case self::CALLABLE:
return \is_callable($value);

case self::RESOURCE:
return \is_resource($value);

case self::OBJECT:
return \is_object($value);

case self::ITERABLE:
return \is_iterable($value);

case self::COUNTABLE:
return \is_countable($value);

case self::ARRAY:
return \is_array($value);

case self::ARRAY_LIST:
return \is_array($value) && $value && array_is_list($value);

case self::ARRAY_ASSOC:
return \is_array($value) && $value && !array_is_list($value);

case self::ARRAY_EMPTY:
return \is_array($value) && !$value;

case self::STRING_JSON:
return self::isJson($value);
}

return \is_object($value) && $this->value === \get_class($value);
}

/**
* @internal
*/
public function __toString(): string
{
return $this->value;
}

public static function bool(): self
{
return new self(self::BOOL);
}

public static function string(): self
{
return new self(self::STRING);
}

public static function int(): self
{
return new self(self::INT);
}

public static function float(): self
{
return new self(self::FLOAT);
}

public static function numeric(): self
{
return new self(self::NUMERIC);
}

public static function callable(): self
{
return new self(self::CALLABLE);
}

public static function resource(): self
{
return new self(self::RESOURCE);
}

public static function iterable(): self
{
return new self(self::ITERABLE);
}

public static function countable(): self
{
return new self(self::COUNTABLE);
}

public static function array(): self
{
return new self(self::ARRAY);
}

public static function arrayList(): self
{
return new self(self::ARRAY_LIST);
}

public static function arrayAssoc(): self
{
return new self(self::ARRAY_ASSOC);
}

public static function arrayEmpty(): self
{
return new self(self::ARRAY_EMPTY);
}

public static function json(): self
{
return new self(self::STRING_JSON);
}

public static function object(): self
{
return new self(self::OBJECT);
}

/**
* @param mixed $value
*/
private static function isJson($value): bool
{
if (!\is_string($value)) {
return false;
}

// TODO: use \JSON_THROW_ON_ERROR once min PHP >= 7.3
\json_decode($value);

return \JSON_ERROR_NONE === \json_last_error();
}
}
Loading

0 comments on commit af570fd

Please sign in to comment.