Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat - add the has_shape method #1

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 10 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"platform": {
"php": "7.4"
},
"config": {
"platform": {
"php": "7.4.33"
}
},
"autoload": {
"psr-4": {
"StellarWP\\Arrays\\": "src/Arrays/"
Expand All @@ -27,7 +32,9 @@
}
],
"minimum-stability": "stable",
"require": {},
"require": {
"php": ">=7.4"
},
"require-dev": {
"codeception/module-asserts": "^1.0",
"codeception/module-cli": "^1.0",
Expand All @@ -42,7 +49,8 @@
"szepeviktor/phpstan-wordpress": "^1.1",
"symfony/event-dispatcher-contracts": "^2.5.1",
"symfony/string": "^5.4",
"saggre/phpdocumentor-markdown": "^0.1.3"
"saggre/phpdocumentor-markdown": "^0.1.3",
"illuminate/support": "^8.83"
},
"scripts": {
"create-docs": [
Expand Down
24 changes: 21 additions & 3 deletions docs/classes/StellarWP/Arrays/Arr.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ The sanitized array

**See Also:**

* https://gist.github.com/esthezia/5804445 -
* https://gist.github.com/esthezia/5804445 -

***

Expand Down Expand Up @@ -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. |

***

Expand Down Expand Up @@ -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 -

***

Expand Down Expand Up @@ -1415,7 +1433,7 @@ Integer position of first needle occurrence.

**See Also:**

* \StellarWP\Arrays\strpos() -
* \StellarWP\Arrays\strpos() -

***

Expand Down
64 changes: 57 additions & 7 deletions src/Arrays/Arr.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,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
*/
Expand Down Expand Up @@ -357,7 +357,7 @@ public static function filter_prefixed( array $array, string $prefix ): array {
public static function first( $array, callable $callback = null, $default = null ) {
if ( is_null( $callback ) ) {
if ( empty( $array ) ) {
return value( $default );
return $default;
}

foreach ( $array as $item ) {
Expand All @@ -371,7 +371,7 @@ public static function first( $array, callable $callback = null, $default = null
}
}

return value( $default );
return $default;
}

/**
Expand Down Expand Up @@ -543,7 +543,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
Expand Down Expand Up @@ -691,7 +691,7 @@ public static function join( $array, $glue, $finalGlue = '' ) {
*/
public static function last( $array, callable $callback = null, $default = null ) {
if ( is_null( $callback ) ) {
return empty( $array ) ? value( $default ) : end( $array );
return empty( $array ) ? $default : end( $array );
}

return static::first( array_reverse( $array, true ), $callback, $default );
Expand Down Expand Up @@ -942,7 +942,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;
Expand Down Expand Up @@ -1386,4 +1386,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<string|int,callable> $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;
}
}
131 changes: 128 additions & 3 deletions tests/wpunit/ArraysTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -766,10 +766,135 @@ 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 ) );
}

public function test_first(): void {
$this->assertEquals(
'lorem',
Arr::first(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'lorem', 'default')
);
$this->assertEquals(
'dolor',
Arr::first(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'dolor', 'default')
);
$this->assertEquals(
'default',
Arr::first(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'foo', 'default')
);
$this->assertEquals(
'default',
Arr::first(['ipsum', 'lorem', 'dolor'], fn($value) => str_starts_with($value,'p'), 'default')
);
$this->assertEquals(
'lorem',
Arr::first(['ipsum', 'lorem', 'dolor', 'loller'], fn($value) => str_starts_with($value,'l'), 'default')
);
}

public function test_last():void{
$this->assertEquals(
'lorem',
Arr::last(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'lorem', 'default')
);
$this->assertEquals(
'dolor',
Arr::last(['ipsum', 'dolor', 'lorem'], fn($value) => $value === 'dolor', 'default')
);
$this->assertEquals(
'default',
Arr::last(['ipsum', 'lorem', 'dolor'], fn($value) => $value === 'foo', 'default')
);
$this->assertEquals(
'default',
Arr::last(['ipsum', 'lorem', 'dolor'], fn($value) => str_starts_with($value,'p'), 'default')
);
$this->assertEquals(
'loller',
Arr::last(['ipsum', 'lorem', 'dolor', 'loller'], fn($value) => str_starts_with($value,'l'), 'default')
);
}
}
Loading