diff --git a/includes/abilities-api/class-wp-ability-category.php b/includes/abilities-api/class-wp-ability-category.php index 957ce5e..a40ea8e 100644 --- a/includes/abilities-api/class-wp-ability-category.php +++ b/includes/abilities-api/class-wp-ability-category.php @@ -18,7 +18,7 @@ * * @see WP_Ability_Categories_Registry */ -final class WP_Ability_Category { +final class WP_Ability_Category implements \JsonSerializable { /** * The unique slug for the ability category. @@ -192,6 +192,56 @@ public function get_meta(): array { return $this->meta; } + /** + * Converts the category to an array representation. + * + * Returns a complete array representation of the category including slug, label, + * description, and metadata. + * + * @since n.e.x.t + * + * @return array { + * The category as an associative array. + * + * @type string $slug The unique category slug. + * @type string $label The human-readable label. + * @type string $description The detailed description. + * @type array $meta Optional metadata for the category. + * } + */ + public function to_array(): array { + $array = array( + 'slug' => $this->get_slug(), + 'label' => $this->get_label(), + 'description' => $this->get_description(), + 'meta' => $this->get_meta(), + ); + + /** + * Filters the array representation of a category. + * + * @since n.e.x.t + * + * @param array $array The category as an associative array. + * @param \WP_Ability_Category $category The category instance. + */ + return apply_filters( "wp_ability_category_{$this->get_slug()}_to_array", $array, $this ); + } + + /** + * Serializes the category to a value that can be serialized natively by json_encode(). + * + * Implements the JsonSerializable interface to allow the category to be passed + * directly to json_encode() without manually calling to_array(). + * + * @since n.e.x.t + * + * @return array The category as an associative array. + */ + public function jsonSerialize(): array { + return $this->to_array(); + } + /** * Wakeup magic method. * diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index d116080..2f52b15 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -18,7 +18,7 @@ * * @see WP_Abilities_Registry */ -class WP_Ability { +class WP_Ability implements \JsonSerializable { /** * The default value for the `show_in_rest` meta. @@ -370,17 +370,6 @@ public function get_description(): string { return $this->description; } - /** - * Retrieves the ability category for the ability. - * - * @since 6.9.0 - * - * @return string The ability category for the ability. - */ - public function get_category(): string { - return $this->category; - } - /** * Retrieves the input schema for the ability. * @@ -452,6 +441,134 @@ public function normalize_input( $input = null ) { return null; } + /** + * Retrieves the category for the ability. + * + * @since n.e.x.t + * + * @return string The category for the ability. + */ + public function get_category(): string { + return $this->category; + } + + /** + * Converts the ability to an array representation. + * + * Returns a complete array representation of the ability including name, label, + * description, schemas, and metadata. Callbacks are excluded as they are not serializable. + * + * @since n.e.x.t + * + * @return array { + * The ability as an associative array. + * + * @type string $name The ability name with namespace. + * @type string $label The human-readable label. + * @type string $description The detailed description. + * @type array $input_schema The input validation schema. + * @type array $output_schema The output validation schema. + * @type array $meta { + * Metadata for the ability. May contain additional custom keys beyond those documented below. + * + * @type array $annotations { + * Behavior annotations. + * + * @type string $instructions Usage instructions. + * @type bool $readonly Whether the ability is read-only. + * @type bool $destructive Whether the ability is destructive. + * @type bool $idempotent Whether the ability is idempotent. + * } + * @type bool $show_in_rest Whether the ability is exposed in REST API. + * @type mixed ...$0 Additional custom metadata keys. + * } + * } + */ + public function to_array(): array { + $array = array( + 'name' => $this->get_name(), + 'label' => $this->get_label(), + 'description' => $this->get_description(), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'meta' => $this->get_meta(), + ); + + return $array; + } + + /** + * Serializes the ability to a value that can be serialized natively by json_encode(). + * + * Implements the JsonSerializable interface to allow the ability to be passed + * directly to json_encode() without manually calling to_array(). + * + * @since n.e.x.t + * + * @return array The ability as an associative array. + */ + public function jsonSerialize(): array { + return $this->to_array(); + } + + /** + * Converts the ability to a JSON Schema representation. + * + * Generates a JSON Schema Draft 4 compliant schema describing the ability's + * structure, including input/output schemas and metadata. + * + * @since n.e.x.t + * + * @return array A JSON Schema representation of the ability. + */ + public function to_json_schema(): array { + $input_schema = $this->get_input_schema(); + $output_schema = $this->get_output_schema(); + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'title' => $this->get_label(), + 'description' => $this->get_description(), + 'properties' => array( + 'name' => array( + 'type' => 'string', + 'enum' => array( $this->get_name() ), + ), + 'meta' => array( + 'type' => 'object', + 'properties' => array( + 'annotations' => array( + 'type' => 'object', + 'properties' => array( + 'instructions' => array( 'type' => 'string' ), + 'readonly' => array( 'type' => 'boolean' ), + 'destructive' => array( 'type' => 'boolean' ), + 'idempotent' => array( 'type' => 'boolean' ), + ), + ), + 'show_in_rest' => array( + 'type' => 'boolean', + ), + ), + ), + ), + 'required' => array( 'name', 'meta' ), + ); + + if ( ! empty( $input_schema ) ) { + $schema['properties']['input_schema'] = $input_schema; + $schema['required'][] = 'input_schema'; + } + + if ( ! empty( $output_schema ) ) { + $schema['properties']['output_schema'] = $output_schema; + $schema['required'][] = 'output_schema'; + } + + return $schema; + } + /** * Validates input data against the input schema. * diff --git a/tests/unit/abilities-api/wpAbility.php b/tests/unit/abilities-api/wpAbility.php index 73a5fbf..debd4e4 100644 --- a/tests/unit/abilities-api/wpAbility.php +++ b/tests/unit/abilities-api/wpAbility.php @@ -778,4 +778,363 @@ public function test_after_action_not_fired_on_output_validation_error() { $this->assertFalse( $after_action_fired, 'after_execute_ability action should not be fired when output validation fails' ); $this->assertInstanceOf( WP_Error::class, $result, 'Should return WP_Error for output validation failure' ); } + + /** + * Tests that to_array() returns correct structure with all expected keys. + */ + public function test_to_array_returns_correct_structure() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $array = $ability->to_array(); + + $this->assertIsArray( $array, 'to_array() should return an array' ); + $this->assertArrayHasKey( 'name', $array, 'Array should contain name key' ); + $this->assertArrayHasKey( 'label', $array, 'Array should contain label key' ); + $this->assertArrayHasKey( 'description', $array, 'Array should contain description key' ); + $this->assertArrayHasKey( 'input_schema', $array, 'Array should contain input_schema key' ); + $this->assertArrayHasKey( 'output_schema', $array, 'Array should contain output_schema key' ); + $this->assertArrayHasKey( 'meta', $array, 'Array should contain meta key' ); + + $this->assertSame( self::$test_ability_name, $array['name'], 'Name should match' ); + $this->assertSame( 'Calculator', $array['label'], 'Label should match' ); + $this->assertSame( 'Calculates the result of math operations.', $array['description'], 'Description should match' ); + } + + /** + * Tests that to_array() does not include callbacks. + */ + public function test_to_array_excludes_callbacks() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $array = $ability->to_array(); + + $this->assertArrayNotHasKey( 'execute_callback', $array, 'Array should not contain execute_callback' ); + $this->assertArrayNotHasKey( 'permission_callback', $array, 'Array should not contain permission_callback' ); + } + + /** + * Tests to_array() with both input and output schemas. + */ + public function test_to_array_with_full_schemas() { + $args = array_merge( + self::$test_ability_properties, + array( + 'input_schema' => array( + 'type' => 'integer', + 'description' => 'Test input.', + ), + ) + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $array = $ability->to_array(); + + $this->assertIsArray( $array['input_schema'], 'Input schema should be an array' ); + $this->assertSame( 'integer', $array['input_schema']['type'], 'Input schema type should match' ); + $this->assertIsArray( $array['output_schema'], 'Output schema should be an array' ); + $this->assertSame( 'number', $array['output_schema']['type'], 'Output schema type should match' ); + } + + /** + * Tests to_array() without input schema. + */ + public function test_to_array_without_input_schema() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $array = $ability->to_array(); + + $this->assertArrayHasKey( 'input_schema', $array, 'input_schema key should exist' ); + $this->assertIsArray( $array['input_schema'], 'input_schema should be an array' ); + $this->assertEmpty( $array['input_schema'], 'input_schema should be empty' ); + } + + /** + * Tests to_array() meta structure. + */ + public function test_to_array_meta_structure() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $array = $ability->to_array(); + + $this->assertIsArray( $array['meta'], 'Meta should be an array' ); + $this->assertArrayHasKey( 'annotations', $array['meta'], 'Meta should contain annotations' ); + $this->assertArrayHasKey( 'show_in_rest', $array['meta'], 'Meta should contain show_in_rest' ); + + $this->assertIsArray( $array['meta']['annotations'], 'Annotations should be an array' ); + $this->assertArrayHasKey( 'readonly', $array['meta']['annotations'], 'Annotations should contain readonly' ); + $this->assertArrayHasKey( 'destructive', $array['meta']['annotations'], 'Annotations should contain destructive' ); + $this->assertArrayHasKey( 'idempotent', $array['meta']['annotations'], 'Annotations should contain idempotent' ); + $this->assertArrayHasKey( 'instructions', $array['meta']['annotations'], 'Annotations should contain instructions' ); + + $this->assertTrue( $array['meta']['annotations']['readonly'], 'Readonly should be true' ); + $this->assertFalse( $array['meta']['annotations']['destructive'], 'Destructive should be false' ); + $this->assertFalse( $array['meta']['show_in_rest'], 'show_in_rest should default to false' ); + } + + /** + * Tests to_array() with custom meta properties. + */ + public function test_to_array_with_custom_meta() { + $args = array_merge( + self::$test_ability_properties, + array( + 'meta' => array( + 'custom_property' => 'custom_value', + 'another_prop' => 123, + ), + ) + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $array = $ability->to_array(); + + $this->assertArrayHasKey( 'custom_property', $array['meta'], 'Custom meta property should be included' ); + $this->assertSame( 'custom_value', $array['meta']['custom_property'], 'Custom meta value should match' ); + $this->assertArrayHasKey( 'another_prop', $array['meta'], 'Another custom meta property should be included' ); + $this->assertSame( 123, $array['meta']['another_prop'], 'Another custom meta value should match' ); + } + + /** + * Tests that to_json_schema() returns valid JSON Schema structure. + */ + public function test_to_json_schema_returns_valid_structure() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $schema = $ability->to_json_schema(); + + $this->assertIsArray( $schema, 'to_json_schema() should return an array' ); + $this->assertArrayHasKey( '$schema', $schema, 'Schema should contain $schema key' ); + $this->assertSame( 'http://json-schema.org/draft-04/schema#', $schema['$schema'], 'Schema version should be Draft 4' ); + $this->assertArrayHasKey( 'type', $schema, 'Schema should contain type key' ); + $this->assertSame( 'object', $schema['type'], 'Schema type should be object' ); + $this->assertArrayHasKey( 'title', $schema, 'Schema should contain title key' ); + $this->assertSame( 'Calculator', $schema['title'], 'Schema title should match label' ); + $this->assertArrayHasKey( 'description', $schema, 'Schema should contain description key' ); + $this->assertSame( 'Calculates the result of math operations.', $schema['description'], 'Schema description should match' ); + } + + /** + * Tests to_json_schema() with both input and output schemas. + */ + public function test_to_json_schema_with_both_schemas() { + $args = array_merge( + self::$test_ability_properties, + array( + 'input_schema' => array( + 'type' => 'integer', + 'description' => 'Test input.', + ), + ) + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $schema = $ability->to_json_schema(); + + $this->assertArrayHasKey( 'properties', $schema, 'Schema should contain properties' ); + $this->assertArrayHasKey( 'input_schema', $schema['properties'], 'Properties should contain input_schema' ); + $this->assertArrayHasKey( 'output_schema', $schema['properties'], 'Properties should contain output_schema' ); + + $this->assertSame( 'integer', $schema['properties']['input_schema']['type'], 'Input schema should be preserved' ); + $this->assertSame( 'number', $schema['properties']['output_schema']['type'], 'Output schema should be preserved' ); + + $this->assertContains( 'input_schema', $schema['required'], 'input_schema should be in required array' ); + $this->assertContains( 'output_schema', $schema['required'], 'output_schema should be in required array' ); + } + + /** + * Tests to_json_schema() without input schema. + */ + public function test_to_json_schema_without_input_schema() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $schema = $ability->to_json_schema(); + + $this->assertArrayNotHasKey( 'input_schema', $schema['properties'], 'Properties should not contain input_schema when not defined' ); + $this->assertNotContains( 'input_schema', $schema['required'], 'input_schema should not be in required array when not defined' ); + $this->assertArrayHasKey( 'output_schema', $schema['properties'], 'Properties should contain output_schema' ); + $this->assertContains( 'output_schema', $schema['required'], 'output_schema should be in required array' ); + } + + /** + * Tests to_json_schema() meta structure. + */ + public function test_to_json_schema_meta_structure() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $schema = $ability->to_json_schema(); + + $this->assertArrayHasKey( 'meta', $schema['properties'], 'Properties should contain meta' ); + $this->assertSame( 'object', $schema['properties']['meta']['type'], 'Meta type should be object' ); + $this->assertArrayHasKey( 'properties', $schema['properties']['meta'], 'Meta should have properties' ); + $this->assertArrayHasKey( 'annotations', $schema['properties']['meta']['properties'], 'Meta properties should contain annotations' ); + $this->assertArrayHasKey( 'show_in_rest', $schema['properties']['meta']['properties'], 'Meta properties should contain show_in_rest' ); + + $annotations = $schema['properties']['meta']['properties']['annotations']; + $this->assertSame( 'object', $annotations['type'], 'Annotations type should be object' ); + $this->assertArrayHasKey( 'properties', $annotations, 'Annotations should have properties' ); + $this->assertArrayHasKey( 'readonly', $annotations['properties'], 'Annotations properties should contain readonly' ); + $this->assertArrayHasKey( 'destructive', $annotations['properties'], 'Annotations properties should contain destructive' ); + $this->assertArrayHasKey( 'idempotent', $annotations['properties'], 'Annotations properties should contain idempotent' ); + $this->assertArrayHasKey( 'instructions', $annotations['properties'], 'Annotations properties should contain instructions' ); + + $this->assertSame( 'boolean', $schema['properties']['meta']['properties']['show_in_rest']['type'], 'show_in_rest type should be boolean' ); + } + + /** + * Tests to_json_schema() name property uses enum for constant value. + */ + public function test_to_json_schema_name_is_constant() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $schema = $ability->to_json_schema(); + + $this->assertArrayHasKey( 'name', $schema['properties'], 'Properties should contain name' ); + $this->assertArrayHasKey( 'enum', $schema['properties']['name'], 'Name should have enum keyword' ); + $this->assertIsArray( $schema['properties']['name']['enum'], 'Name enum should be an array' ); + $this->assertCount( 1, $schema['properties']['name']['enum'], 'Name enum should have exactly one value' ); + $this->assertSame( self::$test_ability_name, $schema['properties']['name']['enum'][0], 'Name enum value should match ability name' ); + $this->assertContains( 'name', $schema['required'], 'name should be in required array' ); + } + + /** + * Tests that to_array() filter is applied. + */ + public function test_to_array_filter() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $filter_callback = static function ( $array, $ability_instance ) { + $array['custom_field'] = 'custom_value'; + return $array; + }; + + add_filter( 'wp_ability_test/calculator_to_array', $filter_callback, 10, 2 ); + + $array = $ability->to_array(); + + remove_filter( 'wp_ability_test/calculator_to_array', $filter_callback ); + + $this->assertArrayHasKey( 'custom_field', $array, 'Filtered array should contain custom field' ); + $this->assertSame( 'custom_value', $array['custom_field'], 'Custom field value should match' ); + } + + /** + * Tests that to_json_schema() filter is applied. + */ + public function test_to_json_schema_filter() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $filter_callback = static function ( $schema, $ability_instance ) { + $schema['custom_property'] = 'custom_schema_value'; + return $schema; + }; + + add_filter( 'wp_ability_test/calculator_to_json_schema', $filter_callback, 10, 2 ); + + $schema = $ability->to_json_schema(); + + remove_filter( 'wp_ability_test/calculator_to_json_schema', $filter_callback ); + + $this->assertArrayHasKey( 'custom_property', $schema, 'Filtered schema should contain custom property' ); + $this->assertSame( 'custom_schema_value', $schema['custom_property'], 'Custom property value should match' ); + } + + /** + * Tests that to_array() filter receives ability instance as second parameter. + */ + public function test_to_array_filter_receives_ability_instance() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $received_ability = null; + + $filter_callback = static function ( $array, $ability_instance ) use ( &$received_ability ) { + $received_ability = $ability_instance; + return $array; + }; + + add_filter( 'wp_ability_test/calculator_to_array', $filter_callback, 10, 2 ); + + $ability->to_array(); + + remove_filter( 'wp_ability_test/calculator_to_array', $filter_callback ); + + $this->assertInstanceOf( WP_Ability::class, $received_ability, 'Filter should receive WP_Ability instance' ); + $this->assertSame( self::$test_ability_name, $received_ability->get_name(), 'Received ability should match' ); + } + + /** + * Tests that to_json_schema() filter receives ability instance as second parameter. + */ + public function test_to_json_schema_filter_receives_ability_instance() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $received_ability = null; + + $filter_callback = static function ( $schema, $ability_instance ) use ( &$received_ability ) { + $received_ability = $ability_instance; + return $schema; + }; + + add_filter( 'wp_ability_test/calculator_to_json_schema', $filter_callback, 10, 2 ); + + $ability->to_json_schema(); + + remove_filter( 'wp_ability_test/calculator_to_json_schema', $filter_callback ); + + $this->assertInstanceOf( WP_Ability::class, $received_ability, 'Filter should receive WP_Ability instance' ); + $this->assertSame( self::$test_ability_name, $received_ability->get_name(), 'Received ability should match' ); + } + + /** + * Tests that WP_Ability implements JsonSerializable. + */ + public function test_implements_json_serializable() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertInstanceOf( JsonSerializable::class, $ability, 'WP_Ability should implement JsonSerializable' ); + } + + /** + * Tests that json_encode() works with WP_Ability. + */ + public function test_json_encode() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $json = json_encode( $ability ); + + $this->assertIsString( $json, 'json_encode should return a string' ); + $this->assertNotFalse( $json, 'json_encode should not fail' ); + + $decoded = json_decode( $json, true ); + + $this->assertIsArray( $decoded, 'Decoded JSON should be an array' ); + $this->assertArrayHasKey( 'name', $decoded, 'Decoded array should contain name' ); + $this->assertSame( self::$test_ability_name, $decoded['name'], 'Name should match' ); + $this->assertArrayHasKey( 'label', $decoded, 'Decoded array should contain label' ); + $this->assertArrayHasKey( 'description', $decoded, 'Decoded array should contain description' ); + $this->assertArrayHasKey( 'meta', $decoded, 'Decoded array should contain meta' ); + } + + /** + * Tests that jsonSerialize() returns same data as to_array(). + */ + public function test_json_serialize_matches_to_array() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $array = $ability->to_array(); + $json_serialize = $ability->jsonSerialize(); + + $this->assertSame( $array, $json_serialize, 'jsonSerialize() should return the same data as to_array()' ); + } + + /** + * Tests that json_encode() applies to_array() filter. + */ + public function test_json_encode_applies_filter() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $filter_callback = static function ( $array, $ability_instance ) { + $array['filtered'] = true; + return $array; + }; + + add_filter( 'wp_ability_test/calculator_to_array', $filter_callback, 10, 2 ); + + $json = json_encode( $ability ); + $decoded = json_decode( $json, true ); + + remove_filter( 'wp_ability_test/calculator_to_array', $filter_callback ); + + $this->assertArrayHasKey( 'filtered', $decoded, 'json_encode should apply to_array filter' ); + $this->assertTrue( $decoded['filtered'], 'Filtered value should be present' ); + } } diff --git a/tests/unit/abilities-api/wpAbilityCategoryRegistry.php b/tests/unit/abilities-api/wpAbilityCategoryRegistry.php index c517ad6..1ca4b0c 100644 --- a/tests/unit/abilities-api/wpAbilityCategoryRegistry.php +++ b/tests/unit/abilities-api/wpAbilityCategoryRegistry.php @@ -705,6 +705,268 @@ public function test_register_category_with_unknown_property(): void { } /** + * Test category to_array() returns correct structure. + */ + public function test_category_to_array_structure(): void { + $category = $this->register_category_during_hook( + 'test-math', + array( + 'label' => 'Math', + 'description' => 'Mathematical operations.', + ) + ); + + $array = $category->to_array(); + + $this->assertIsArray( $array ); + $this->assertArrayHasKey( 'slug', $array ); + $this->assertArrayHasKey( 'label', $array ); + $this->assertArrayHasKey( 'description', $array ); + $this->assertArrayHasKey( 'meta', $array ); + + $this->assertSame( 'test-math', $array['slug'] ); + $this->assertSame( 'Math', $array['label'] ); + $this->assertSame( 'Mathematical operations.', $array['description'] ); + $this->assertSame( array(), $array['meta'] ); + } + + /** + * Test category to_array() includes meta. + */ + public function test_category_to_array_with_meta(): void { + $meta = array( + 'icon' => 'dashicons-calculator', + 'priority' => 10, + 'custom' => array( 'key' => 'value' ), + ); + + $category = $this->register_category_during_hook( + 'test-meta', + array( + 'label' => 'Math', + 'description' => 'Mathematical operations.', + 'meta' => $meta, + ) + ); + + $array = $category->to_array(); + + $this->assertIsArray( $array ); + $this->assertArrayHasKey( 'meta', $array ); + $this->assertSame( $meta, $array['meta'] ); + $this->assertSame( 'dashicons-calculator', $array['meta']['icon'] ); + $this->assertSame( 10, $array['meta']['priority'] ); + $this->assertSame( array( 'key' => 'value' ), $array['meta']['custom'] ); + } + + /** + * Test category to_array() filter is applied. + */ + public function test_category_to_array_filter(): void { + $category = $this->register_category_during_hook( + 'test-filter', + array( + 'label' => 'Math', + 'description' => 'Mathematical operations.', + ) + ); + + add_filter( + 'wp_ability_category_test-filter_to_array', + static function ( $array ) { + $array['custom_field'] = 'custom_value'; + return $array; + } + ); + + $array = $category->to_array(); + + $this->assertArrayHasKey( 'custom_field', $array ); + $this->assertSame( 'custom_value', $array['custom_field'] ); + } + + /** + * Test category to_array() filter receives correct parameters. + */ + public function test_category_to_array_filter_parameters(): void { + $category = $this->register_category_during_hook( + 'test-params', + array( + 'label' => 'System', + 'description' => 'System operations.', + ) + ); + + $filter_called = false; + $received_array = null; + $received_object = null; + + add_filter( + 'wp_ability_category_test-params_to_array', + static function ( $array, $cat ) use ( &$filter_called, &$received_array, &$received_object ) { + $filter_called = true; + $received_array = $array; + $received_object = $cat; + return $array; + }, + 10, + 2 + ); + + $category->to_array(); + + $this->assertTrue( $filter_called, 'Filter should have been called' ); + $this->assertIsArray( $received_array, 'First parameter should be an array' ); + $this->assertInstanceOf( WP_Ability_Category::class, $received_object, 'Second parameter should be WP_Ability_Category instance' ); + $this->assertSame( $category, $received_object, 'Second parameter should be the same category instance' ); + } + + /** + * Test category to_array() filter can modify all fields. + */ + public function test_category_to_array_filter_modify_fields(): void { + $category = $this->register_category_during_hook( + 'test-modify', + array( + 'label' => 'Original', + 'description' => 'Original description.', + 'meta' => array( 'foo' => 'bar' ), + ) + ); + + add_filter( + 'wp_ability_category_test-modify_to_array', + static function ( $array ) { + $array['label'] = 'Modified Label'; + $array['description'] = 'Modified Description'; + $array['meta']['new_field'] = 'new_value'; + unset( $array['meta']['foo'] ); + return $array; + } + ); + + $array = $category->to_array(); + + $this->assertSame( 'Modified Label', $array['label'] ); + $this->assertSame( 'Modified Description', $array['description'] ); + $this->assertArrayHasKey( 'new_field', $array['meta'] ); + $this->assertSame( 'new_value', $array['meta']['new_field'] ); + $this->assertArrayNotHasKey( 'foo', $array['meta'] ); + } + + /** + * Test category implements JsonSerializable interface. + */ + public function test_category_implements_json_serializable(): void { + $category = $this->register_category_during_hook( + 'test-json', + array( + 'label' => 'Math', + 'description' => 'Mathematical operations.', + ) + ); + + $this->assertInstanceOf( \JsonSerializable::class, $category ); + } + + /** + * Test category can be json_encode()'d directly. + */ + public function test_category_json_encode(): void { + $category = $this->register_category_during_hook( + 'test-encode', + array( + 'label' => 'Math', + 'description' => 'Mathematical operations.', + 'meta' => array( 'icon' => 'dashicons-calculator' ), + ) + ); + + $json = json_encode( $category ); + $this->assertNotFalse( $json, 'json_encode should not fail' ); + + $decoded = json_decode( $json, true ); + $this->assertIsArray( $decoded ); + $this->assertSame( 'test-encode', $decoded['slug'] ); + $this->assertSame( 'Math', $decoded['label'] ); + $this->assertSame( 'Mathematical operations.', $decoded['description'] ); + $this->assertSame( array( 'icon' => 'dashicons-calculator' ), $decoded['meta'] ); + } + + /** + * Test category jsonSerialize() returns same as to_array(). + */ + public function test_category_json_serialize_matches_to_array(): void { + $category = $this->register_category_during_hook( + 'test-match', + array( + 'label' => 'System', + 'description' => 'System operations.', + 'meta' => array( 'priority' => 5 ), + ) + ); + + $to_array_result = $category->to_array(); + $json_serialize = $category->jsonSerialize(); + + $this->assertSame( $to_array_result, $json_serialize, 'jsonSerialize() should return same result as to_array()' ); + } + + /** + * Test category jsonSerialize() respects filters. + */ + public function test_category_json_serialize_respects_filters(): void { + $category = $this->register_category_during_hook( + 'test-json-filter', + array( + 'label' => 'Math', + 'description' => 'Mathematical operations.', + ) + ); + + add_filter( + 'wp_ability_category_test-json-filter_to_array', + static function ( $array ) { + $array['filtered'] = true; + return $array; + } + ); + + $json = json_encode( $category ); + $decoded = json_decode( $json, true ); + + $this->assertArrayHasKey( 'filtered', $decoded ); + $this->assertTrue( $decoded['filtered'] ); + } + + /** + * Test category to_array() with complex nested meta. + */ + public function test_category_to_array_with_nested_meta(): void { + $meta = array( + 'level1' => array( + 'level2' => array( + 'level3' => 'deep value', + 'array' => array( 1, 2, 3 ), + ), + ), + 'simple' => 'value', + ); + + $category = $this->register_category_during_hook( + 'test-nested', + array( + 'label' => 'Complex', + 'description' => 'Complex meta structure.', + 'meta' => $meta, + ) + ); + + $array = $category->to_array(); + + $this->assertSame( $meta, $array['meta'] ); + $this->assertSame( 'deep value', $array['meta']['level1']['level2']['level3'] ); + $this->assertSame( array( 1, 2, 3 ), $array['meta']['level1']['level2']['array'] ); * Test category registry singleton. * * @ticket 64098