From cfa0ed9ee3b366ee8e919cba7726019eb3dd13a8 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Fri, 15 Mar 2024 18:02:22 +0100 Subject: [PATCH] feat - add the has_shape method --- README.md | 1 + docs/classes/StellarWP/Arrays/Arr.md | 24 +++++++- src/Arrays/Arr.php | 60 ++++++++++++++++++-- tests/wpunit/ArraysTest.php | 85 +++++++++++++++++++++++++++- 4 files changed, 160 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6739e77..2a5ad81 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ A library for array manipulations. * [get_first_set](/docs/classes/StellarWP/Arrays/Arr.md#get_first_set) * [get_in_any](/docs/classes/StellarWP/Arrays/Arr.md#get_in_any) * [has](/docs/classes/StellarWP/Arrays/Arr.md#has) + * [has_shape]/docs/classes/StellarWP/Arrays/Arr.md#has_shape) * [insert_after_key](/docs/classes/StellarWP/Arrays/Arr.md#insert_after_key) * [insert_before_key](/docs/classes/StellarWP/Arrays/Arr.md#insert_before_key) * [is_assoc](/docs/classes/StellarWP/Arrays/Arr.md#is_assoc) diff --git a/docs/classes/StellarWP/Arrays/Arr.md b/docs/classes/StellarWP/Arrays/Arr.md index 8a3cf0a..25b5d0f 100644 --- a/docs/classes/StellarWP/Arrays/Arr.md +++ b/docs/classes/StellarWP/Arrays/Arr.md @@ -274,7 +274,7 @@ The sanitized array **See Also:** -* https://gist.github.com/esthezia/5804445 - +* https://gist.github.com/esthezia/5804445 - *** @@ -604,7 +604,25 @@ public static has(\ArrayAccess|array $array, array|string|int|null $indexes): bo | `$indexes` | **array|string|int|null** | The indexes to search; in order the function will look from the first to the last. | +### has_shape +Check if an array has a specific shape. + +```php +public static has_shape(mixed $array, array $shape): bool +``` + +* This method is **static**. + + + + +**Parameters:** + +| Parameter | Type | Description | +|-----------|-----------|-----------------------------------------------------------------------------------| +| `$array` | **mixed** | The array to check. | +| `$shape` | **array** | The shape to check for. A map from keys to the callable or Closure to check them. | *** @@ -865,7 +883,7 @@ public merge_recursive(array& $array1, array& $array2): array **See Also:** -* http://php.net/manual/en/function.array-merge-recursive.php#92195 - +* http://php.net/manual/en/function.array-merge-recursive.php#92195 - *** @@ -1415,7 +1433,7 @@ Integer position of first needle occurrence. **See Also:** -* \StellarWP\Arrays\strpos() - +* \StellarWP\Arrays\strpos() - *** diff --git a/src/Arrays/Arr.php b/src/Arrays/Arr.php index 24303de..94688b6 100644 --- a/src/Arrays/Arr.php +++ b/src/Arrays/Arr.php @@ -3,8 +3,10 @@ namespace StellarWP\Arrays; use ArrayAccess; +use BadMethodCallException; use Illuminate\Support\Enumerable; use InvalidArgumentException; +use Throwable; /** * Array utilities @@ -299,8 +301,8 @@ public static function except( $array, $keys ) { /** * Determine if the given key exists in the provided array. * - * @param \ArrayAccess|array $array - * @param string|int|float $key + * @param ArrayAccess|array $array + * @param string|int|float $key * * @return bool */ @@ -543,7 +545,7 @@ public static function get_in_any( array $variables, $indexes, $default = null ) /** * Check if an item or items exist in an array using "dot" notation. * - * @param \ArrayAccess|array $array + * @param ArrayAccess|array $array * @param array|string|int|null $indexes The indexes to search; in order the function will look from the first to the last. * * @return bool @@ -942,7 +944,7 @@ public static function query( $array ) { * * @return mixed * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public static function random( $array, $number = null, $preserveKeys = false ) { $requested = is_null( $number ) ? 1 : $number; @@ -1386,4 +1388,54 @@ public static function wrap( $value ) { return is_array( $value ) ? $value : [ $value ]; } + + /** + * Checks if an array has a specific shape. + * + * @since TBD + * + * @param array $array The array to check. + * @param array $shape The shape to check for. Each key, either a string or an integer, + * maps to a callable that will be used to validate the value at that key. + * The callable must have the signature `fn( mixed $value ) :bool`. + * @param bool $strict Whether the array should only contain the keys specified in the shape. + * + * @return bool Whether the array has the specified shape. + */ + public static function has_shape( $array, array $shape, bool $strict = false ): bool { + if ( ! is_array( $array ) ) { + return false; + } + + if ( + $strict + && ( + array_intersect_key( $array, $shape ) !== $array + || + array_diff_key( $array, $shape ) !== [] + ) + ) { + return false; + } + + if ( count( array_intersect_key( $shape, $array ) ) < count( $shape ) ) { + return false; + } + + foreach ( $shape as $key => $check ) { + if ( ! is_callable( $check ) ) { + throw new \BadMethodCallException( 'The shape array must contain only callables as values.' ); + } + + try { + if ( ! $check( $array[ $key ] ) ) { + return false; + } + } catch ( \Throwable $th ) { + return false; + } + } + + return true; + } } diff --git a/tests/wpunit/ArraysTest.php b/tests/wpunit/ArraysTest.php index 078e725..94e9023 100644 --- a/tests/wpunit/ArraysTest.php +++ b/tests/wpunit/ArraysTest.php @@ -766,10 +766,89 @@ public function array_visit_recursive_data_provider() { ]; } + public function has_shape_data_provider(): array { + return [ + 'not an array' => [ 'foo', [], true, false ], + 'empty array, empty shape' => [ [], [], true, true ], + 'empty array, non-empty shape, strict' => [ + [], + [ 'foo' => 'is_string' ], + true, + false + ], + 'empty array, non-empty shape, non-strict' => [ + [], + [ 'foo' => 'is_string' ], + false, + false + ], + 'non-empty array, function shape, missing key, strict' => [ + [ 'foo' => 23 ], + [ 'bar' => 'is_string' ], + true, + false + ], + 'non-empty array, function shape, missing key, non-strict' => [ + [ 'foo' => 23 ], + [ 'bar' => 'is_string' ], + false, + false + ], + 'non-empty array, function shape, extra key, strict' => [ + [ 'foo' => 23, 'bar' => 'baz' ], + [ 'foo' => 'is_int' ], + true, + false + ], + 'non-empty array, function shape, extra key, non-strict' => [ + [ 'foo' => 23, 'bar' => 'baz' ], + [ 'foo' => 'is_int' ], + false, + true + ], + 'non-empty array, closure shape, all key fail failure, strict' => [ + [ 'foo' => 23, 'bar' => 89 ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + true, + false + ], + 'non-empty array, closure shape, all key fail failure, non-strict' => [ + [ 'foo' => 23, 'bar' => 89 ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + false, + false + ], + 'non-empty array, closure shape, all key pass, strict' => [ + [ 'foo' => 'hello', 'bar' => 'world' ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + true, + true + ], + 'non-empty array, closure shape, all key pass, non-strict ' => [ + [ 'foo' => 'hello', 'bar' => 'world' ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + false, + true + ], + 'non-empty array, closure shape, some key pass, strict' => [ + [ 'foo' => 'hello', 'bar' => 89 ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + true, + false + ], + 'non-empty array, closure shape, some key pass, non-strict' => [ + [ 'foo' => 'hello', 'bar' => 89 ], + [ 'foo' => fn( $foo ) => $foo === 'hello', 'bar' => fn( $bar ) => $bar === 'world' ], + false, + false + ], + ]; + } + /** - * @dataProvider array_visit_recursive_data_provider + * @dataProvider has_shape_data_provider */ - public function test_array_visit_recursive( $input, $visitor, $expected ) { - $this->assertEqualSets( $expected, Arr::array_visit_recursive( $input, $visitor ) ); + public function test_has_shape( $input, $shape, $strict, $expected ): void { + $this->assertEquals( $expected, Arr::has_shape( $input, $shape, $strict ) ); } }