diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b08aacb..316c572 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,8 @@ name: build on: - push: - pull_request: + push: + pull_request: jobs: tests: @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.1] + php: [8.1, 8.2, 8.3] dependency-version: [prefer-stable] os: [ubuntu-latest] @@ -25,7 +25,6 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip tools: composer:v2 coverage: none @@ -34,7 +33,7 @@ jobs: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - name: Execute tests - run: vendor/bin/pest --verbose + run: vendor/bin/pest coverage: runs-on: ubuntu-latest @@ -63,12 +62,12 @@ jobs: - name: Upload coverage run: | - vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover + vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover - style: + linting: runs-on: ubuntu-latest - name: Coding style + name: Linting steps: - name: Checkout code @@ -77,9 +76,13 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 - tools: phpcs + php-version: 8.2 + tools: composer:v2 coverage: none - - name: Execute check - run: phpcs --standard=psr12 src/ + - name: Install dependencies + run: | + composer update --prefer-stable --prefer-dist --no-interaction + + - name: Execute Duster + run: vendor/bin/duster lint -u tlint,phpcodesniffer,pint,phpstan -vvv diff --git a/.gitignore b/.gitignore index 7ae6add..c9c70a3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ composer.lock vendor phpcs.xml phpunit.xml +.phpunit.cache .phpunit.result.cache +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7906b..12861df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,15 @@ All notable changes to `enum` will be documented in this file. Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles. + ## NEXT - YYYY-MM-DD ### Added - Nothing +### Changed +- Nothing + ### Deprecated - Nothing @@ -22,6 +26,33 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi - Nothing +## 2.0.0 - 2024-10-05 + +### Added +- Custom and default implementation of magic methods +- The `Meta` attribute and related methods +- Method `value()` to get the value of a backed case or the name of a pure case +- Methods `toArray()`, `map()` to the `CasesCollection` +- Generics in docblocks +- Static analysis + +### Changed +- Renamed keys to meta +- `CasesCollection` methods return an instance of the collection whenever possible +- `CasesCollection::groupBy()` groups into instances of the collection +- Filtering methods keep the collection keys +- Renamed methods `CollectsCases::casesBy*()` to `CollectsCases::keyBy*()` +- Renamed `cases()` to `all()` in `CasesCollection` +- Renamed `get()` to `resolveMeta()` in `SelfAware` +- When hydrating from meta, the value is no longer mandatory and it defaults to `true` +- The value for `pluck()` is now mandatory +- Renamed sorting methods +- Introduced PER code style + +### Removed +- Parameter `$default` from the `CasesCollection::first()` method + + ## 1.0.0 - 2022-07-12 ### Added diff --git a/README.md b/README.md index 853d5bb..6bdbd6a 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,13 @@ [![Build Status][ico-actions]][link-actions] [![Coverage Status][ico-scrutinizer]][link-scrutinizer] [![Quality Score][ico-code-quality]][link-code-quality] +[![PHPStan Level][ico-phpstan]][link-phpstan] [![Latest Version][ico-version]][link-packagist] [![Software License][ico-license]](LICENSE.md) -[![PSR-12][ico-psr12]][link-psr12] +[![PER][ico-per]][link-per] [![Total Downloads][ico-downloads]][link-downloads] -Zero-dependencies PHP library to supercharge enum functionalities. Similar libraries worth mentioning are: -- [Enum Helper](https://github.com/datomatic/enum-helper) by [Alberto Peripolli](https://github.com/trippo) -- [Enums](https://github.com/archtechx/enums) by [Samuel ล tancl](https://github.com/stancl) +Zero-dependencies PHP library to supercharge enum functionalities. ## ๐Ÿ“ฆ Install @@ -25,14 +24,15 @@ composer require cerbero/enum ## ๐Ÿ”ฎ Usage -* [Classification](#classification) -* [Comparison](#comparison) -* [Keys resolution](#keys-resolution) -* [Hydration](#hydration) -* [Elaborating cases](#elaborating-cases) -* [Cases collection](#cases-collection) +* [โš–๏ธ Comparison](#%EF%B8%8F-comparison) +* [๐Ÿท๏ธ Meta](#%EF%B8%8F-meta) +* [๐Ÿšฐ Hydration](#-hydration) +* [๐ŸŽฒ Enum operations](#-enum-operations) +* [๐Ÿงบ Cases collection](#-cases-collection) +* [๐Ÿช„ Magic](#-magic) +* [๐Ÿคณ Self-awareness](#-self-awareness) -To supercharge our enums with all functionalities provided by this package, we can simply use the `Enumerates` trait in both pure enums and backed enums: +To supercharge our enums with all the features provided by this package, we can let our enums use the `Enumerates` trait: ```php use Cerbero\Enum\Concerns\Enumerates; @@ -41,43 +41,30 @@ enum PureEnum { use Enumerates; - case one; - case two; - case three; + case One; + case Two; + case Three; } enum BackedEnum: int { use Enumerates; - case one = 1; - case two = 2; - case three = 3; + case One = 1; + case Two = 2; + case Three = 3; } ``` -### Classification +### โš–๏ธ Comparison -These methods determine whether an enum is pure or backed: +We can check whether an enum includes some names or values. Pure enums check for names and backed enums check for values: ```php -PureEnum::isPure(); // true -PureEnum::isBacked(); // false - -BackedEnum::isPure(); // false -BackedEnum::isBacked(); // true -``` - - -### Comparison - -We can check whether an enum includes some names or values. Pure enums check for names, whilst backed enums check for values: - -```php -PureEnum::has('one'); // true +PureEnum::has('One'); // true PureEnum::has('four'); // false -PureEnum::doesntHave('one'); // false +PureEnum::doesntHave('One'); // false PureEnum::doesntHave('four'); // true BackedEnum::has(1); // true @@ -86,217 +73,213 @@ BackedEnum::doesntHave(1); // false BackedEnum::doesntHave(4); // true ``` -Otherwise we can let cases determine whether they match with a name or a value: +Otherwise we can check whether cases match a given name or value: ```php -PureEnum::one->is('one'); // true -PureEnum::one->is(1); // false -PureEnum::one->is('four'); // false -PureEnum::one->isNot('one'); // false -PureEnum::one->isNot(1); // true -PureEnum::one->isNot('four'); // true - -BackedEnum::one->is(1); // true -BackedEnum::one->is('1'); // false -BackedEnum::one->is(4); // false -BackedEnum::one->isNot(1); // false -BackedEnum::one->isNot('1'); // true -BackedEnum::one->isNot(4); // true +PureEnum::One->is('One'); // true +PureEnum::One->is(1); // false +PureEnum::One->is('four'); // false +PureEnum::One->isNot('One'); // false +PureEnum::One->isNot(1); // true +PureEnum::One->isNot('four'); // true + +BackedEnum::One->is(1); // true +BackedEnum::One->is('1'); // false +BackedEnum::One->is(4); // false +BackedEnum::One->isNot(1); // false +BackedEnum::One->isNot('1'); // true +BackedEnum::One->isNot(4); // true ``` -Comparisons can also be performed within arrays: +Comparisons can also be performed against arrays: ```php -PureEnum::one->in(['one', 'four']); // true -PureEnum::one->in([1, 4]); // false -PureEnum::one->notIn('one', 'four'); // false -PureEnum::one->notIn([1, 4]); // true - -BackedEnum::one->in([1, 4]); // true -BackedEnum::one->in(['one', 'four']); // false -BackedEnum::one->notIn([1, 4]); // false -BackedEnum::one->notIn(['one', 'four']); // true +PureEnum::One->in(['One', 'four']); // true +PureEnum::One->in([1, 4]); // false +PureEnum::One->notIn(['One', 'four']); // false +PureEnum::One->notIn([1, 4]); // true + +BackedEnum::One->in([1, 4]); // true +BackedEnum::One->in(['One', 'four']); // false +BackedEnum::One->notIn([1, 4]); // false +BackedEnum::One->notIn(['One', 'four']); // true ``` -### Keys resolution +### ๐Ÿท๏ธ Meta -With the term "key" we refer to any element defined in an enum, such as names, values or methods implemented by cases. Take the following enum for example: +Meta add extra information to a case. Meta can be added by implementing a public non-static method and/or by attaching `#[Meta]` attributes to cases: ```php enum BackedEnum: int { use Enumerates; - case one = 1; - case two = 2; - case three = 3; + #[Meta(color: 'red', shape: 'triangle')] + case One = 1; - public function color(): string - { - return match ($this) { - static::one => 'red', - static::two => 'green', - static::three => 'blue', - }; - } + #[Meta(color: 'green', shape: 'square')] + case Two = 2; + + #[Meta(color: 'blue', shape: 'circle')] + case Three = 3; public function isOdd(): bool { - return match ($this) { - static::one => true, - static::two => false, - static::three => true, - }; + return $this->value % 2 != 0; } } ``` -The keys defined in this enum are `name`, `value` (as it is a backed enum), `color` and `isOdd`. We can retrieve any key assigned to a case by calling `get()`: +The above enum defines 3 meta for each case: `color`, `shape` and `isOdd`. The `#[Meta]` attributes are ideal to declare static information, whilst public non-static methods are ideal to declare dynamic information. + +`#[Meta]` attributes can also be attached to the enum itself to provide default values when a case does not declare its own meta values: ```php -PureEnum::one->get('name'); // 'one' -PureEnum::one->get('value'); // throws ValueError as it is a pure enum -PureEnum::one->get('color'); // 'red' -PureEnum::one->get(fn (PureEnum $caseOne) => $caseOne->isOdd()); // true - -BackedEnum::one->get('name'); // 'one' -BackedEnum::one->get('value'); // 1 -BackedEnum::one->get('color'); // 'red' -BackedEnum::one->get(fn (BackedEnum $caseOne) => $caseOne->isOdd()); // true -``` +#[Meta(color: 'red', shape: 'triangle')] +enum BackedEnum: int +{ + use Enumerates; -At first glance this method may seem an overkill as "keys" can be accessed directly by cases like this: + case One = 1; -```php -BackedEnum::one->name; // 'one' -BackedEnum::one->value; // 1 -BackedEnum::one->color(); // 'red' -BackedEnum::one->isOdd(); // true + #[Meta(color: 'green', shape: 'square')] + case Two = 2; + + case Three = 3; +} ``` -However `get()` is useful to resolve keys dynamically as a key may be a property, a method or a closure. It often gets called internally for more advanced functionalities that we are going to explore very soon. +In the above example all cases have a `red` color and a `triangle` shape, except the case `Two` that overrides the default meta values. +Meta can also be leveraged for the [hydration](#-hydration), [elaboration](#-enum-operations) and [collection](#-cases-collection) of cases. -### Hydration -An enum case can be instantiated from its own name, value (if backed) and [keys](#keys-resolution): +### ๐Ÿšฐ Hydration + +An enum case can be instantiated from its own name, value (if backed) or [meta](#%EF%B8%8F-meta): ```php -PureEnum::from('one'); // PureEnum::one +PureEnum::from('One'); // PureEnum::One PureEnum::from('four'); // throws ValueError -PureEnum::tryFrom('one'); // PureEnum::one +PureEnum::tryFrom('One'); // PureEnum::One PureEnum::tryFrom('four'); // null -PureEnum::fromName('one'); // PureEnum::one +PureEnum::fromName('One'); // PureEnum::One PureEnum::fromName('four'); // throws ValueError -PureEnum::tryFromName('one'); // PureEnum::one +PureEnum::tryFromName('One'); // PureEnum::One PureEnum::tryFromName('four'); // null -PureEnum::fromKey('name', 'one'); // CasesCollection -PureEnum::fromKey('value', 1); // throws ValueError -PureEnum::fromKey('color', 'red'); // CasesCollection -PureEnum::fromKey(fn (PureEnum $case) => $case->isOdd(), true); // CasesCollection -PureEnum::tryFromKey('name', 'one'); // CasesCollection -PureEnum::tryFromKey('value', 1); // null -PureEnum::tryFromKey('color', 'red'); // CasesCollection -PureEnum::tryFromKey(fn (PureEnum $case) => $case->isOdd(), true); // CasesCollection - -BackedEnum::from(1); // BackedEnum::one +PureEnum::fromMeta('color', 'red'); // CasesCollection[PureEnum::One] +PureEnum::fromMeta('color', 'purple'); // throws ValueError +PureEnum::fromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] +PureEnum::fromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[PureEnum::One, PureEnum::Three] +PureEnum::tryFromMeta('color', 'red'); // CasesCollection[PureEnum::One] +PureEnum::tryFromMeta('color', 'purple'); // null +PureEnum::tryFromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] +PureEnum::tryFromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[PureEnum::One, PureEnum::Three] + +BackedEnum::from(1); // BackedEnum::One BackedEnum::from('1'); // throws ValueError -BackedEnum::tryFrom(1); // BackedEnum::one +BackedEnum::tryFrom(1); // BackedEnum::One BackedEnum::tryFrom('1'); // null -BackedEnum::fromName('one'); // BackedEnum::one +BackedEnum::fromName('One'); // BackedEnum::One BackedEnum::fromName('four'); // throws ValueError -BackedEnum::tryFromName('one'); // BackedEnum::one +BackedEnum::tryFromName('One'); // BackedEnum::One BackedEnum::tryFromName('four'); // null -BackedEnum::fromKey('name', 'one'); // CasesCollection -BackedEnum::fromKey('value', 1); // CasesCollection -BackedEnum::fromKey('color', 'red'); // CasesCollection -BackedEnum::fromKey(fn (BackedEnum $case) => $case->isOdd(), true); // CasesCollection -BackedEnum::tryFromKey('name', 'one'); // CasesCollection -BackedEnum::tryFromKey('value', 1); // CasesCollection -BackedEnum::tryFromKey('color', 'red'); // CasesCollection -BackedEnum::tryFromKey(fn (BackedEnum $case) => $case->isOdd(), true); // CasesCollection +BackedEnum::fromMeta('color', 'red'); // CasesCollection[BackedEnum::One] +BackedEnum::fromMeta('color', 'purple'); // throws ValueError +BackedEnum::fromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] +BackedEnum::fromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[BackedEnum::One, BackedEnum::Three] +BackedEnum::tryFromMeta('color', 'red'); // CasesCollection[BackedEnum::One] +BackedEnum::tryFromMeta('color', 'purple'); // null +BackedEnum::tryFromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] +BackedEnum::tryFromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[BackedEnum::One, BackedEnum::Three] ``` -While pure enums try to hydrate cases from names, backed enums can hydrate from both names and values. Even keys can be used to hydrate cases, cases are then wrapped into a [`CasesCollection`](#cases-collection) to allow further processing. +Hydrating from meta can return multiple cases. To facilitate further processing, such cases are [collected into a `CasesCollection`](#-cases-collection). -### Elaborating cases +### ๐ŸŽฒ Enum operations -There is a bunch of operations that can be performed on the cases of an enum. If the result of an operation is a plain list of cases, they get wrapped into a [`CasesCollection`](#cases-collection) for additional elaboration, otherwise the final result of the operation is returned: +A number of operations can be performed against an enum to affect all its cases: ```php -PureEnum::collect(); // CasesCollection +PureEnum::collect(); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] PureEnum::count(); // 3 -PureEnum::casesByName(); // ['one' => PureEnum::one, 'two' => PureEnum::two, 'three' => PureEnum::three] -PureEnum::casesByValue(); // [] -PureEnum::casesBy('color'); // ['red' => PureEnum::one, 'green' => PureEnum::two, 'blue' => PureEnum::three] -PureEnum::groupBy('color'); // ['red' => [PureEnum::one], 'green' => [PureEnum::two], 'blue' => [PureEnum::three]] -PureEnum::names(); // ['one', 'two', 'three'] +PureEnum::first(); // PureEnum::One +PureEnum::first(fn(PureEnum $case, int $key) => ! $case->isOdd()); // PureEnum::Two +PureEnum::names(); // ['One', 'Two', 'Three'] PureEnum::values(); // [] -PureEnum::pluck(); // ['one', 'two', 'three'] +PureEnum::pluck('name'); // ['One', 'Two', 'Three'] PureEnum::pluck('color'); // ['red', 'green', 'blue'] -PureEnum::pluck(fn (PureEnum $case) => $case->isOdd()); // [true, false, true] +PureEnum::pluck(fn(PureEnum $case) => $case->isOdd()); // [true, false, true] PureEnum::pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] -PureEnum::pluck(fn (PureEnum $case) => $case->isOdd(), fn (PureEnum $case) => $case->name); // ['one' => true, 'two' => false, 'three' => true] -PureEnum::filter('isOdd'); // CasesCollection -PureEnum::filter(fn (PureEnum $case) => $case->isOdd()); // CasesCollection -PureEnum::only('two', 'three'); // CasesCollection -PureEnum::except('two', 'three'); // CasesCollection -PureEnum::onlyValues(2, 3); // CasesCollection<> -PureEnum::exceptValues(2, 3); // CasesCollection<> -PureEnum::sort(); // CasesCollection -PureEnum::sortDesc(); // CasesCollection -PureEnum::sortByValue(); // CasesCollection<> -PureEnum::sortDescByValue(); // CasesCollection<> -PureEnum::sortBy('color'); // CasesCollection -PureEnum::sortDescBy(fn (PureEnum $case) => $case->color()); // CasesCollection - -BackedEnum::collect(); // CasesCollection +PureEnum::pluck(fn(PureEnum $case) => $case->isOdd(), fn(PureEnum $case) => $case->name); // ['One' => true, 'Two' => false, 'Three' => true] +PureEnum::map(fn(PureEnum $case, int $key) => $case->name . $key); // ['One0', 'Two1', 'Three2'] +PureEnum::keyByName(); // CasesCollection['One' => PureEnum::One, 'Two' => PureEnum::Two, 'Three' => PureEnum::Three] +PureEnum::keyBy('color'); // CasesCollection['red' => PureEnum::One, 'green' => PureEnum::Two, 'blue' => PureEnum::Three] +PureEnum::keyByValue(); // CasesCollection[] +PureEnum::groupBy('color'); // CasesCollection['red' => CasesCollection[PureEnum::One], 'green' => CasesCollection[PureEnum::Two], 'blue' => CasesCollection[PureEnum::Three]] +PureEnum::filter('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] +PureEnum::filter(fn(PureEnum $case) => $case->isOdd()); // CasesCollection[PureEnum::One, PureEnum::Three] +PureEnum::only('Two', 'Three'); // CasesCollection[PureEnum::Two, PureEnum::Three] +PureEnum::except('Two', 'Three'); // CasesCollection[PureEnum::One] +PureEnum::onlyValues(2, 3); // CasesCollection[] +PureEnum::exceptValues(2, 3); // CasesCollection[] +PureEnum::sort(); // CasesCollection[PureEnum::One, PureEnum::Three, PureEnum::Two] +PureEnum::sortBy('color'); // CasesCollection[PureEnum::Three, PureEnum::Two, PureEnum::One] +PureEnum::sortByValue(); // CasesCollection[] +PureEnum::sortDesc(); // CasesCollection[PureEnum::Two, PureEnum::Three, PureEnum::One] +PureEnum::sortByDesc(fn(PureEnum $case) => $case->color()); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] +PureEnum::sortByDescValue(); // CasesCollection[] + +BackedEnum::collect(); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] BackedEnum::count(); // 3 -BackedEnum::casesByName(); // ['one' => BackedEnum::one, 'two' => BackedEnum::two, 'three' => BackedEnum::three] -BackedEnum::casesByValue(); // [1 => BackedEnum::one, 2 => BackedEnum::two, 3 => BackedEnum::three] -BackedEnum::casesBy('color'); // ['red' => BackedEnum::one, 'green' => BackedEnum::two, 'blue' => BackedEnum::three] -BackedEnum::groupBy('color'); // ['red' => [BackedEnum::one], 'green' => [BackedEnum::two], 'blue' => [BackedEnum::three]] -BackedEnum::names(); // ['one', 'two', 'three'] +BackedEnum::first(); // BackedEnum::One +BackedEnum::first(fn(BackedEnum $case, int $key) => ! $case->isOdd()); // BackedEnum::Two +BackedEnum::names(); // ['One', 'Two', 'Three'] BackedEnum::values(); // [1, 2, 3] -BackedEnum::pluck(); // [1, 2, 3] +BackedEnum::pluck('value'); // [1, 2, 3] BackedEnum::pluck('color'); // ['red', 'green', 'blue'] -BackedEnum::pluck(fn (BackedEnum $case) => $case->isOdd()); // [true, false, true] +BackedEnum::pluck(fn(BackedEnum $case) => $case->isOdd()); // [true, false, true] BackedEnum::pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] -BackedEnum::pluck(fn (BackedEnum $case) => $case->isOdd(), fn (BackedEnum $case) => $case->name); // ['one' => true, -BackedEnum::filter('isOdd'); // CasesCollection -BackedEnum::filter(fn (BackedEnum $case) => $case->isOdd()); // CasesCollection -BackedEnum::only('two', 'three'); // CasesCollection -BackedEnum::except('two', 'three'); // CasesCollection -BackedEnum::onlyValues(2, 3); // CasesCollection<> -BackedEnum::exceptValues(2, 3); // CasesCollection<>'two' => false, 'three' => true] -BackedEnum::sort(); // CasesCollection -BackedEnum::sortDesc(); // CasesCollection -BackedEnum::sortByValue(); // CasesCollection -BackedEnum::sortDescByValue(); // CasesCollection -BackedEnum::sortBy('color'); // CasesCollection -BackedEnum::sortDescBy(fn (BackedEnum $case) => $case->color()); // CasesCollection +BackedEnum::pluck(fn(BackedEnum $case) => $case->isOdd(), fn(BackedEnum $case) => $case->name); // ['One' => true, 'Two' => false, 'Three' => true] +BackedEnum::map(fn(BackedEnum $case, int $key) => $case->name . $key); // ['One0', 'Two1', 'Three2'] +BackedEnum::keyByName(); // CasesCollection['One' => BackedEnum::One, 'Two' => BackedEnum::Two, 'Three' => BackedEnum::Three] +BackedEnum::keyBy('color'); // CasesCollection['red' => BackedEnum::One, 'green' => BackedEnum::Two, 'blue' => BackedEnum::Three] +BackedEnum::keyByValue(); // CasesCollection[1 => BackedEnum::One, 2 => BackedEnum::Two, 3 => BackedEnum::Three] +BackedEnum::groupBy('color'); // CasesCollection['red' => CasesCollection[BackedEnum::One], 'green' => CasesCollection[BackedEnum::Two], 'blue' => CasesCollection[BackedEnum::Three]] +BackedEnum::filter('isOdd'); // CasesCollection[BackedEnum::One, BackedEnum::Three] +BackedEnum::filter(fn(BackedEnum $case) => $case->isOdd()); // CasesCollection[BackedEnum::One, BackedEnum::Three] +BackedEnum::only('Two', 'Three'); // CasesCollection[BackedEnum::Two, BackedEnum::Three] +BackedEnum::except('Two', 'Three'); // CasesCollection[BackedEnum::One] +BackedEnum::onlyValues(2, 3); // CasesCollection[] +BackedEnum::exceptValues(2, 3); // CasesCollection['Two' => false, 'Three' => true] +BackedEnum::sort(); // CasesCollection[BackedEnum::One, BackedEnum::Three, BackedEnum::Two] +BackedEnum::sortBy('color'); // CasesCollection[BackedEnum::Three, BackedEnum::Two, BackedEnum::One] +BackedEnum::sortByValue(); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] +BackedEnum::sortDesc(); // CasesCollection[BackedEnum::Two, BackedEnum::Three, BackedEnum::One] +BackedEnum::sortByDescValue(); // CasesCollection[BackedEnum::Three, BackedEnum::Two, BackedEnum::One] +BackedEnum::sortByDesc(fn(BackedEnum $case) => $case->color()); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] ``` -### Cases collection +### ๐Ÿงบ Cases collection -When a plain list of cases is returned by one of the [cases operations](#elaborating-cases), it gets wrapped into a `CasesCollection` which provides a fluent API to perform further operations on the set of cases: +When an [enum operation](#-enum-operations) can return multiple cases, they are collected into a `CasesCollection` which provides a fluent API to perform further operations on the set of cases: ```php -PureEnum::filter('isOdd')->sortBy('color')->pluck('color', 'name'); // ['three' => 'blue', 'one' => 'red'] +PureEnum::filter('isOdd')->sortBy('color')->pluck('color', 'name'); // ['Three' => 'blue', 'One' => 'red'] ``` -Cases can be collected by calling `collect()` or any other [cases operation](#elaborating-cases) returning a `CasesCollection`: +Cases can be collected by calling `collect()` or any other [enum operation](#-enum-operations) returning a `CasesCollection`: ```php -PureEnum::collect(); // CasesCollection +PureEnum::collect(); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] -BackedEnum::only('one', 'two'); // CasesCollection +BackedEnum::only('One', 'Two'); // CasesCollection[BackedEnum::One, BackedEnum::Two] ``` -We can iterate cases collections within any loop: +We can iterate a cases collection within any loop: ```php foreach (PureEnum::collect() as $case) { @@ -304,74 +287,123 @@ foreach (PureEnum::collect() as $case) { } ``` -Obtaining the underlying plain list of cases is easy: +All the [enum operations listed above](#-enum-operations) are also available when dealing with a collection of cases. + + +### ๐Ÿช„ Magic + +Enums can implement magic methods to be invoked or to handle calls to inaccessible methods. By default when calling an inaccessible static method, the name or value of the case matching the missing method is returned: + +```php +PureEnum::One(); // 'One' + +BackedEnum::One(); // 1 +``` + +To improve the autocompletion of our IDE, we can add some method annotations to our enums: + +```php +/** + * @method static int One() + * @method static int Two() + * @method static int Three() + */ +enum BackedEnum: int +{ + use Enumerates; + + case One = 1; + case Two = 2; + case Three = 3; +} +``` + +By default, we can also obtain the name or value of a case by simply invoking it: + +```php +$case = PureEnum::One; +$case(); // 'One' + +$case = BackedEnum::One; +$case(); // 1 +``` + +When calling an inaccessible method of a case, by default the value of the meta matching the missing method is returned: + +```php +PureEnum::One->color(); // 'red' + +BackedEnum::One->shape(); // 'triangle' +``` + +To improve the autocompletion of our IDE, we can add some method annotations to our enums: ```php -PureEnum::collect()->cases(); // [PureEnum::one, PureEnum::two, PureEnum::three] +/** + * @method string color() + */ +enum BackedEnum: int +{ + use Enumerates; + + #[Meta(color: 'red')] + case One = 1; + + #[Meta(color: 'green')] + case Two = 2; + + #[Meta(color: 'blue')] + case Three = 3; +} ``` -Sometimes we may need to extract only the first case of the collection: +Depending on our needs, we can customize the default behavior of all enums in our application when invoking a case or calling inaccessible methods: ```php -PureEnum::filter(fn (PureEnum $case) => !$case->isOdd())->first(); // PureEnum::two +use Cerbero\Enum\Enums; + +// define the logic to run when calling an inaccessible method of an enum +Enums::onStaticCall(function(string $enum, string $name, array $arguments) { + // $enum is the fully qualified name of the enum that called the inaccessible method + // $name is the inaccessible method name + // $arguments are the parameters passed to the inaccessible method +}); + +// define the logic to run when calling an inaccessible method of a case +Enums::onCall(function(object $case, string $name, array $arguments) { + // $case is the instance of the case that called the inaccessible method + // $name is the inaccessible method name + // $arguments are the parameters passed to the inaccessible method +}); + +// define the logic to run when invoking a case +Enums::onInvoke(function(object $case, mixed ...$arguments) { + // $case is the instance of the case that is being invoked + // $arguments are the parameters passed when invoking the case +}); ``` -For reference, here are all the operations available in `CasesCollection`: + +### ๐Ÿคณ Self-awareness + +Finally, the following methods can be useful for inspecting enums or auto-generating code: ```php -PureEnum::collect()->cases(); // [PureEnum::one, PureEnum::two, PureEnum::three] -PureEnum::collect()->count(); // 3 -PureEnum::collect()->first(); // PureEnum::one -PureEnum::collect()->keyByName(); // ['one' => PureEnum::one, 'two' => PureEnum::two, 'three' => PureEnum::three] -PureEnum::collect()->keyByValue(); // [] -PureEnum::collect()->keyBy('color'); // ['red' => PureEnum::one, 'green' => PureEnum::two, 'blue' => PureEnum::three] -PureEnum::collect()->groupBy('color'); // ['red' => [PureEnum::one], 'green' => [PureEnum::two], 'blue' => [PureEnum::three]] -PureEnum::collect()->names(); // ['one', 'two', 'three'] -PureEnum::collect()->values(); // [] -PureEnum::collect()->pluck(); // ['one', 'two', 'three'] -PureEnum::collect()->pluck('color'); // ['red', 'green', 'blue'] -PureEnum::collect()->pluck(fn (PureEnum $case) => $case->isOdd()); // [true, false, true] -PureEnum::collect()->pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] -PureEnum::collect()->pluck(fn (PureEnum $case) => $case->isOdd(), fn (PureEnum $case) => $case->name); // ['one' => true, 'two' => false, 'three' => true] -PureEnum::collect()->filter('isOdd'); // CasesCollection -PureEnum::collect()->filter(fn (PureEnum $case) => $case->isOdd()); // CasesCollection -PureEnum::collect()->only('two', 'three'); // CasesCollection -PureEnum::collect()->except('two', 'three'); // CasesCollection -PureEnum::collect()->onlyValues(2, 3); // CasesCollection<> -PureEnum::collect()->exceptValues(2, 3); // CasesCollection<> -PureEnum::collect()->sort(); // CasesCollection -PureEnum::collect()->sortDesc(); // CasesCollection -PureEnum::collect()->sortByValue(); // CasesCollection<> -PureEnum::collect()->sortDescByValue(); // CasesCollection<> -PureEnum::collect()->sortBy('color'); // CasesCollection -PureEnum::collect()->sortDescBy(fn (PureEnum $case) => $case->color()); // CasesCollection - -BackedEnum::collect()->cases(); // [BackedEnum::one, BackedEnum::two, BackedEnum::three] -BackedEnum::collect()->count(); // 3 -BackedEnum::collect()->first(); // BackedEnum::one -BackedEnum::collect()->keyByName(); // ['one' => BackedEnum::one, 'two' => BackedEnum::two, 'three' => BackedEnum::three] -BackedEnum::collect()->keyByValue(); // [1 => BackedEnum::one, 2 => BackedEnum::two, 3 => BackedEnum::three] -BackedEnum::collect()->keyBy('color'); // ['red' => BackedEnum::one, 'green' => BackedEnum::two, 'blue' => BackedEnum::three] -BackedEnum::collect()->groupBy('color'); // ['red' => [BackedEnum::one], 'green' => [BackedEnum::two], 'blue' => [BackedEnum::three]] -BackedEnum::collect()->names(); // ['one', 'two', 'three'] -BackedEnum::collect()->values(); // [1, 2, 3] -BackedEnum::collect()->pluck(); // [1, 2, 3] -BackedEnum::collect()->pluck('color'); // ['red', 'green', 'blue'] -BackedEnum::collect()->pluck(fn (BackedEnum $case) => $case->isOdd()); // [true, false, true] -BackedEnum::collect()->pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] -BackedEnum::collect()->pluck(fn (BackedEnum $case) => $case->isOdd(), fn (BackedEnum $case) => $case->name); // ['one' => true, 'two' => false, 'three' => true] -BackedEnum::collect()->filter('isOdd'); // CasesCollection -BackedEnum::collect()->filter(fn (BackedEnum $case) => $case->isOdd()); // CasesCollection -BackedEnum::collect()->only('two', 'three'); // CasesCollection -BackedEnum::collect()->except('two', 'three'); // CasesCollection -BackedEnum::collect()->onlyValues(2, 3); // CasesCollection -BackedEnum::collect()->exceptValues(2, 3); // CasesCollection -BackedEnum::collect()->sort(); // CasesCollection -BackedEnum::collect()->sortDesc(); // CasesCollection -BackedEnum::collect()->sortByValue(); // CasesCollection -BackedEnum::collect()->sortDescByValue(); // CasesCollection -BackedEnum::collect()->sortBy('color'); // CasesCollection -BackedEnum::collect()->sortDescBy(fn (BackedEnum $case) => $case->color()); // CasesCollection +PureEnum::isPure(); // true +PureEnum::isBacked(); // false +PureEnum::metaNames(); // ['color', 'shape', 'isOdd'] +PureEnum::One->resolveItem('name'); // 'One' +PureEnum::One->resolveMeta('isOdd'); // true +PureEnum::One->resolveMetaAttribute('color'); // 'red' +PureEnum::One->value(); // 'One' + +BackedEnum::isPure(); // false +BackedEnum::isBacked(); // true +BackedEnum::metaNames(); // ['color', 'shape', 'isOdd'] +BackedEnum::One->resolveItem('value'); // 1 +BackedEnum::One->resolveMeta('isOdd'); // true +BackedEnum::One->resolveMetaAttribute('color'); // 'red' +BackedEnum::One->value(); // 1 ``` ## ๐Ÿ“† Change log @@ -404,19 +436,21 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio [ico-author]: https://img.shields.io/static/v1?label=author&message=cerbero90&color=50ABF1&logo=twitter&style=flat-square [ico-php]: https://img.shields.io/packagist/php-v/cerbero/enum?color=%234F5B93&logo=php&style=flat-square [ico-version]: https://img.shields.io/packagist/v/cerbero/enum.svg?label=version&style=flat-square -[ico-actions]: https://img.shields.io/github/workflow/status/cerbero90/enum/build?style=flat-square&logo=github +[ico-actions]: https://img.shields.io/github/actions/workflow/status/cerbero90/enum/build.yml?branch=master&style=flat-square&logo=github [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square -[ico-psr12]: https://img.shields.io/static/v1?label=compliance&message=PSR-12&color=blue&style=flat-square +[ico-per]: https://img.shields.io/static/v1?label=compliance&message=PER&color=blue&style=flat-square [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/cerbero90/enum.svg?style=flat-square&logo=scrutinizer [ico-code-quality]: https://img.shields.io/scrutinizer/g/cerbero90/enum.svg?style=flat-square&logo=scrutinizer +[ico-phpstan]: https://img.shields.io/badge/level-max-success?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGb0lEQVR42u1Xe1BUZRS/y4Kg8oiR3FCCBUySESZBRCiaBnmEsOzeSzsg+KxYYO9dEEftNRqZjx40FRZkTpqmOz5S2LsXlEZBciatkQnHDGYaGdFy1EpGMHl/p/PdFlt2rk5O+J9n5nA/vtf5ned3lnlISpRhafBlLRLHCtJGVrB/ZBDsaw2lUqzReGAC46DstTYfnSCGUjaaDvgxACo6j3vUenNdImeRXqdnWV5az5rrnzeZznj8J+E5Ftsclhf3s4J4CS/oRx5Bvon8ZU65FGYQxAwcf85a7CeRz+C41THejueydCZ7AAK34nwv3kHP/oUKdOL4K7258fF7Cud427O48RQeGkIGJ77N8fZqlrcfRP4d/x90WQfHXLeBt9dTrSlwl3V65ynWLM1SEA2qbNQckbe4Xmww10Hmy3shid0CMcmlEJtSDsl5VZBdfAgMvI3uuR+moJqN6LaxmpsOBeLCDmTifCB92RcQmbAUJvtqALc5sQr8p86gYBCcFdBq9wOin7NQax6ewlB6rqLZHf23FP10y3lj6uJtEBg2HxiVCtzd3SEwMBCio6Nh9uzZ4O/vLwOZ4OUNM2NyIGPFrvuzBG//lRPs+VQ2k1ki+ePkd84bskz7YFpYgizEz88P8vPzYffu3dDS0gJNTU1QXV0NqampRK1WIwgfiE4qhOyig0rC+pCvK8QUoML7uJVHA5kcQUp3DSpqWjc3d/Dy8oKioiLo6uqCoaEhuHb1KvT09AAhBFpbW4lOpyMyyIBQSCmoUQLQzgniNvz+obB2HS2RwBgE6dOxCyJogmNkP2u1Wrhw4QJ03+iGrR9XEd3CTNBn6eCbo40wPDwMdXV1BF1DVG5qiEtboxSUP6J71+D3NwUAhLOIRQzm7lnnhYUv7QFv/yDZ/Lm5ubK2DVI9iZ8bR8JDtEB57lNzENQN6OjoIGlpabIVZsYaMTO+hrikRRA1JxmSX9hE7/sJtVyF38tKsUCVZxBhz9jI3wGT/QJlADzPAyXrnj0kInzGHQCRMyOg/ed2uHjxIuE4TgYQHq2DLJqumashY+lnsMC4GVC5do6XVuK9l+4SkN8y+GfYeVJn2g++U7QygPT0dBgYGIDvT58mnF5PQcjC83PzSF9fH7S1tZGEhAQZQOT8JaA317oIkM6jS8uVLSDzOQqg23Uh+MlkOf00Gg0cP34c+vv74URzM9n41gby/rvvkc7OThlATU3NCGYJUXt4QaLuTYwBcTSOBmj1RD7D4Tsix4ByOjZRF/zgupDEbgZ3j4ly/qekpND0o5aQ44HS4OAgsVqtI1gTZO01IbG0aP1bknnxCDUvArHi+B0lJSlzglTFYO2udF3Ql9TCrHn5oEIreHp6QlRUFJSUlJCqqipSWVlJ8vLyCGYIFS7HS3zGa87mv4lcjLwLlStlLTKYYUUAlvrlDGcW45wKxXX6aqHZNutM+1oQBHFTewAKkoH4+vqCj48PYAGS5yb5amjNoO+CU2SL53NKpDD0vxHHmOJir7L5xUvZgm0us2R142ScOIyVqYvlpWU4XoHIP8DXL2b+wjdWeXh6U2FjmIIKmbWAYPFRMus62h/geIvjOQYlpuDysQrLL6Ger49HgW8jqvXUhI7UvDb9iaSTDqHtyItiF5Suw5ewF/Nd8VJ6zlhsn06bEhwX4NyfCvuGEeRpTmh4mkG68yDpyuzB9EUcjU5awbAgncPlAeSdAQER0zCndzqVbeXC4qDsMpvGEYBXRnsDx4N3Auf1FCTjTIaVtY/QTmd0I8bBVm1kejEubUfO01vqImn3c49X7qpeqI9inIgtbpxK3YrKfIJCt+OeV2nfUVFR4ca4EkVENyA7gkYcMfB1R5MMmxZ7ez/2KF5SSN1yV+158UPsJT0ZBcI2bRLtIXGoYu5FerOUiJe1OfsL3XEWH43l2KS+iJF9+S4FpcNgsc+j8cT8H4o1bfPg/qkLt50uJ1RzdMsGg0UqwfEN114Pwb1CtWTGg+Y9U5ClK9x7xUWI7BI5VQVp0AVcQ3bZkQhmnEgdHhKyNSZe16crtBIlc7sIb6cRLft2PCgoKGjijBDtjrAQ7a3EdMsxzIRflAFIhPb6mHYmYwX+WBlPQgskhgVryyJCQyNyBLsBQdQ6fgsQhyt6MSOOsWZ7gbH8wETmgRKAijatNL8Ngm0xx4tLcsps0Wzx4al0jXlI40B/A3pa144MDtSgAAAAAElFTkSuQmCC [ico-downloads]: https://img.shields.io/packagist/dt/cerbero/enum.svg?style=flat-square [link-author]: https://twitter.com/cerbero90 [link-php]: https://www.php.net [link-packagist]: https://packagist.org/packages/cerbero/enum [link-actions]: https://github.com/cerbero90/enum/actions?query=workflow%3Abuild -[link-psr12]: https://www.php-fig.org/psr/psr-12/ +[link-per]: https://www.php-fig.org/per/coding-style/ [link-scrutinizer]: https://scrutinizer-ci.com/g/cerbero90/enum/code-structure [link-code-quality]: https://scrutinizer-ci.com/g/cerbero90/enum [link-downloads]: https://packagist.org/packages/cerbero/enum +[link-phpstan]: https://phpstan.org/ [link-contributors]: ../../contributors diff --git a/composer.json b/composer.json index 4d39d58..bac7027 100644 --- a/composer.json +++ b/composer.json @@ -18,9 +18,11 @@ "php": "^8.1" }, "require-dev": { - "pestphp/pest": "^1.21", + "pestphp/pest": "^2.0", + "phpstan/phpstan": "^1.9", "scrutinizer/ocular": "^1.9", - "squizlabs/php_codesniffer": "^3.0" + "squizlabs/php_codesniffer": "^3.0", + "tightenco/duster": "^2.0" }, "autoload": { "psr-4": { @@ -33,9 +35,9 @@ } }, "scripts": { - "test": "pest", - "check-style": "phpcs --standard=PSR12 src", - "fix-style": "phpcbf --standard=PSR12 src" + "fix": "duster fix -u tlint,phpcodesniffer,pint", + "lint": "duster lint -u tlint,phpcodesniffer,pint,phpstan", + "test": "pest" }, "extra": { "branch-alias": { diff --git a/duster.json b/duster.json new file mode 100644 index 0000000..e3834c7 --- /dev/null +++ b/duster.json @@ -0,0 +1,13 @@ +{ + "include": [ + "src" + ], + "exclude": [ + "tests" + ], + "scripts": { + "lint": { + "phpstan": ["./vendor/bin/phpstan", "analyse"] + } + } +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1 @@ + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..5209c3e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: max + paths: + - src +includes: + - phpstan-baseline.neon diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 917e093..990c7b1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,28 +1,23 @@ - - - - tests - - - - - src/ - - - - - - - - + + + + + + + + + + + tests + + + + + + + + src/ + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..03e092d --- /dev/null +++ b/pint.json @@ -0,0 +1,19 @@ +{ + "preset": "per", + "rules": { + "align_multiline_comment": true, + "combine_consecutive_issets": true, + "combine_consecutive_unsets": true, + "concat_space": {"spacing": "one"}, + "explicit_string_variable": true, + "ordered_imports": { + "sort_algorithm": "alpha", + "imports_order": [ + "class", + "function", + "const" + ] + }, + "simple_to_complex_string_variable": true + } +} diff --git a/src/Attributes/Meta.php b/src/Attributes/Meta.php new file mode 100644 index 0000000..c9443a7 --- /dev/null +++ b/src/Attributes/Meta.php @@ -0,0 +1,62 @@ + + */ + protected array $all; + + /** + * Instantiate the class. + */ + public function __construct(mixed ...$meta) + { + foreach ($meta as $key => $value) { + if (! is_string($key)) { + throw new InvalidArgumentException('The name of meta must be a string'); + } + + $this->all[$key] = $value; + } + } + + /** + * Retrieve the meta names. + * + * @return string[] + */ + public function names(): array + { + return array_keys($this->all); + } + + /** + * Determine whether the given meta exists. + */ + public function has(string $meta): bool + { + return array_key_exists($meta, $this->all); + } + + /** + * Retrieve the value for the given meta. + */ + public function get(string $meta): mixed + { + return $this->all[$meta] ?? null; + } +} diff --git a/src/CasesCollection.php b/src/CasesCollection.php index 452f76e..c50fbea 100644 --- a/src/CasesCollection.php +++ b/src/CasesCollection.php @@ -6,244 +6,254 @@ use Countable; use IteratorAggregate; use Traversable; -use UnitEnum; /** * The collection of enum cases. * + * @template TKey of array-key + * @template TValue + * + * @implements IteratorAggregate */ class CasesCollection implements Countable, IteratorAggregate { /** - * Whether the cases belong to a backed enum - * - * @var bool + * Whether the cases belong to a backed enum. */ - protected bool $enumIsBacked; + protected readonly bool $enumIsBacked; /** - * Instantiate the class + * Instantiate the class. * - * @param array $cases + * @param array $cases */ - public function __construct(protected array $cases) + final public function __construct(protected array $cases) { - $this->enumIsBacked = $this->first() instanceof BackedEnum; + $this->enumIsBacked = reset($cases) instanceof BackedEnum; } /** - * Retrieve the iterable cases + * Retrieve the count of cases. + */ + public function count(): int + { + return count($this->cases); + } + + /** + * Retrieve the iterable cases. * - * @return Traversable + * @return Traversable */ public function getIterator(): Traversable { - return (function () { - foreach ($this->cases as $case) { - yield $case; - } - })(); + yield from $this->cases; } /** - * Retrieve the cases + * Retrieve all the cases as a plain array. * - * @return array + * @return array */ - public function cases(): array + public function all(): array { return $this->cases; } /** - * Retrieve the count of cases + * Retrieve all the cases as a plain array recursively. * - * @return int + * @return array */ - public function count(): int + public function toArray(): array { - return count($this->cases); + $array = []; + + foreach ($this->cases as $key => $value) { + $array[$key] = $value instanceof self ? $value->toArray() : $value; + } + + return $array; } /** - * Retrieve the first case + * Retrieve the first case. * - * @param callable|null $callback - * @param mixed $default - * @return mixed + * @param (callable(TValue, TKey): bool)|null $callback + * @return ?TValue */ - public function first(callable $callback = null, mixed $default = null): mixed + public function first(callable $callback = null): mixed { - $callback ??= fn () => true; + $callback ??= fn() => true; - foreach ($this->cases as $case) { - if ($callback($case)) { + foreach ($this->cases as $key => $case) { + if ($callback($case, $key)) { return $case; } } - return $default; + return null; } /** - * Retrieve the cases keyed by name + * Retrieve all the names of the cases. * - * @return array + * @return string[] */ - public function keyByName(): array + public function names(): array { - return $this->keyBy('name'); + return array_column($this->cases, 'name'); + } + + /** + * Retrieve all the values of the backed cases. + * + * @return list + */ + public function values(): array + { + return array_column($this->cases, 'value'); } /** - * Retrieve the cases keyed by the given key + * Retrieve an array of values optionally keyed by the given key. + * + * @template TPluckValue * - * @param callable|string $key - * @return array + * @param (callable(TValue): TPluckValue)|string $value + * @param (callable(TValue): array-key)|string|null $key + * @return array */ - public function keyBy(callable|string $key): array + public function pluck(callable|string $value, callable|string $key = null): array { $result = []; foreach ($this->cases as $case) { - $result[$case->get($key)] = $case; + if ($key === null) { + $result[] = $case->resolveItem($value); + } else { + $result[$case->resolveItem($key)] = $case->resolveItem($value); + } } return $result; } /** - * Retrieve the cases keyed by value + * Retrieve the result of mapping over the cases. * - * @return array + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return array */ - public function keyByValue(): array + public function map(callable $callback): array { - return $this->enumIsBacked ? $this->keyBy('value') : []; + $keys = array_keys($this->cases); + $values = array_map($callback, $this->cases, $keys); + + return array_combine($keys, $values); } /** - * Retrieve the cases grouped by the given key - * - * @param callable|string $key - * @return array + * Retrieve the cases keyed by their own name. */ - public function groupBy(callable|string $key): array + public function keyByName(): static { - $result = []; - - foreach ($this->cases as $case) { - $result[$case->get($key)][] = $case; - } - - return $result; + return $this->keyBy('name'); } /** - * Retrieve all the names of the cases + * Retrieve the cases keyed by the given key. * - * @return array + * @param (callable(TValue): array-key)|string $key */ - public function names(): array + public function keyBy(callable|string $key): static { - return array_column($this->cases, 'name'); + $keyed = []; + + foreach ($this->cases as $case) { + $keyed[$case->resolveItem($key)] = $case; + } + + return new static($keyed); } /** - * Retrieve all the values of the backed cases - * - * @return array + * Retrieve the cases keyed by their own value. */ - public function values(): array + public function keyByValue(): static { - return array_column($this->cases, 'value'); + return $this->enumIsBacked ? $this->keyBy('value') : new static([]); } /** - * Retrieve an array of values optionally keyed by the given key + * Retrieve the cases grouped by the given key. * - * @param callable|string|null $value - * @param callable|string|null $key - * @return array + * @param (callable(TValue): array-key)|string $key */ - public function pluck(callable|string $value = null, callable|string $key = null): array + public function groupBy(callable|string $key): static { - $result = []; - $value ??= $this->enumIsBacked ? 'value' : 'name'; + $grouped = []; foreach ($this->cases as $case) { - if ($key === null) { - $result[] = $case->get($value); - } else { - $result[$case->get($key)] = $case->get($value); - } + $grouped[$case->resolveItem($key)][] = $case; } - return $result; + foreach ($grouped as $key => $cases) { + $grouped[$key] = new static($cases); + } + + return new static($grouped); } /** - * Retrieve a collection with the filtered cases + * Retrieve a new collection with the filtered cases. * - * @param callable|string $filter - * @return static + * @param (callable(TValue): bool)|string $filter */ public function filter(callable|string $filter): static { - $callback = is_callable($filter) ? $filter : fn (mixed $case) => $case->get($filter) === true; - $cases = array_filter($this->cases, $callback); + /** @phpstan-ignore method.nonObject */ + $callback = is_callable($filter) ? $filter : fn(mixed $case) => $case->resolveItem($filter) === true; - return new static(array_values($cases)); + return new static(array_filter($this->cases, $callback)); } /** - * Retrieve a collection of cases having the given names - * - * @param string ...$name - * @return static + * Retrieve a new collection of cases having only the given names. */ public function only(string ...$name): static { - return $this->filter(fn (UnitEnum $case) => in_array($case->name, $name)); + return $this->filter(fn(mixed $case) => in_array($case->name, $name)); } /** - * Retrieve a collection of cases not having the given names - * - * @param string ...$name - * @return static + * Retrieve a collection of cases not having the given names. */ public function except(string ...$name): static { - return $this->filter(fn (UnitEnum $case) => !in_array($case->name, $name)); + return $this->filter(fn(mixed $case) => !in_array($case->name, $name)); } /** - * Retrieve a collection of backed cases having the given values - * - * @param string|int ...$value - * @return static + * Retrieve a new collection of backed cases having only the given values. */ public function onlyValues(string|int ...$value): static { - return $this->filter(fn (UnitEnum $case) => $this->enumIsBacked && in_array($case->value, $value, true)); + return $this->filter(fn(mixed $case) => $this->enumIsBacked && in_array($case->value, $value, true)); } /** - * Retrieve a collection of backed cases not having the given values - * - * @param string|int ...$value - * @return static + * Retrieve a new collection of backed cases not having the given values. */ public function exceptValues(string|int ...$value): static { - return $this->filter(fn (UnitEnum $case) => $this->enumIsBacked && !in_array($case->value, $value, true)); + return $this->filter(fn(mixed $case) => $this->enumIsBacked && !in_array($case->value, $value, true)); } /** - * Retrieve a collection of cases sorted by name ascending - * - * @return static + * Retrieve a new collection of cases sorted by their own name ascending. */ public function sort(): static { @@ -251,62 +261,54 @@ public function sort(): static } /** - * Retrieve a collection of cases sorted by name descending + * Retrieve a new collection of cases sorted by the given key ascending. * - * @return static - */ - public function sortDesc(): static - { - return $this->sortDescBy('name'); - } - - /** - * Retrieve a collection of cases sorted by the given key ascending - * - * @param callable|string $key - * @return static + * @param (callable(TValue): mixed)|string $key */ public function sortBy(callable|string $key): static { $cases = $this->cases; - usort($cases, fn ($a, $b) => $a->get($key) <=> $b->get($key)); + uasort($cases, fn(mixed $a, mixed $b) => $a->resolveItem($key) <=> $b->resolveItem($key)); return new static($cases); } /** - * Retrieve a collection of cases sorted by the given key descending - * - * @param callable|string $key - * @return static + * Retrieve a new collection of cases sorted by their own value ascending. */ - public function sortDescBy(callable|string $key): static + public function sortByValue(): static { - $cases = $this->cases; - - usort($cases, fn ($a, $b) => $a->get($key) > $b->get($key) ? -1 : 1); + return $this->enumIsBacked ? $this->sortBy('value') : new static([]); + } - return new static($cases); + /** + * Retrieve a new collection of cases sorted by their own name descending. + */ + public function sortDesc(): static + { + return $this->sortByDesc('name'); } /** - * Retrieve a collection of cases sorted by value ascending + * Retrieve a new collection of cases sorted by the given key descending. * - * @return static + * @param (callable(TValue): mixed)|string $key */ - public function sortByValue(): static + public function sortByDesc(callable|string $key): static { - return $this->enumIsBacked ? $this->sortBy('value') : new static([]); + $cases = $this->cases; + + uasort($cases, fn(mixed $a, mixed $b) => $b->resolveItem($key) <=> $a->resolveItem($key)); + + return new static($cases); } /** - * Retrieve a collection of cases sorted by value descending - * - * @return static + * Retrieve a new collection of cases sorted by their own value descending. */ - public function sortDescByValue(): static + public function sortByDescValue(): static { - return $this->enumIsBacked ? $this->sortDescBy('value') : new static([]); + return $this->enumIsBacked ? $this->sortByDesc('value') : new static([]); } } diff --git a/src/Concerns/CollectsCases.php b/src/Concerns/CollectsCases.php index de50bd0..2be87dd 100644 --- a/src/Concerns/CollectsCases.php +++ b/src/Concerns/CollectsCases.php @@ -5,219 +5,237 @@ use Cerbero\Enum\CasesCollection; /** - * The trait to collect cases of an enum. - * + * The trait to collect the cases of an enum. */ trait CollectsCases { /** - * Retrieve a collection with all the cases + * Retrieve a collection with all the cases. * - * @return CasesCollection + * @return CasesCollection */ public static function collect(): CasesCollection { - return new CasesCollection(static::cases()); + return new CasesCollection(self::cases()); } /** - * Retrieve the count of cases - * - * @return int + * Retrieve the count of cases. */ public static function count(): int { - return static::collect()->count(); + return self::collect()->count(); } /** - * Retrieve all cases keyed by name + * Retrieve the first case. * - * @return array + * @param (callable(self, array-key): bool)|null $callback */ - public static function casesByName(): array + public static function first(callable $callback = null): ?self { - return static::collect()->keyByName(); + return self::collect()->first($callback); } /** - * Retrieve all cases keyed by value + * Retrieve the name of all the cases. * - * @return array + * @return string[] */ - public static function casesByValue(): array + public static function names(): array { - return static::collect()->keyByValue(); + return self::collect()->names(); } /** - * Retrieve all cases keyed by the given key + * Retrieve the value of all the backed cases. * - * @param callable|string $key - * @return array + * @return list */ - public static function casesBy(callable|string $key): array + public static function values(): array { - return static::collect()->keyBy($key); + return self::collect()->values(); } /** - * Retrieve all cases grouped by the given key + * Retrieve an array of values optionally keyed by the given key. + * + * @template TPluckValue * - * @param callable|string $key - * @return array + * @param (callable(self): TPluckValue)|string $value + * @param (callable(self): array-key)|string|null $key + * @return array */ - public static function groupBy(callable|string $key): array + public static function pluck(callable|string $value, callable|string $key = null): array { - return static::collect()->groupBy($key); + return self::collect()->pluck($value, $key); } /** - * Retrieve all the names of the cases + * Retrieve the result of mapping over all the cases. * - * @return array + * @template TMapValue + * + * @param callable(self, array-key): TMapValue $callback + * @return array */ - public static function names(): array + public static function map(callable $callback): array { - return static::collect()->names(); + return self::collect()->map($callback); } /** - * Retrieve all the values of the backed cases + * Retrieve all the cases keyed by their own name. * - * @return array + * @return CasesCollection */ - public static function values(): array + public static function keyByName(): CasesCollection { - return static::collect()->values(); + return self::collect()->keyByName(); } /** - * Retrieve a collection with the filtered cases + * Retrieve all the cases keyed by the given key. * - * @param callable|string $filter - * @return CasesCollection + * @param (callable(self): array-key)|string $key + * @return CasesCollection */ - public static function filter(callable|string $filter): CasesCollection + public static function keyBy(callable|string $key): CasesCollection { - return static::collect()->filter($filter); + return self::collect()->keyBy($key); } /** - * Retrieve a collection of cases having the given names + * Retrieve all the cases keyed by their own value. * - * @param string ...$name - * @return CasesCollection + * @return CasesCollection */ - public static function only(string ...$name): CasesCollection + public static function keyByValue(): CasesCollection { - return static::collect()->only(...$name); + return self::collect()->keyByValue(); } /** - * Retrieve a collection of cases not having the given names + * Retrieve all the cases grouped by the given key. * - * @param string ...$name - * @return CasesCollection + * @param (callable(self): array-key)|string $key + * @return CasesCollection> */ - public static function except(string ...$name): CasesCollection + public static function groupBy(callable|string $key): CasesCollection { - return static::collect()->except(...$name); + return self::collect()->groupBy($key); } /** - * Retrieve a collection of backed cases having the given values + * Retrieve only the filtered cases. * - * @param string|int ...$value - * @return CasesCollection + * @param (callable(self): bool)|string $filter + * @return CasesCollection */ - public static function onlyValues(string|int ...$value): CasesCollection + public static function filter(callable|string $filter): CasesCollection { - return static::collect()->onlyValues(...$value); + return self::collect()->filter($filter); } /** - * Retrieve a collection of backed cases not having the given values + * Retrieve only the cases having the given names. * - * @param string|int ...$value - * @return CasesCollection + * @return CasesCollection */ - public static function exceptValues(string|int ...$value): CasesCollection + public static function only(string ...$names): CasesCollection { - return static::collect()->exceptValues(...$value); + return self::collect()->only(...$names); } /** - * Retrieve an array of values optionally keyed by the given key + * Retrieve only the cases not having the given names. * - * @param callable|string|null $value - * @param callable|string|null $key - * @return array + * @return CasesCollection */ - public static function pluck(callable|string $value = null, callable|string $key = null): array + public static function except(string ...$names): CasesCollection { - return static::collect()->pluck($value, $key); + return self::collect()->except(...$names); } /** - * Retrieve a collection of cases sorted by name ascending + * Retrieve only the cases having the given values. * - * @return CasesCollection + * @return CasesCollection + */ + public static function onlyValues(string|int ...$values): CasesCollection + { + return self::collect()->onlyValues(...$values); + } + + /** + * Retrieve only the cases not having the given values. + * + * @return CasesCollection + */ + public static function exceptValues(string|int ...$values): CasesCollection + { + return self::collect()->exceptValues(...$values); + } + + /** + * Retrieve all the cases sorted by their own name ascending. + * + * @return CasesCollection */ public static function sort(): CasesCollection { - return static::collect()->sort(); + return self::collect()->sort(); } /** - * Retrieve a collection of cases sorted by name descending + * Retrieve all the cases sorted by the given key ascending. * - * @return CasesCollection + * @param (callable(self): mixed)|string $key + * @return CasesCollection */ - public static function sortDesc(): CasesCollection + public static function sortBy(callable|string $key): CasesCollection { - return static::collect()->sortDesc(); + return self::collect()->sortBy($key); } /** - * Retrieve a collection of cases sorted by value ascending + * Retrieve all the cases sorted by their own value ascending. * - * @return CasesCollection + * @return CasesCollection */ public static function sortByValue(): CasesCollection { - return static::collect()->sortByValue(); + return self::collect()->sortByValue(); } /** - * Retrieve a collection of cases sorted by value descending + * Retrieve all the cases sorted by their own name descending. * - * @return CasesCollection + * @return CasesCollection */ - public static function sortDescByValue(): CasesCollection + public static function sortDesc(): CasesCollection { - return static::collect()->sortDescByValue(); + return self::collect()->sortDesc(); } /** - * Retrieve a collection of cases sorted by the given key ascending + * Retrieve all the cases sorted by the given key descending. * - * @param callable|string $key - * @return CasesCollection + * @param (callable(self): mixed)|string $key + * @return CasesCollection */ - public static function sortBy(callable|string $key): CasesCollection + public static function sortByDesc(callable|string $key): CasesCollection { - return static::collect()->sortBy($key); + return self::collect()->sortByDesc($key); } /** - * Retrieve a collection of cases sorted by the given key descending + * Retrieve all the cases sorted by their own value descending. * - * @param callable|string $key - * @return CasesCollection + * @return CasesCollection */ - public static function sortDescBy(callable|string $key): CasesCollection + public static function sortByDescValue(): CasesCollection { - return static::collect()->sortDescBy($key); + return self::collect()->sortByDescValue(); } } diff --git a/src/Concerns/Compares.php b/src/Concerns/Compares.php index 73fb861..4e4ba4c 100644 --- a/src/Concerns/Compares.php +++ b/src/Concerns/Compares.php @@ -3,20 +3,16 @@ namespace Cerbero\Enum\Concerns; /** - * The trait to compare cases of an enum. - * + * The trait to compare the cases of an enum. */ trait Compares { /** - * Determine whether the enum has the given target - * - * @param mixed $target - * @return bool + * Determine whether the enum includes the given target. */ public static function has(mixed $target): bool { - foreach (static::cases() as $case) { + foreach (self::cases() as $case) { if ($case->is($target)) { return true; } @@ -26,14 +22,11 @@ public static function has(mixed $target): bool } /** - * Determine whether the enum does not have the given target - * - * @param mixed $target - * @return bool + * Determine whether the enum does not include the given target. */ public static function doesntHave(mixed $target): bool { - foreach (static::cases() as $case) { + foreach (self::cases() as $case) { if ($case->is($target)) { return false; } @@ -43,21 +36,15 @@ public static function doesntHave(mixed $target): bool } /** - * Determine whether the current case matches the given target - * - * @param mixed $target - * @return bool + * Determine whether this case matches the given target. */ public function is(mixed $target): bool { - return in_array($target, [$this, static::isPure() ? $this->name : $this->value], true); + return in_array($target, [$this, self::isPure() ? $this->name : $this->value], true); } /** - * Determine whether the current case does not match the given target - * - * @param mixed $target - * @return bool + * Determine whether this case does not match the given target. */ public function isNot(mixed $target): bool { @@ -65,10 +52,9 @@ public function isNot(mixed $target): bool } /** - * Determine whether the current case matches at least one of the given targets + * Determine whether this case matches at least one of the given targets. * - * @param iterable $targets - * @return bool + * @param iterable $targets */ public function in(iterable $targets): bool { @@ -82,10 +68,9 @@ public function in(iterable $targets): bool } /** - * Determine whether the current case does not match any of the given targets + * Determine whether this case does not match any of the given targets. * - * @param iterable $targets - * @return bool + * @param iterable $targets */ public function notIn(iterable $targets): bool { diff --git a/src/Concerns/Enumerates.php b/src/Concerns/Enumerates.php index 3ca0e57..95c9251 100644 --- a/src/Concerns/Enumerates.php +++ b/src/Concerns/Enumerates.php @@ -3,14 +3,13 @@ namespace Cerbero\Enum\Concerns; /** - * The trait to extend enum functionalities. - * + * The trait to supercharge the functionalities of an enum. */ trait Enumerates { use CollectsCases; use Compares; use Hydrates; - use KeysAware; + use IsMagic; use SelfAware; } diff --git a/src/Concerns/Hydrates.php b/src/Concerns/Hydrates.php index 2c664da..5b6afef 100644 --- a/src/Concerns/Hydrates.php +++ b/src/Concerns/Hydrates.php @@ -7,58 +7,40 @@ /** * The trait to hydrate an enum. - * */ trait Hydrates { /** - * Retrieve the case hydrated from the given name (called by pure enums only) + * Retrieve the case hydrated from the given name or fail. + * This method can be called by pure enums only. * - * @param string $name - * @return static * @throws ValueError */ public static function from(string $name): static { - return static::fromName($name); + return self::fromName($name); } /** - * Retrieve the case hydrated from the given name or NULL (called by pure enums only) + * Retrieve the case hydrated from the given name or fail. * - * @param string $name - * @return static|null - */ - public static function tryFrom(string $name): ?static - { - return static::tryFromName($name); - } - - /** - * Retrieve the case hydrated from the given name - * - * @param string $name - * @return static * @throws ValueError */ public static function fromName(string $name): static { - if ($case = static::tryFromName($name)) { + if ($case = self::tryFromName($name)) { return $case; } - throw new ValueError(sprintf('"%s" is not a valid name for enum "%s"', $name, static::class)); + throw new ValueError(sprintf('"%s" is not a valid name for enum "%s"', $name, self::class)); } /** - * Retrieve the case hydrated from the given name or NULL - * - * @param string $name - * @return static|null + * Retrieve the case hydrated from the given name or NULL. */ public static function tryFromName(string $name): ?static { - foreach (static::cases() as $case) { + foreach (self::cases() as $case) { if ($case->name === $name) { return $case; } @@ -68,37 +50,42 @@ public static function tryFromName(string $name): ?static } /** - * Retrieve cases hydrated from the given key + * Retrieve the case hydrated from the given name or NULL. + * This method can be called by pure enums only. + */ + public static function tryFrom(string $name): ?static + { + return self::tryFromName($name); + } + + /** + * Retrieve all the cases hydrated from the given meta or fail. * - * @param callable|string $key - * @param mixed $value - * @return CasesCollection + * @return CasesCollection * @throws ValueError */ - public static function fromKey(callable|string $key, mixed $value): CasesCollection + public static function fromMeta(string $meta, mixed $value = true): CasesCollection { - if ($result = static::tryFromKey($key, $value)) { - return $result; + if ($cases = self::tryFromMeta($meta, $value)) { + return $cases; } - $target = is_callable($key) ? 'given callable key' : "key \"$key\""; - - throw new ValueError(sprintf('Invalid value for the %s for enum "%s"', $target, static::class)); + throw new ValueError(sprintf('Invalid value for the meta "%s" for enum "%s"', $meta, self::class)); } /** - * Retrieve cases hydrated from the given key or NULL + * Retrieve all the cases hydrated from the given meta or NULL. * - * @param callable|string $key - * @param mixed $value - * @return CasesCollection|null + * @return ?CasesCollection */ - public static function tryFromKey(callable|string $key, mixed $value): CasesCollection|null + public static function tryFromMeta(string $meta, mixed $value = true): ?CasesCollection { $cases = []; - foreach (static::cases() as $case) { - if ($case->get($key) === $value) { + foreach (self::cases() as $case) { + $metaValue = $case->resolveMeta($meta); + + if ((is_callable($value) && $value($metaValue) === true) || $metaValue === $value) { $cases[] = $case; } } diff --git a/src/Concerns/IsMagic.php b/src/Concerns/IsMagic.php new file mode 100644 index 0000000..8244e10 --- /dev/null +++ b/src/Concerns/IsMagic.php @@ -0,0 +1,35 @@ +$key ?? $this->$key()); - } catch (Throwable) { - $target = is_callable($key) ? 'The given callable' : "\"$key\""; - throw new ValueError(sprintf('%s is not a valid key for enum "%s"', $target, static::class)); - } - } -} diff --git a/src/Concerns/SelfAware.php b/src/Concerns/SelfAware.php index 6c69832..4fa8834 100644 --- a/src/Concerns/SelfAware.php +++ b/src/Concerns/SelfAware.php @@ -3,30 +3,129 @@ namespace Cerbero\Enum\Concerns; use BackedEnum; +use Cerbero\Enum\Attributes\Meta; +use ReflectionAttribute; +use ReflectionEnum; +use ReflectionEnumUnitCase; +use ReflectionMethod; +use ValueError; /** * The trait to make an enum self-aware. - * */ trait SelfAware { /** - * Determine whether the enum is pure - * - * @return bool + * Determine whether the enum is pure. */ public static function isPure(): bool { - return !static::isBacked(); + return !self::isBacked(); } /** - * Determine whether the enum is backed - * - * @return bool + * Determine whether the enum is backed. */ public static function isBacked(): bool { - return is_subclass_of(static::class, BackedEnum::class); + return is_subclass_of(self::class, BackedEnum::class); + } + + /** + * Retrieve all the meta names of the enum. + * + * @return string[] + */ + public static function metaNames(): array + { + $meta = []; + $enum = new ReflectionEnum(self::class); + + foreach ($enum->getAttributes(Meta::class) as $attribute) { + array_push($meta, ...$attribute->newInstance()->names()); + } + + foreach ($enum->getCases() as $case) { + foreach ($case->getAttributes(Meta::class) as $attribute) { + array_push($meta, ...$attribute->newInstance()->names()); + } + } + + foreach ($enum->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if (! $method->isStatic() && $method->getFileName() == $enum->getFileName()) { + $meta[] = $method->getShortName(); + } + } + + return array_values(array_unique($meta)); + } + + /** + * Retrieve the given item of this case. + * + * @template TItemValue + * + * @param (callable(self): TItemValue)|string $item + * @return TItemValue + * @throws ValueError + */ + public function resolveItem(callable|string $item): mixed + { + return match (true) { + is_callable($item) => $item($this), + property_exists($this, $item) => $this->$item, + default => $this->resolveMeta($item), + }; + } + + /** + * Retrieve the given meta of this case. + * + * @throws ValueError + */ + public function resolveMeta(string $meta): mixed + { + $enum = new ReflectionEnum($this); + $enumFileName = $enum->getFileName(); + + foreach ($enum->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if (! $method->isStatic() && $method->getFileName() == $enumFileName && $method->getShortName() == $meta) { + return $this->$meta(); + } + } + + return $this->resolveMetaAttribute($meta); + } + + /** + * Retrieve the given meta from the attributes. + * + * @throws ValueError + */ + public function resolveMetaAttribute(string $meta): mixed + { + $case = new ReflectionEnumUnitCase($this, $this->name); + + foreach ($case->getAttributes(Meta::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (($metadata = $attribute->newInstance())->has($meta)) { + return $metadata->get($meta); + } + } + + foreach ($case->getEnum()->getAttributes(Meta::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (($metadata = $attribute->newInstance())->has($meta)) { + return $metadata->get($meta); + } + } + + throw new ValueError(sprintf('"%s" is not a valid meta for enum "%s"', $meta, self::class)); + } + + /** + * Retrieve the value of a backed case or the name of a pure case. + */ + public function value(): string|int + { + return $this->value ?? $this->name; } } diff --git a/src/Enums.php b/src/Enums.php new file mode 100644 index 0000000..1ddc0aa --- /dev/null +++ b/src/Enums.php @@ -0,0 +1,97 @@ + $arguments): mixed + */ + protected static ?Closure $onStaticCall = null; + + /** + * The logic to run when an inaccessible case method is called. + * + * @var ?Closure(object $case, string $name, array $arguments): mixed + */ + protected static ?Closure $onCall = null; + + /** + * The logic to run when a case is invoked. + * + * @var ?Closure(object $case, mixed ...$arguments): mixed + */ + protected static ?Closure $onInvoke = null; + + /** + * Set the logic to run when an inaccessible enum method is called. + * + * @param Closure(class-string $enum, string $name, array $arguments): mixed $callback + */ + public static function onStaticCall(Closure $callback): void + { + static::$onStaticCall = $callback; + } + + /** + * Set the logic to run when an inaccessible case method is called. + * + * @param Closure(object $case, string $name, array $arguments): mixed $callback + */ + public static function onCall(Closure $callback): void + { + static::$onCall = $callback; + } + + /** + * Set the logic to run when a case is invoked. + * + * @param Closure(object $case, mixed ...$arguments): mixed $callback + */ + public static function onInvoke(Closure $callback): void + { + static::$onInvoke = $callback; + } + + /** + * Handle the call to an inaccessible enum method. + * + * @param class-string $enum + * @param array $arguments + */ + public static function handleStaticCall(string $enum, string $name, array $arguments): mixed + { + return static::$onStaticCall + ? (static::$onStaticCall)($enum, $name, $arguments) + : $enum::fromName($name)->value(); + } + + /** + * Handle the call to an inaccessible case method. + * + * @param array $arguments + */ + public static function handleCall(object $case, string $name, array $arguments): mixed + { + /** @phpstan-ignore method.notFound */ + return static::$onCall ? (static::$onCall)($case, $name, $arguments) : $case->resolveMetaAttribute($name); + } + + /** + * Handle the invocation of a case. + */ + public static function handleInvoke(object $case, mixed ...$arguments): mixed + { + /** @phpstan-ignore method.notFound */ + return static::$onInvoke ? (static::$onInvoke)($case, ...$arguments) : $case->value(); + } +} diff --git a/tests/BackedEnum.php b/tests/BackedEnum.php index 2c224b6..b020853 100644 --- a/tests/BackedEnum.php +++ b/tests/BackedEnum.php @@ -2,47 +2,24 @@ namespace Cerbero\Enum; +use Cerbero\Enum\Attributes\Meta; use Cerbero\Enum\Concerns\Enumerates; /** * The backed enum to test. - * */ +#[Meta(color: 'green', shape: 'square')] enum BackedEnum: int { use Enumerates; + #[Meta(color: 'red', shape: 'triangle')] case one = 1; - case two = 2; - case three = 3; - /** - * Retrieve the color of the case - * - * @return string - */ - public function color(): string - { - return match ($this) { - static::one => 'red', - static::two => 'green', - static::three => 'blue', - }; - } + case two = 2; - /** - * Retrieve the shape of the case - * - * @return string - */ - public function shape(): string - { - return match ($this) { - static::one => 'triangle', - static::two => 'square', - static::three => 'circle', - }; - } + #[Meta(color: 'blue', shape: 'circle')] + case three = 3; /** * Retrieve whether the case is odd @@ -51,10 +28,6 @@ public function shape(): string */ public function isOdd(): bool { - return match ($this) { - static::one => true, - static::two => false, - static::three => true, - }; + return $this->value % 2 != 0; } } diff --git a/tests/BackedEnumTest.php b/tests/BackedEnumTest.php index 33d2772..74379ea 100644 --- a/tests/BackedEnumTest.php +++ b/tests/BackedEnumTest.php @@ -2,6 +2,8 @@ use Cerbero\Enum\CasesCollection; use Cerbero\Enum\BackedEnum; +use Cerbero\Enum\Enums; +use Pest\Expectation; it('determines whether the enum is pure') ->expect(BackedEnum::isPure()) @@ -15,6 +17,27 @@ ->expect(BackedEnum::names()) ->toBe(['one', 'two', 'three']); +it('retrieves the first case', fn() => expect(BackedEnum::first())->toBe(BackedEnum::one)); + +it('retrieves the first case with a closure') + ->expect(BackedEnum::first(fn(BackedEnum $case) => !$case->isOdd())) + ->toBe(BackedEnum::two); + +it('retrieves the result of mapping over all the cases', function() { + $cases = $keys = []; + + $mapped = BackedEnum::map(function(BackedEnum $case, int $key) use (&$cases, &$keys) { + $cases[] = $case; + $keys[] = $key; + + return $case->color(); + }); + + expect($cases)->toBe([BackedEnum::one, BackedEnum::two, BackedEnum::three]) + ->and($keys)->toBe([0, 1, 2]) + ->and($mapped)->toBe(['red', 'green', 'blue']); +}); + it('retrieves all the values of the backed cases') ->expect(BackedEnum::values()) ->toBe([1, 2, 3]); @@ -22,74 +45,94 @@ it('retrieves a collection with all the cases') ->expect(BackedEnum::collect()) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([BackedEnum::one, BackedEnum::two, BackedEnum::three]); it('retrieves all cases keyed by name') - ->expect(BackedEnum::casesByName()) + ->expect(BackedEnum::keyByName()) + ->toBeInstanceOf(CasesCollection::class) + ->all() ->toBe(['one' => BackedEnum::one, 'two' => BackedEnum::two, 'three' => BackedEnum::three]); it('retrieves all cases keyed by value') - ->expect(BackedEnum::casesByValue()) + ->expect(BackedEnum::keyByValue()) + ->toBeInstanceOf(CasesCollection::class) + ->all() ->toBe([1 => BackedEnum::one, 2 => BackedEnum::two, 3 => BackedEnum::three]); it('retrieves all cases keyed by a custom key') - ->expect(BackedEnum::casesBy('color')) + ->expect(BackedEnum::keyBy('color')) + ->toBeInstanceOf(CasesCollection::class) + ->all() ->toBe(['red' => BackedEnum::one, 'green' => BackedEnum::two, 'blue' => BackedEnum::three]); it('retrieves all cases keyed by the result of a closure') - ->expect(BackedEnum::casesBy(fn (BackedEnum $case) => $case->shape())) - ->toBe(['triangle' => BackedEnum::one, 'square' => BackedEnum::two, 'circle' => BackedEnum::three]); + ->expect(BackedEnum::keyBy(fn(BackedEnum $case) => $case->shape())) + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $case, Expectation $key) => $key->toBe('triangle')->and($case)->toBe(BackedEnum::one), + fn(Expectation $case, Expectation $key) => $key->toBe('square')->and($case)->toBe(BackedEnum::two), + fn(Expectation $case, Expectation $key) => $key->toBe('circle')->and($case)->toBe(BackedEnum::three), + ); it('retrieves all cases grouped by a custom key', function () { expect(BackedEnum::groupBy('color')) - ->toBe(['red' => [BackedEnum::one], 'green' => [BackedEnum::two], 'blue' => [BackedEnum::three]]); + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $cases, Expectation $key) => $key->toBe('red')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([BackedEnum::one]), + fn(Expectation $cases, Expectation $key) => $key->toBe('green')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([BackedEnum::two]), + fn(Expectation $cases, Expectation $key) => $key->toBe('blue')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([BackedEnum::three]), + ); }); it('retrieves all cases grouped by the result of a closure', function () { - expect(BackedEnum::groupBy(fn (BackedEnum $case) => $case->isOdd())) - ->toBe([1 => [BackedEnum::one, BackedEnum::three], 0 => [BackedEnum::two]]); + expect(BackedEnum::groupBy(fn(BackedEnum $case) => $case->isOdd())) + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $cases) => $cases->toBeInstanceOf(CasesCollection::class)->all()->toBe([BackedEnum::one, BackedEnum::three]), + fn(Expectation $cases) => $cases->toBeInstanceOf(CasesCollection::class)->all()->toBe([BackedEnum::two]), + ); }); it('retrieves a collection with the filtered cases') - ->expect(BackedEnum::filter(fn (UnitEnum $case) => $case->name !== 'three')) + ->expect(BackedEnum::filter(fn(UnitEnum $case) => $case->name !== 'three')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([BackedEnum::one, BackedEnum::two]); -it('retrieves a collection with cases filtered by a key', function () { +it('retrieves a collection with cases filtered by a meta', function () { expect(BackedEnum::filter('isOdd')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::one, BackedEnum::three]); + ->all() + ->toBe([0 => BackedEnum::one, 2 => BackedEnum::three]); }); it('retrieves a collection of cases having the given names') ->expect(BackedEnum::only('two', 'three')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::two, BackedEnum::three]); + ->all() + ->toBe([1 => BackedEnum::two, 2 => BackedEnum::three]); it('retrieves a collection of cases not having the given names') ->expect(BackedEnum::except('one', 'three')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::two]); + ->all() + ->toBe([1 => BackedEnum::two]); it('retrieves a collection of backed cases having the given values') ->expect(BackedEnum::onlyValues(2, 3)) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::two, BackedEnum::three]); + ->all() + ->toBe([1 => BackedEnum::two, 2 => BackedEnum::three]); it('retrieves a collection of backed cases not having the given values') ->expect(BackedEnum::exceptValues(1, 3)) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::two]); + ->all() + ->toBe([1 => BackedEnum::two]); it('retrieves an array of values') - ->expect(BackedEnum::pluck()) + ->expect(BackedEnum::pluck('value')) ->toBe([1, 2, 3]); it('retrieves an array of custom values') @@ -101,11 +144,11 @@ ->toBe(['triangle' => 'red', 'square' => 'green', 'circle' => 'blue']); it('retrieves an associative array with keys and values resolved from closures') - ->expect(BackedEnum::pluck(fn (BackedEnum $case) => $case->name, fn (BackedEnum $case) => $case->color())) + ->expect(BackedEnum::pluck(fn(BackedEnum $case) => $case->name, fn(BackedEnum $case) => $case->color())) ->toBe(['red' => 'one', 'green' => 'two', 'blue' => 'three']); it('determines whether an enum has a target') - ->expect(fn (mixed $target, bool $result) => BackedEnum::has($target) === $result) + ->expect(fn(mixed $target, bool $result) => BackedEnum::has($target) === $result) ->toBeTrue() ->with([ [BackedEnum::one, true], @@ -119,7 +162,7 @@ ]); it('determines whether an enum does not have a target') - ->expect(fn (mixed $target, bool $result) => BackedEnum::doesntHave($target) === $result) + ->expect(fn(mixed $target, bool $result) => BackedEnum::doesntHave($target) === $result) ->toBeTrue() ->with([ [BackedEnum::one, false], @@ -133,7 +176,7 @@ ]); it('determines whether an enum case matches a target') - ->expect(fn (mixed $target, bool $result) => BackedEnum::one->is($target) === $result) + ->expect(fn(mixed $target, bool $result) => BackedEnum::one->is($target) === $result) ->toBeTrue() ->with([ [BackedEnum::one, true], @@ -147,7 +190,7 @@ ]); it('determines whether an enum case does not match a target') - ->expect(fn (mixed $target, bool $result) => BackedEnum::one->isNot($target) === $result) + ->expect(fn(mixed $target, bool $result) => BackedEnum::one->isNot($target) === $result) ->toBeTrue() ->with([ [BackedEnum::one, false], @@ -161,7 +204,7 @@ ]); it('determines whether an enum case matches some targets') - ->expect(fn (mixed $targets, bool $result) => BackedEnum::one->in($targets) === $result) + ->expect(fn(mixed $targets, bool $result) => BackedEnum::one->in($targets) === $result) ->toBeTrue() ->with([ [[BackedEnum::one, BackedEnum::two], true], @@ -175,7 +218,7 @@ ]); it('determines whether an enum case does not match any target') - ->expect(fn (mixed $targets, bool $result) => BackedEnum::one->notIn($targets) === $result) + ->expect(fn(mixed $targets, bool $result) => BackedEnum::one->notIn($targets) === $result) ->toBeTrue() ->with([ [[BackedEnum::one, BackedEnum::two], false], @@ -191,49 +234,49 @@ it('retrieves a collection of cases sorted by name ascending') ->expect(BackedEnum::sort()) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::one, BackedEnum::three, BackedEnum::two]); + ->all() + ->toBe([0 => BackedEnum::one, 2 => BackedEnum::three, 1 => BackedEnum::two]); it('retrieves a collection of cases sorted by name descending') ->expect(BackedEnum::sortDesc()) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::two, BackedEnum::three, BackedEnum::one]); + ->all() + ->toBe([1 => BackedEnum::two, 2 => BackedEnum::three, 0 => BackedEnum::one]); it('retrieves a collection of cases sorted by value ascending') ->expect(BackedEnum::sortByValue()) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([BackedEnum::one, BackedEnum::two, BackedEnum::three]); it('retrieves a collection of cases sorted by value descending') - ->expect(BackedEnum::sortDescByValue()) + ->expect(BackedEnum::sortByDescValue()) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::three, BackedEnum::two, BackedEnum::one]); + ->all() + ->toBe([2 => BackedEnum::three, 1 => BackedEnum::two, 0 => BackedEnum::one]); it('retrieves a collection of cases sorted by a custom value ascending') ->expect(BackedEnum::sortBy('color')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::three, BackedEnum::two, BackedEnum::one]); + ->all() + ->toBe([2 => BackedEnum::three, 1 => BackedEnum::two, 0 => BackedEnum::one]); it('retrieves a collection of cases sorted by a custom value descending') - ->expect(BackedEnum::sortDescBy('color')) + ->expect(BackedEnum::sortByDesc('color')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([BackedEnum::one, BackedEnum::two, BackedEnum::three]); it('retrieves a collection of cases sorted by the result of a closure ascending') - ->expect(BackedEnum::sortBy(fn (BackedEnum $case) => $case->shape())) + ->expect(BackedEnum::sortBy(fn(BackedEnum $case) => $case->shape())) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::three, BackedEnum::two, BackedEnum::one]); + ->all() + ->toBe([2 => BackedEnum::three, 1 => BackedEnum::two, 0 => BackedEnum::one]); it('retrieves a collection of cases sorted by the result of a closure descending') - ->expect(BackedEnum::sortDescBy(fn (BackedEnum $case) => $case->shape())) + ->expect(BackedEnum::sortByDesc(fn(BackedEnum $case) => $case->shape())) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([BackedEnum::one, BackedEnum::two, BackedEnum::three]); it('retrieves the count of cases') @@ -241,7 +284,7 @@ ->toBe(3); it('retrieves the case hydrated from a value') - ->expect(fn (int $value, BackedEnum $case) => BackedEnum::from($value) === $case) + ->expect(fn(int $value, BackedEnum $case) => BackedEnum::from($value) === $case) ->toBeTrue() ->with([ [1, BackedEnum::one], @@ -249,11 +292,12 @@ [3, BackedEnum::three], ]); -it('throws a value error when hydrating backed cases with a missing value', fn () => BackedEnum::from(4)) - ->throws(ValueError::class, '4 is not a valid backing value for enum "Cerbero\Enum\BackedEnum"'); +it('throws a value error when hydrating backed cases with a missing value', fn() => BackedEnum::from(4)) + ->throwsIf(version_compare(PHP_VERSION, '8.2') == -1, ValueError::class, '4 is not a valid backing value for enum "Cerbero\Enum\BackedEnum"') + ->throwsIf(version_compare(PHP_VERSION, '8.2') >= 0, ValueError::class, '4 is not a valid backing value for enum Cerbero\Enum\BackedEnum'); it('retrieves the case hydrated from a value or returns null') - ->expect(fn (int $value, ?BackedEnum $case) => BackedEnum::tryFrom($value) === $case) + ->expect(fn(int $value, ?BackedEnum $case) => BackedEnum::tryFrom($value) === $case) ->toBeTrue() ->not->toThrow(ValueError::class) ->with([ @@ -264,7 +308,7 @@ ]); it('retrieves the case hydrated from a name') - ->expect(fn (string $name, BackedEnum $case) => BackedEnum::fromName($name) === $case) + ->expect(fn(string $name, BackedEnum $case) => BackedEnum::fromName($name) === $case) ->toBeTrue() ->with([ ['one', BackedEnum::one], @@ -272,11 +316,11 @@ ['three', BackedEnum::three], ]); -it('throws a value error when hydrating backed cases with a missing name', fn () => BackedEnum::fromName('four')) +it('throws a value error when hydrating backed cases with a missing name', fn() => BackedEnum::fromName('four')) ->throws(ValueError::class, '"four" is not a valid name for enum "Cerbero\Enum\BackedEnum"'); it('retrieves the case hydrated from a name or returns null') - ->expect(fn (string $name, ?BackedEnum $case) => BackedEnum::tryFromName($name) === $case) + ->expect(fn(string $name, ?BackedEnum $case) => BackedEnum::tryFromName($name) === $case) ->toBeTrue() ->not->toThrow(ValueError::class) ->with([ @@ -286,43 +330,101 @@ ['four', null], ]); -it('retrieves the cases hydrated from a key') - ->expect(fn (string $key, mixed $value, array $cases) => BackedEnum::fromKey($key, $value)->cases() === $cases) +it('retrieves the cases hydrated from a meta') + ->expect(fn(string $meta, mixed $value, array $cases) => BackedEnum::fromMeta($meta, $value)->all() === $cases) ->toBeTrue() ->with([ ['color', 'red', [BackedEnum::one]], - ['name', 'three', [BackedEnum::three]], + ['shape', 'circle', [BackedEnum::three]], ['isOdd', true, [BackedEnum::one, BackedEnum::three]], ]); -it('retrieves the cases hydrated from a key using a closure') - ->expect(BackedEnum::fromKey(fn (BackedEnum $case) => $case->shape(), 'square')) +it('retrieves the cases hydrated from a meta using a closure') + ->expect(BackedEnum::fromMeta('shape', fn(string $shape) => $shape == 'square')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([BackedEnum::two]); -it('throws a value error when hydrating cases with an invalid key', fn () => BackedEnum::fromKey('color', 'orange')) - ->throws(ValueError::class, 'Invalid value for the key "color" for enum "Cerbero\Enum\BackedEnum"'); +it('throws a value error when hydrating cases with an invalid meta', fn() => BackedEnum::fromMeta('color', 'orange')) + ->throws(ValueError::class, 'Invalid value for the meta "color" for enum "Cerbero\Enum\BackedEnum"'); -it('retrieves the case hydrated from a key or returns null') - ->expect(fn (string $key, mixed $value, ?array $cases) => BackedEnum::tryFromKey($key, $value)?->cases() === $cases) +it('retrieves the case hydrated from a meta or returns null') + ->expect(fn(string $meta, mixed $value, ?array $cases) => BackedEnum::tryFromMeta($meta, $value)?->all() === $cases) ->toBeTrue() ->not->toThrow(ValueError::class) ->with([ ['color', 'red', [BackedEnum::one]], - ['name', 'three', [BackedEnum::three]], ['isOdd', true, [BackedEnum::one, BackedEnum::three]], ['shape', 'rectangle', null], ]); -it('attempts to retrieve the case hydrated from a key using a closure') - ->expect(BackedEnum::tryFromKey(fn (BackedEnum $case) => $case->shape(), 'square')) +it('attempts to retrieve the case hydrated from a meta using a closure') + ->expect(BackedEnum::tryFromMeta('shape', fn(string $shape) => $shape == 'square')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([BackedEnum::two]); -it('retrieves the key of a case') - ->expect(fn (string $key, mixed $value) => BackedEnum::one->get($key) === $value) +it('handles the call to an inaccessible enum method') + ->expect(BackedEnum::one()) + ->toBe(1); + +it('fails handling the call to an invalid enum method', fn() => BackedEnum::four()) + ->throws(ValueError::class, '"four" is not a valid name for enum "Cerbero\Enum\BackedEnum"'); + +it('runs custom logic when calling an inaccessible enum method', function() { + Enums::onStaticCall(function(string $enum, string $name, array $arguments) { + expect($enum)->toBe(BackedEnum::class) + ->and($name)->toBe('unknownStaticMethod') + ->and($arguments)->toBe([1, 2, 3]); + + return 'ciao'; + }); + + expect(BackedEnum::unknownStaticMethod(1, 2, 3))->toBe('ciao'); + + (fn() => static::$onStaticCall = null)->bindTo(null, Enums::class)(); +}); + +it('handles the call to an inaccessible case method', fn() => BackedEnum::one->unknownMethod()) + ->throws(Error::class, '"unknownMethod" is not a valid meta for enum "Cerbero\Enum\BackedEnum"'); + +it('runs custom logic when calling an inaccessible case method', function() { + Enums::onCall(function(object $case, string $name, array $arguments) { + expect($case)->toBeInstanceOf(BackedEnum::class) + ->and($name)->toBe('unknownMethod') + ->and($arguments)->toBe([1, 2, 3]); + + return 'ciao'; + }); + + expect(BackedEnum::one->unknownMethod(1, 2, 3))->toBe('ciao'); + + (fn() => self::$onCall = null)->bindTo(null, Enums::class)(); +}); + +it('handles the invocation of a case') + ->expect((BackedEnum::one)()) + ->toBe(1); + +it('runs custom logic when invocating a case', function() { + Enums::onInvoke(function(object $case, mixed ...$arguments) { + expect($case)->toBeInstanceOf(BackedEnum::class) + ->and($arguments)->toBe([1, 2, 3]); + + return 'ciao'; + }); + + expect((BackedEnum::one)(1, 2, 3))->toBe('ciao'); + + (fn() => self::$onInvoke = null)->bindTo(null, Enums::class)(); +}); + +it('retrieves the meta names of an enum', function() { + expect(BackedEnum::metaNames())->toBe(['color', 'shape', 'isOdd']); +}); + +it('retrieves a case item') + ->expect(fn(string $item, mixed $value) => BackedEnum::one->resolveItem($item) === $value) ->toBeTrue() ->with([ ['name', 'one'], @@ -331,9 +433,13 @@ ['shape', 'triangle'], ]); -it('retrieves the key of a case using a closure') - ->expect(BackedEnum::one->get(fn (BackedEnum $case) => $case->color())) +it('retrieves the item of a case using a closure') + ->expect(BackedEnum::one->resolveItem(fn(BackedEnum $case) => $case->color())) ->toBe('red'); -it('throws a value error when attempting to retrieve an invalid key', fn () => BackedEnum::one->get('invalid')) - ->throws(ValueError::class, '"invalid" is not a valid key for enum "Cerbero\Enum\BackedEnum"'); +it('throws a value error when attempting to retrieve an invalid item', fn() => BackedEnum::one->resolveItem('invalid')) + ->throws(ValueError::class, '"invalid" is not a valid meta for enum "Cerbero\Enum\BackedEnum"'); + +it('retrieves the value of a backed case or the name of a pure case', function() { + expect(BackedEnum::one->value())->toBe(1); +}); diff --git a/tests/CasesCollectionTest.php b/tests/CasesCollectionTest.php index f22c519..54e7bd7 100644 --- a/tests/CasesCollectionTest.php +++ b/tests/CasesCollectionTest.php @@ -3,10 +3,11 @@ use Cerbero\Enum\BackedEnum; use Cerbero\Enum\CasesCollection; use Cerbero\Enum\PureEnum; +use Pest\Expectation; it('retrieves all the cases') ->expect(new CasesCollection(PureEnum::cases())) - ->cases() + ->all() ->toBe([PureEnum::one, PureEnum::two, PureEnum::three]); it('retrieves the count of all the cases') @@ -14,6 +15,11 @@ ->count() ->toBe(3); +it('retrieves all the cases as a plain array recursively') + ->expect((new CasesCollection(PureEnum::cases()))->groupBy('isOdd')) + ->toArray() + ->toBe([1 => [PureEnum::one, PureEnum::three], 0 => [PureEnum::two]]); + it('retrieves the first case') ->expect(new CasesCollection(PureEnum::cases())) ->first() @@ -21,7 +27,7 @@ it('retrieves the first case with a closure') ->expect(new CasesCollection(PureEnum::cases())) - ->first(fn (PureEnum $case) => !$case->isOdd()) + ->first(fn(PureEnum $case) => !$case->isOdd()) ->toBe(PureEnum::two); it('returns null if no case is present') @@ -29,29 +35,37 @@ ->first() ->toBeNull(); -it('returns a default value if no case is present') - ->expect(new CasesCollection([])) - ->first(default: PureEnum::one) - ->toBe(PureEnum::one); +it('retrieves the result of mapping over the cases') + ->expect(new CasesCollection(PureEnum::cases())) + ->map(fn(PureEnum $case) => $case->color()) + ->toBe(['red', 'green', 'blue']); it('retrieves the cases keyed by name') - ->expect(new CasesCollection(PureEnum::cases())) - ->keyByName() + ->expect((new CasesCollection(PureEnum::cases()))->keyByName()) + ->toBeInstanceOf(CasesCollection::class) + ->all() ->toBe(['one' => PureEnum::one, 'two' => PureEnum::two, 'three' => PureEnum::three]); it('retrieves the cases keyed by a custom key') - ->expect(new CasesCollection(PureEnum::cases())) - ->keyBy('color') + ->expect((new CasesCollection(PureEnum::cases()))->keyBy('color')) + ->toBeInstanceOf(CasesCollection::class) + ->all() ->toBe(['red' => PureEnum::one, 'green' => PureEnum::two, 'blue' => PureEnum::three]); it('retrieves the cases keyed by a custom closure') ->expect(new CasesCollection(PureEnum::cases())) - ->keyBy(fn (PureEnum $case) => $case->shape()) - ->toBe(['triangle' => PureEnum::one, 'square' => PureEnum::two, 'circle' => PureEnum::three]); - -it('retrieves the cases keyed by value') - ->expect(new CasesCollection(BackedEnum::cases())) - ->keyByValue() + ->keyBy(fn(PureEnum $case) => $case->shape()) + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $case, Expectation $key) => $key->toBe('triangle')->and($case)->toBe(PureEnum::one), + fn(Expectation $case, Expectation $key) => $key->toBe('square')->and($case)->toBe(PureEnum::two), + fn(Expectation $case, Expectation $key) => $key->toBe('circle')->and($case)->toBe(PureEnum::three), + ); + +it('retrieves the cases keyed by value xxxxxxx') + ->expect((new CasesCollection(BackedEnum::cases()))->keyByValue()) + ->toBeInstanceOf(CasesCollection::class) + ->all() ->toBe([1 => BackedEnum::one, 2 => BackedEnum::two, 3 => BackedEnum::three]); it('retrieves an empty array when trying to key cases by value belonging to a pure enum') @@ -62,12 +76,21 @@ it('retrieves the cases grouped by a custom key') ->expect(new CasesCollection(PureEnum::cases())) ->groupBy('color') - ->toBe(['red' => [PureEnum::one], 'green' => [PureEnum::two], 'blue' => [PureEnum::three]]); + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $cases, Expectation $key) => $key->toBe('red')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::one]), + fn(Expectation $cases, Expectation $key) => $key->toBe('green')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::two]), + fn(Expectation $cases, Expectation $key) => $key->toBe('blue')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::three]), + ); it('retrieves the cases grouped by a custom closure') ->expect(new CasesCollection(PureEnum::cases())) - ->groupBy(fn (PureEnum $case) => $case->isOdd()) - ->toBe([1 => [PureEnum::one, PureEnum::three], 0 => [PureEnum::two]]); + ->groupBy(fn(PureEnum $case) => $case->isOdd()) + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $cases) => $cases->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::one, PureEnum::three]), + fn(Expectation $cases) => $cases->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::two]), + ); it('retrieves all the names of the cases') ->expect(new CasesCollection(PureEnum::cases())) @@ -84,14 +107,14 @@ ->values() ->toBeEmpty(); -it('retrieves a list of names by default when plucking a pure enum') +it('retrieves a list of names') ->expect(new CasesCollection(PureEnum::cases())) - ->pluck() + ->pluck('name') ->toBe(['one', 'two', 'three']); -it('retrieves a list of names by default when plucking a backed enum') +it('retrieves a list of values') ->expect(new CasesCollection(BackedEnum::cases())) - ->pluck() + ->pluck('value') ->toBe([1, 2, 3]); it('retrieves a list of custom values when plucking with an argument') @@ -101,7 +124,7 @@ it('retrieves a list of custom values when plucking with a closure') ->expect(new CasesCollection(PureEnum::cases())) - ->pluck(fn (PureEnum $case) => $case->shape()) + ->pluck(fn(PureEnum $case) => $case->shape()) ->toBe(['triangle', 'square', 'circle']); it('retrieves an associative array with custom values and keys when plucking with arguments') @@ -111,105 +134,105 @@ it('retrieves an associative array with custom values and keys when plucking with closures') ->expect(new CasesCollection(PureEnum::cases())) - ->pluck(fn (PureEnum $case) => $case->shape(), fn (PureEnum $case) => $case->color()) + ->pluck(fn(PureEnum $case) => $case->shape(), fn(PureEnum $case) => $case->color()) ->toBe(['red' => 'triangle', 'green' => 'square', 'blue' => 'circle']); it('retrieves a collection with filtered cases', function () { - expect((new CasesCollection(PureEnum::cases()))->filter(fn (PureEnum $case) => $case->isOdd())) + expect((new CasesCollection(PureEnum::cases()))->filter(fn(PureEnum $case) => $case->isOdd())) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::one, PureEnum::three]); + ->all() + ->toBe([0 => PureEnum::one, 2 => PureEnum::three]); }); it('retrieves a collection with cases filtered by a key', function () { expect((new CasesCollection(PureEnum::cases()))->filter('isOdd')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::one, PureEnum::three]); + ->all() + ->toBe([0 => PureEnum::one, 2 => PureEnum::three]); }); it('retrieves a collection of cases with the given names', function () { expect((new CasesCollection(PureEnum::cases()))->only('one', 'three')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::one, PureEnum::three]); + ->all() + ->toBe([0 => PureEnum::one, 2 => PureEnum::three]); }); it('retrieves a collection of cases excluding the given names', function () { expect((new CasesCollection(PureEnum::cases()))->except('one', 'three')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::two]); + ->all() + ->toBe([1 => PureEnum::two]); }); it('retrieves a collection of cases with the given values', function () { expect((new CasesCollection(BackedEnum::cases()))->onlyValues(1, 3)) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::one, BackedEnum::three]); + ->all() + ->toBe([0 => BackedEnum::one, 2 => BackedEnum::three]); }); it('retrieves a collection of cases excluding the given values', function () { expect((new CasesCollection(BackedEnum::cases()))->exceptValues(1, 3)) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::two]); + ->all() + ->toBe([1 => BackedEnum::two]); }); it('retrieves an empty collection of cases when when including values of pure enums', function () { expect((new CasesCollection(PureEnum::cases()))->onlyValues(1, 3)) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBeEmpty(); }); it('retrieves an empty collection of cases when when excluding values of pure enums', function () { expect((new CasesCollection(PureEnum::cases()))->exceptValues(1, 3)) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBeEmpty(); }); it('retrieves a collection of cases sorted by name ascending', function () { expect((new CasesCollection(PureEnum::cases()))->sort()) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::one, PureEnum::three, PureEnum::two]); + ->all() + ->toBe([0 => PureEnum::one, 2 => PureEnum::three, 1 => PureEnum::two]); }); it('retrieves a collection of cases sorted by name decending', function () { expect((new CasesCollection(PureEnum::cases()))->sortDesc()) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::two, PureEnum::three, PureEnum::one]); + ->all() + ->toBe([1 => PureEnum::two, 2 => PureEnum::three, 0 => PureEnum::one]); }); it('retrieves a collection of cases sorted by a key ascending', function () { expect((new CasesCollection(PureEnum::cases()))->sortBy('color')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::three, PureEnum::two, PureEnum::one]); + ->all() + ->toBe([2 => PureEnum::three, 1 => PureEnum::two, 0 => PureEnum::one]); }); it('retrieves a collection of cases sorted by a key decending', function () { - expect((new CasesCollection(PureEnum::cases()))->sortDescBy('color')) + expect((new CasesCollection(PureEnum::cases()))->sortByDesc('color')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([PureEnum::one, PureEnum::two, PureEnum::three]); }); it('retrieves a collection of cases sorted by value ascending', function () { expect((new CasesCollection(BackedEnum::cases()))->sortByValue()) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([BackedEnum::one, BackedEnum::two, BackedEnum::three]); }); it('retrieves a collection of cases sorted by value decending', function () { - expect((new CasesCollection(BackedEnum::cases()))->sortDescByValue()) + expect((new CasesCollection(BackedEnum::cases()))->sortByDescValue()) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([BackedEnum::three, BackedEnum::two, BackedEnum::one]); + ->all() + ->toBe([2 => BackedEnum::three, 1 => BackedEnum::two, 0 => BackedEnum::one]); }); it('retrieves the iterator', function () { diff --git a/tests/InvalidMetaAttribute.php b/tests/InvalidMetaAttribute.php new file mode 100644 index 0000000..4889037 --- /dev/null +++ b/tests/InvalidMetaAttribute.php @@ -0,0 +1,19 @@ + 'red', - static::two => 'green', - static::three => 'blue', - }; - } + case two; - /** - * Retrieve the shape of the case - * - * @return string - */ - public function shape(): string - { - return match ($this) { - static::one => 'triangle', - static::two => 'square', - static::three => 'circle', - }; - } + #[Meta(color: 'blue', shape: 'circle')] + case three; /** * Retrieve whether the case is odd @@ -51,10 +28,6 @@ public function shape(): string */ public function isOdd(): bool { - return match ($this) { - static::one => true, - static::two => false, - static::three => true, - }; + return $this->name != 'two'; } } diff --git a/tests/PureEnumTest.php b/tests/PureEnumTest.php index b52341b..aaa2d12 100644 --- a/tests/PureEnumTest.php +++ b/tests/PureEnumTest.php @@ -1,7 +1,10 @@ expect(PureEnum::isPure()) @@ -14,37 +17,76 @@ it('retrieves a collection with all the cases') ->expect(PureEnum::collect()) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([PureEnum::one, PureEnum::two, PureEnum::three]); +it('retrieves the first case', fn() => expect(PureEnum::first())->toBe(PureEnum::one)); + +it('retrieves the first case with a closure') + ->expect(PureEnum::first(fn(PureEnum $case) => !$case->isOdd())) + ->toBe(PureEnum::two); + +it('retrieves the result of mapping over all the cases', function() { + $cases = $keys = []; + + $mapped = PureEnum::map(function(PureEnum $case, int $key) use (&$cases, &$keys) { + $cases[] = $case; + $keys[] = $key; + + return $case->color(); + }); + + expect($cases)->toBe([PureEnum::one, PureEnum::two, PureEnum::three]) + ->and($keys)->toBe([0, 1, 2]) + ->and($mapped)->toBe(['red', 'green', 'blue']); +}); + it('retrieves all cases keyed by name', function () { - expect(PureEnum::casesByName()) + expect(PureEnum::keyByName()) + ->toBeInstanceOf(CasesCollection::class) + ->all() ->toBe(['one' => PureEnum::one, 'two' => PureEnum::two, 'three' => PureEnum::three]); }); it('retrieves all cases keyed by value', function () { - expect(PureEnum::casesByValue()) + expect(PureEnum::keyByValue()) ->toBeEmpty(); }); it('retrieves all cases keyed by a custom key', function () { - expect(PureEnum::casesBy('color')) + expect(PureEnum::keyBy('color')) + ->toBeInstanceOf(CasesCollection::class) + ->all() ->toBe(['red' => PureEnum::one, 'green' => PureEnum::two, 'blue' => PureEnum::three]); }); it('retrieves all cases keyed by the result of a closure', function () { - expect(PureEnum::casesBy(fn (PureEnum $case) => $case->shape())) - ->toBe(['triangle' => PureEnum::one, 'square' => PureEnum::two, 'circle' => PureEnum::three]); + expect(PureEnum::keyBy(fn(PureEnum $case) => $case->shape())) + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $case, Expectation $key) => $key->toBe('triangle')->and($case)->toBe(PureEnum::one), + fn(Expectation $case, Expectation $key) => $key->toBe('square')->and($case)->toBe(PureEnum::two), + fn(Expectation $case, Expectation $key) => $key->toBe('circle')->and($case)->toBe(PureEnum::three), + ); }); it('retrieves all cases grouped by a custom key', function () { expect(PureEnum::groupBy('color')) - ->toBe(['red' => [PureEnum::one], 'green' => [PureEnum::two], 'blue' => [PureEnum::three]]); + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $cases, Expectation $key) => $key->toBe('red')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::one]), + fn(Expectation $cases, Expectation $key) => $key->toBe('green')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::two]), + fn(Expectation $cases, Expectation $key) => $key->toBe('blue')->and($cases)->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::three]), + ); }); it('retrieves all cases grouped by the result of a closure', function () { - expect(PureEnum::groupBy(fn (PureEnum $case) => $case->isOdd())) - ->toBe([1 => [PureEnum::one, PureEnum::three], 0 => [PureEnum::two]]); + expect(PureEnum::groupBy(fn(PureEnum $case) => $case->isOdd())) + ->toBeInstanceOf(CasesCollection::class) + ->sequence( + fn(Expectation $cases) => $cases->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::one, PureEnum::three]), + fn(Expectation $cases) => $cases->toBeInstanceOf(CasesCollection::class)->all()->toBe([PureEnum::two]), + ); }); it('retrieves all the names of the cases', function () { @@ -56,49 +98,49 @@ }); it('retrieves a collection with the filtered cases', function () { - expect(PureEnum::filter(fn (UnitEnum $case) => $case->name !== 'three')) + expect(PureEnum::filter(fn(UnitEnum $case) => $case->name !== 'three')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([PureEnum::one, PureEnum::two]); }); -it('retrieves a collection with cases filtered by a key', function () { +it('retrieves a collection with cases filtered by a meta', function () { expect(PureEnum::filter('isOdd')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::one, PureEnum::three]); + ->all() + ->toBe([0 => PureEnum::one, 2 => PureEnum::three]); }); it('retrieves a collection of cases having the given names', function () { expect(PureEnum::only('two', 'three')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::two, PureEnum::three]); + ->all() + ->toBe([1 => PureEnum::two, 2 => PureEnum::three]); }); it('retrieves a collection of cases not having the given names', function () { expect(PureEnum::except('one', 'three')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::two]); + ->all() + ->toBe([1 => PureEnum::two]); }); it('retrieves a collection of backed cases having the given values', function () { expect(PureEnum::onlyValues(2, 3)) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBeEmpty(); }); it('retrieves a collection of backed cases not having the given values', function () { expect(PureEnum::exceptValues(1, 3)) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBeEmpty(); }); -it('retrieves an array of values', function () { - expect(PureEnum::pluck())->toBe(['one', 'two', 'three']); +it('retrieves an array of names', function () { + expect(PureEnum::pluck('name'))->toBe(['one', 'two', 'three']); }); it('retrieves an array of custom values', function () { @@ -111,12 +153,12 @@ }); it('retrieves an associative array with keys and values resolved from closures', function () { - expect(PureEnum::pluck(fn (PureEnum $case) => $case->name, fn (PureEnum $case) => $case->color())) + expect(PureEnum::pluck(fn(PureEnum $case) => $case->name, fn(PureEnum $case) => $case->color())) ->toBe(['red' => 'one', 'green' => 'two', 'blue' => 'three']); }); it('determines whether an enum has a target') - ->expect(fn (mixed $target, bool $result) => PureEnum::has($target) === $result) + ->expect(fn(mixed $target, bool $result) => PureEnum::has($target) === $result) ->toBeTrue() ->with([ [PureEnum::one, true], @@ -130,7 +172,7 @@ ]); it('determines whether an enum does not have a target') - ->expect(fn (mixed $target, bool $result) => PureEnum::doesntHave($target) === $result) + ->expect(fn(mixed $target, bool $result) => PureEnum::doesntHave($target) === $result) ->toBeTrue() ->with([ [PureEnum::one, false], @@ -144,7 +186,7 @@ ]); it('determines whether an enum case matches a target') - ->expect(fn (mixed $target, bool $result) => PureEnum::one->is($target) === $result) + ->expect(fn(mixed $target, bool $result) => PureEnum::one->is($target) === $result) ->toBeTrue() ->with([ [PureEnum::one, true], @@ -158,7 +200,7 @@ ]); it('determines whether an enum case does not match a target') - ->expect(fn (mixed $target, bool $result) => PureEnum::one->isNot($target) === $result) + ->expect(fn(mixed $target, bool $result) => PureEnum::one->isNot($target) === $result) ->toBeTrue() ->with([ [PureEnum::one, false], @@ -172,7 +214,7 @@ ]); it('determines whether an enum case matches some targets') - ->expect(fn (mixed $targets, bool $result) => PureEnum::one->in($targets) === $result) + ->expect(fn(mixed $targets, bool $result) => PureEnum::one->in($targets) === $result) ->toBeTrue() ->with([ [[PureEnum::one, PureEnum::two], true], @@ -186,7 +228,7 @@ ]); it('determines whether an enum case does not match any target') - ->expect(fn (mixed $targets, bool $result) => PureEnum::one->notIn($targets) === $result) + ->expect(fn(mixed $targets, bool $result) => PureEnum::one->notIn($targets) === $result) ->toBeTrue() ->with([ [[PureEnum::one, PureEnum::two], false], @@ -202,56 +244,56 @@ it('retrieves a collection of cases sorted by name ascending', function () { expect(PureEnum::sort()) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::one, PureEnum::three, PureEnum::two]); + ->all() + ->toBe([0 => PureEnum::one, 2 => PureEnum::three, 1 => PureEnum::two]); }); it('retrieves a collection of cases sorted by name descending', function () { expect(PureEnum::sortDesc()) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::two, PureEnum::three, PureEnum::one]); + ->all() + ->toBe([1 => PureEnum::two, 2 => PureEnum::three, 0 => PureEnum::one]); }); it('retrieves a collection of cases sorted by value ascending', function () { expect(PureEnum::sortByValue()) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBeEmpty(); }); it('retrieves a collection of cases sorted by value descending', function () { - expect(PureEnum::sortDescByValue()) + expect(PureEnum::sortByDescValue()) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBeEmpty(); }); it('retrieves a collection of cases sorted by a custom value ascending', function () { expect(PureEnum::sortBy('color')) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::three, PureEnum::two, PureEnum::one]); + ->all() + ->toBe([2 => PureEnum::three, 1 => PureEnum::two, 0 => PureEnum::one]); }); it('retrieves a collection of cases sorted by a custom value descending', function () { - expect(PureEnum::sortDescBy('color')) + expect(PureEnum::sortByDesc('color')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([PureEnum::one, PureEnum::two, PureEnum::three]); }); it('retrieves a collection of cases sorted by the result of a closure ascending', function () { - expect(PureEnum::sortBy(fn (PureEnum $case) => $case->shape())) + expect(PureEnum::sortBy(fn(PureEnum $case) => $case->shape())) ->toBeInstanceOf(CasesCollection::class) - ->cases() - ->toBe([PureEnum::three, PureEnum::two, PureEnum::one]); + ->all() + ->toBe([2 => PureEnum::three, 1 => PureEnum::two, 0 => PureEnum::one]); }); it('retrieves a collection of cases sorted by the result of a closure descending', function () { - expect(PureEnum::sortDescBy(fn (PureEnum $case) => $case->shape())) + expect(PureEnum::sortByDesc(fn(PureEnum $case) => $case->shape())) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([PureEnum::one, PureEnum::two, PureEnum::three]); }); @@ -260,7 +302,7 @@ }); it('retrieves the case hydrated from a value') - ->expect(fn (string $value, PureEnum $case) => PureEnum::from($value) === $case) + ->expect(fn(string $value, PureEnum $case) => PureEnum::from($value) === $case) ->toBeTrue() ->with([ ['one', PureEnum::one], @@ -268,11 +310,11 @@ ['three', PureEnum::three], ]); -it('throws a value error when hydrating cases with an invalid value', fn () => PureEnum::from('1')) +it('throws a value error when hydrating cases with an invalid value', fn() => PureEnum::from('1')) ->throws(ValueError::class, '"1" is not a valid name for enum "Cerbero\Enum\PureEnum"'); it('retrieves the case hydrated from a value or returns null') - ->expect(fn (string $value, ?PureEnum $case) => PureEnum::tryFrom($value) === $case) + ->expect(fn(string $value, ?PureEnum $case) => PureEnum::tryFrom($value) === $case) ->toBeTrue() ->not->toThrow(ValueError::class) ->with([ @@ -283,7 +325,7 @@ ]); it('retrieves the case hydrated from a name') - ->expect(fn (string $name, PureEnum $case) => PureEnum::fromName($name) === $case) + ->expect(fn(string $name, PureEnum $case) => PureEnum::fromName($name) === $case) ->toBeTrue() ->with([ ['one', PureEnum::one], @@ -291,11 +333,11 @@ ['three', PureEnum::three], ]); -it('throws a value error when hydrating cases with an invalid name', fn () => PureEnum::fromName('1')) +it('throws a value error when hydrating cases with an invalid name', fn() => PureEnum::fromName('1')) ->throws(ValueError::class, '"1" is not a valid name for enum "Cerbero\Enum\PureEnum"'); it('retrieves the case hydrated from a name or returns null') - ->expect(fn (string $name, ?PureEnum $case) => PureEnum::tryFromName($name) === $case) + ->expect(fn(string $name, ?PureEnum $case) => PureEnum::tryFromName($name) === $case) ->toBeTrue() ->not->toThrow(ValueError::class) ->with([ @@ -305,43 +347,101 @@ ['four', null], ]); -it('retrieves the cases hydrated from a key') - ->expect(fn (string $key, mixed $value, array $cases) => PureEnum::fromKey($key, $value)->cases() === $cases) +it('retrieves the cases hydrated from a meta') + ->expect(fn(string $meta, mixed $value, array $cases) => PureEnum::fromMeta($meta, $value)->all() === $cases) ->toBeTrue() ->with([ ['color', 'red', [PureEnum::one]], - ['name', 'three', [PureEnum::three]], + ['shape', 'circle', [PureEnum::three]], ['isOdd', true, [PureEnum::one, PureEnum::three]], ]); -it('retrieves the cases hydrated from a key using a closure') - ->expect(PureEnum::fromKey(fn (PureEnum $case) => $case->shape(), 'square')) +it('retrieves the cases hydrated from a meta using a closure') + ->expect(PureEnum::fromMeta('shape', fn(string $meta) => $meta == 'square')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([PureEnum::two]); -it('throws a value error when hydrating cases with an invalid key', fn () => PureEnum::fromKey('color', 'orange')) - ->throws(ValueError::class, 'Invalid value for the key "color" for enum "Cerbero\Enum\PureEnum"'); +it('throws a value error when hydrating cases with an invalid meta', fn() => PureEnum::fromMeta('color', 'orange')) + ->throws(ValueError::class, 'Invalid value for the meta "color" for enum "Cerbero\Enum\PureEnum"'); -it('retrieves the case hydrated from a key or returns null') - ->expect(fn (string $key, mixed $value, ?array $cases) => PureEnum::tryFromKey($key, $value)?->cases() === $cases) +it('retrieves the case hydrated from a meta or returns null') + ->expect(fn(string $meta, mixed $value, ?array $cases) => PureEnum::tryFromMeta($meta, $value)?->all() === $cases) ->toBeTrue() ->not->toThrow(ValueError::class) ->with([ ['color', 'red', [PureEnum::one]], - ['name', 'three', [PureEnum::three]], ['isOdd', true, [PureEnum::one, PureEnum::three]], ['shape', 'rectangle', null], ]); -it('attempts to retrieve the case hydrated from a key using a closure') - ->expect(PureEnum::tryFromKey(fn (PureEnum $case) => $case->shape(), 'square')) +it('attempts to retrieve the case hydrated from a meta using a closure') + ->expect(PureEnum::fromMeta('shape', fn(string $meta) => $meta == 'square')) ->toBeInstanceOf(CasesCollection::class) - ->cases() + ->all() ->toBe([PureEnum::two]); -it('retrieves the key of a case') - ->expect(fn (string $key, mixed $value) => PureEnum::one->get($key) === $value) +it('handles the call to an inaccessible enum method') + ->expect(PureEnum::one()) + ->toBe('one'); + +it('fails handling the call to an invalid enum method', fn() => PureEnum::four()) + ->throws(ValueError::class, '"four" is not a valid name for enum "Cerbero\Enum\PureEnum"'); + +it('runs custom logic when calling an inaccessible enum method', function() { + Enums::onStaticCall(function(string $enum, string $name, array $arguments) { + expect($enum)->toBe(PureEnum::class) + ->and($name)->toBe('unknownStaticMethod') + ->and($arguments)->toBe([1, 2, 3]); + + return 'ciao'; + }); + + expect(PureEnum::unknownStaticMethod(1, 2, 3))->toBe('ciao'); + + (fn() => self::$onStaticCall = null)->bindTo(null, Enums::class)(); +}); + +it('handles the call to an inaccessible case method', fn() => PureEnum::one->unknownMethod()) + ->throws(Error::class, '"unknownMethod" is not a valid meta for enum "Cerbero\Enum\PureEnum"'); + +it('runs custom logic when calling an inaccessible case method', function() { + Enums::onCall(function(object $case, string $name, array $arguments) { + expect($case)->toBeInstanceOf(PureEnum::class) + ->and($name)->toBe('unknownMethod') + ->and($arguments)->toBe([1, 2, 3]); + + return 'ciao'; + }); + + expect(PureEnum::one->unknownMethod(1, 2, 3))->toBe('ciao'); + + (fn() => self::$onCall = null)->bindTo(null, Enums::class)(); +}); + +it('handles the invocation of a case') + ->expect((PureEnum::one)()) + ->toBe('one'); + +it('runs custom logic when invocating a case', function() { + Enums::onInvoke(function(object $case, mixed ...$arguments) { + expect($case)->toBeInstanceOf(PureEnum::class) + ->and($arguments)->toBe([1, 2, 3]); + + return 'ciao'; + }); + + expect((PureEnum::one)(1, 2, 3))->toBe('ciao'); + + (fn() => self::$onInvoke = null)->bindTo(null, Enums::class)(); +}); + +it('retrieves the meta names of an enum', function() { + expect(PureEnum::metaNames())->toBe(['color', 'shape', 'isOdd']); +}); + +it('retrieves the item of a case') + ->expect(fn(string $item, mixed $value) => PureEnum::one->resolveItem($item) === $value) ->toBeTrue() ->with([ ['name', 'one'], @@ -349,9 +449,16 @@ ['shape', 'triangle'], ]); -it('retrieves the key of a case using a closure') - ->expect(PureEnum::one->get(fn (PureEnum $case) => $case->color())) +it('retrieves the item of a case using a closure') + ->expect(PureEnum::one->resolveItem(fn(PureEnum $case) => $case->color())) ->toBe('red'); -it('throws a value error when attempting to retrieve an invalid key', fn () => PureEnum::one->get('invalid')) - ->throws(ValueError::class, '"invalid" is not a valid key for enum "Cerbero\Enum\PureEnum"'); +it('throws a value error when attempting to retrieve an invalid item', fn() => PureEnum::one->resolveItem('invalid')) + ->throws(ValueError::class, '"invalid" is not a valid meta for enum "Cerbero\Enum\PureEnum"'); + +it('retrieves the value of a backed case or the name of a pure case', function() { + expect(PureEnum::one->value())->toBe('one'); +}); + +it('fails if a meta attribute does not have a name', fn() => InvalidMetaAttribute::metaNames()) + ->throws(InvalidArgumentException::class, 'The name of meta must be a string');