From 1269cbdc86698099ff6bca09aedd6ff1bc8526c3 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 9 Oct 2025 13:57:52 +0200 Subject: [PATCH 1/5] Move `show_in_rest` to meta in the registration process --- includes/abilities-api.php | 5 +++++ .../abilities-api/class-wp-abilities-registry.php | 5 +++++ includes/abilities-api/class-wp-ability.php | 14 ++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/includes/abilities-api.php b/includes/abilities-api.php index 6d6c3c3..bef629e 100644 --- a/includes/abilities-api.php +++ b/includes/abilities-api.php @@ -35,11 +35,16 @@ * permission_callback?: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, +<<<<<<< HEAD * annotations?: array, * meta?: array{ * show_in_rest?: bool, * ..., * }, +======= + * annotations?: array, + * meta?: array, +>>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) * ability_class?: class-string<\WP_Ability>, * ... * } $args diff --git a/includes/abilities-api/class-wp-abilities-registry.php b/includes/abilities-api/class-wp-abilities-registry.php index f3c2503..83d5d6b 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -57,11 +57,16 @@ final class WP_Abilities_Registry { * permission_callback?: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, +<<<<<<< HEAD * annotations?: array, * meta?: array{ * show_in_rest?: bool, * ... * }, +======= + * annotations?: array, + * meta?: array, +>>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) * ability_class?: class-string<\WP_Ability>, * ... * } $args diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index fc422f0..bcef2f2 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -123,7 +123,11 @@ class WP_Ability { * @since 0.1.0 * @var array */ +<<<<<<< HEAD protected $meta; +======= + protected $meta = array(); +>>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) /** * Constructor. @@ -187,11 +191,16 @@ public function __construct( string $name, array $args ) { * permission_callback: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, +<<<<<<< HEAD * annotations?: array, * meta?: array{ * show_in_rest?: bool, * ... * }, +======= + * annotations?: array, + * meta?: array, +>>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) * ..., * } $args */ @@ -354,8 +363,13 @@ public function get_meta(): array { * @param mixed $default_value Optional. The default value to return if the metadata item is not found. Default `null`. * @return mixed The value of the metadata item, or the default value if not found. */ +<<<<<<< HEAD public function get_meta_item( string $key, $default_value = null ) { return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value; +======= + public function show_in_rest(): bool { + return $this->meta['show_in_rest'] ?? false; +>>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) } /** From 2e0532d7fe8c2fe04e6f030055f22f152c2b83ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Fri, 10 Oct 2025 07:06:50 +0200 Subject: [PATCH 2/5] Update includes/abilities-api/class-wp-ability.php Co-authored-by: Dovid Levine --- includes/abilities-api.php | 5 ----- .../abilities-api/class-wp-abilities-registry.php | 5 ----- includes/abilities-api/class-wp-ability.php | 14 -------------- 3 files changed, 24 deletions(-) diff --git a/includes/abilities-api.php b/includes/abilities-api.php index bef629e..6d6c3c3 100644 --- a/includes/abilities-api.php +++ b/includes/abilities-api.php @@ -35,16 +35,11 @@ * permission_callback?: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, -<<<<<<< HEAD * annotations?: array, * meta?: array{ * show_in_rest?: bool, * ..., * }, -======= - * annotations?: array, - * meta?: array, ->>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) * ability_class?: class-string<\WP_Ability>, * ... * } $args diff --git a/includes/abilities-api/class-wp-abilities-registry.php b/includes/abilities-api/class-wp-abilities-registry.php index 83d5d6b..f3c2503 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -57,16 +57,11 @@ final class WP_Abilities_Registry { * permission_callback?: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, -<<<<<<< HEAD * annotations?: array, * meta?: array{ * show_in_rest?: bool, * ... * }, -======= - * annotations?: array, - * meta?: array, ->>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) * ability_class?: class-string<\WP_Ability>, * ... * } $args diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index bcef2f2..fc422f0 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -123,11 +123,7 @@ class WP_Ability { * @since 0.1.0 * @var array */ -<<<<<<< HEAD protected $meta; -======= - protected $meta = array(); ->>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) /** * Constructor. @@ -191,16 +187,11 @@ public function __construct( string $name, array $args ) { * permission_callback: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, -<<<<<<< HEAD * annotations?: array, * meta?: array{ * show_in_rest?: bool, * ... * }, -======= - * annotations?: array, - * meta?: array, ->>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) * ..., * } $args */ @@ -363,13 +354,8 @@ public function get_meta(): array { * @param mixed $default_value Optional. The default value to return if the metadata item is not found. Default `null`. * @return mixed The value of the metadata item, or the default value if not found. */ -<<<<<<< HEAD public function get_meta_item( string $key, $default_value = null ) { return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value; -======= - public function show_in_rest(): bool { - return $this->meta['show_in_rest'] ?? false; ->>>>>>> 583da4c (Move `show_in_rest` to meta in the registration process) } /** From 11febabd34f8ed6fd9c0d51ef72615dfe2998268 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 10 Oct 2025 07:33:30 +0200 Subject: [PATCH 3/5] Replace `show_in_rest()` with `get_meta_item` --- tests/unit/abilities-api/wpAbility.php | 58 +++++++++++++------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/unit/abilities-api/wpAbility.php b/tests/unit/abilities-api/wpAbility.php index 6b57157..5209e09 100644 --- a/tests/unit/abilities-api/wpAbility.php +++ b/tests/unit/abilities-api/wpAbility.php @@ -39,10 +39,35 @@ public function set_up(): void { ); } + /** + * Tests that getting non-existing metadata item returns default value. + */ + public function test_meta_get_non_existing_item_returns_default() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertNull( + $ability->get_meta_item( 'non_existing' ), + 'Non-existing metadata item should return null.' + ); + } + + /** + * Tests that getting non-existing metadata item with custom default returns that default. + */ + public function test_meta_get_non_existing_item_with_custom_default() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertSame( + 'default_value', + $ability->get_meta_item( 'non_existing', 'default_value' ), + 'Non-existing metadata item should return custom default value.' + ); + } + /** * Tests getting all annotations when selective overrides are applied. */ - public function test_get_all_annotations() { + public function test_get_merged_annotations_from_meta() { $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); $this->assertEquals( @@ -60,7 +85,7 @@ public function test_get_all_annotations() { /** * Tests getting default annotations when not provided. */ - public function test_get_default_annotations() { + public function test_get_default_annotations_from_meta() { $args = self::$test_ability_properties; unset( $args['annotations'] ); @@ -80,7 +105,7 @@ public function test_get_default_annotations() { /** * Tests getting all annotations when values overridden. */ - public function test_get_all_annotations_overridden() { + public function test_get_overridden_annotations_from_meta() { $annotations = array( 'instructions' => 'Enjoy responsibly.', 'readonly' => true, @@ -102,7 +127,7 @@ public function test_get_all_annotations_overridden() { /** * Tests that invalid `annotations` value throws an exception. */ - public function test_annotations_throws_exception() { + public function test_annotations_from_meta_throws_exception() { $args = array_merge( self::$test_ability_properties, array( @@ -116,31 +141,6 @@ public function test_annotations_throws_exception() { new WP_Ability( self::$test_ability_name, $args ); } - /** - * Tests that getting non-existing metadata item returns default value. - */ - public function test_meta_get_non_existing_item_returns_default() { - $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); - - $this->assertNull( - $ability->get_meta_item( 'non_existing' ), - 'Non-existing metadata item should return null.' - ); - } - - /** - * Tests that getting non-existing metadata item with custom default returns that default. - */ - public function test_meta_get_non_existing_item_with_custom_default() { - $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); - - $this->assertSame( - 'default_value', - $ability->get_meta_item( 'non_existing', 'default_value' ), - 'Non-existing metadata item should return custom default value.' - ); - } - /** * Tests that `show_in_rest` metadata defaults to false when not provided. */ From 287313e44627e8122dce3b5fe7117c2b5c5b4d19 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 10 Oct 2025 07:04:29 +0200 Subject: [PATCH 4/5] Move `annotations` to meta in the registration process --- includes/abilities-api.php | 4 +- .../class-wp-abilities-registry.php | 2 +- includes/abilities-api/class-wp-ability.php | 42 +++++----------- ...lass-wp-rest-abilities-list-controller.php | 1 - ...class-wp-rest-abilities-run-controller.php | 2 +- packages/client/src/__tests__/api.test.ts | 8 ++- packages/client/src/api.ts | 2 +- .../src/store/__tests__/reducer.test.ts | 6 +-- packages/client/src/store/reducer.ts | 1 - packages/client/src/types.ts | 17 +++---- .../abilities-api/wpAbilitiesRegistry.php | 2 +- tests/unit/abilities-api/wpAbility.php | 49 ++++++++++++++----- .../unit/abilities-api/wpRegisterAbility.php | 35 +++++++------ .../wpRestAbilitiesListController.php | 6 +-- .../rest-api/wpRestAbilitiesRunController.php | 18 +++---- 15 files changed, 101 insertions(+), 94 deletions(-) diff --git a/includes/abilities-api.php b/includes/abilities-api.php index 6d6c3c3..9df0bfc 100644 --- a/includes/abilities-api.php +++ b/includes/abilities-api.php @@ -25,7 +25,7 @@ * alphanumeric characters, dashes and the forward slash. * @param array $args An associative array of arguments for the ability. This should include * `label`, `description`, `input_schema`, `output_schema`, `execute_callback`, - * `permission_callback`, `annotations`, `meta`, and `ability_class`. + * `permission_callback`, `meta`, and `ability_class`. * @return ?\WP_Ability An instance of registered ability on success, null on failure. * * @phpstan-param array{ @@ -35,8 +35,8 @@ * permission_callback?: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, - * annotations?: array, * meta?: array{ + * annotations?: array, * show_in_rest?: bool, * ..., * }, diff --git a/includes/abilities-api/class-wp-abilities-registry.php b/includes/abilities-api/class-wp-abilities-registry.php index f3c2503..bd2cf2b 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -57,8 +57,8 @@ final class WP_Abilities_Registry { * permission_callback?: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, - * annotations?: array, * meta?: array{ + * annotations?: array, * show_in_rest?: bool, * ... * }, diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index fc422f0..b9d686e 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -109,14 +109,6 @@ class WP_Ability { */ protected $permission_callback; - /** - * The ability annotations. - * - * @since n.e.x.t - * @var array - */ - protected $annotations = array(); - /** * The optional ability metadata. * @@ -139,7 +131,7 @@ class WP_Ability { * @param string $name The name of the ability, with its namespace. * @param array $args An associative array of arguments for the ability. This should include * `label`, `description`, `input_schema`, `output_schema`, `execute_callback`, - * `permission_callback`, `annotations`, and `meta`. + * `permission_callback`, and `meta`. */ public function __construct( string $name, array $args ) { $this->name = $name; @@ -187,8 +179,8 @@ public function __construct( string $name, array $args ) { * permission_callback: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, - * annotations?: array, * meta?: array{ + * annotations?: array, * show_in_rest?: bool, * ... * }, @@ -234,15 +226,15 @@ protected function prepare_properties( array $args ): array { ); } - if ( isset( $args['annotations'] ) && ! is_array( $args['annotations'] ) ) { + if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) { throw new \InvalidArgumentException( - esc_html__( 'The ability properties should provide a valid `annotations` array.' ) + esc_html__( 'The ability properties should provide a valid `meta` array.' ) ); } - if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) { + if ( isset( $args['meta']['annotations'] ) && ! is_array( $args['meta']['annotations'] ) ) { throw new \InvalidArgumentException( - esc_html__( 'The ability properties should provide a valid `meta` array.' ) + esc_html__( 'The ability meta should provide a valid `annotations` array.' ) ); } @@ -253,16 +245,17 @@ protected function prepare_properties( array $args ): array { } // Set defaults for optional meta. - $args['annotations'] = wp_parse_args( - $args['annotations'] ?? array(), - static::$default_annotations - ); - $args['meta'] = wp_parse_args( + $args['meta'] = wp_parse_args( $args['meta'] ?? array(), array( + 'annotations' => static::$default_annotations, 'show_in_rest' => self::DEFAULT_SHOW_IN_REST, ) ); + $args['meta']['annotations'] = wp_parse_args( + $args['meta']['annotations'], + static::$default_annotations + ); return $args; } @@ -323,17 +316,6 @@ public function get_output_schema(): array { return $this->output_schema; } - /** - * Retrieves the annotations for the ability. - * - * @since n.e.x.t - * - * @return array The annotations for the ability. - */ - public function get_annotations(): array { - return $this->annotations; - } - /** * Retrieves the metadata for the ability. * diff --git a/includes/rest-api/endpoints/class-wp-rest-abilities-list-controller.php b/includes/rest-api/endpoints/class-wp-rest-abilities-list-controller.php index 7ab279e..9f8216f 100644 --- a/includes/rest-api/endpoints/class-wp-rest-abilities-list-controller.php +++ b/includes/rest-api/endpoints/class-wp-rest-abilities-list-controller.php @@ -195,7 +195,6 @@ public function prepare_item_for_response( $ability, $request ) { 'description' => $ability->get_description(), 'input_schema' => $ability->get_input_schema(), 'output_schema' => $ability->get_output_schema(), - 'annotations' => $ability->get_annotations(), 'meta' => $ability->get_meta(), ); diff --git a/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php b/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php index eabd02c..5fdbfea 100644 --- a/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php +++ b/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php @@ -91,7 +91,7 @@ public function run_ability_with_method_check( $request ) { } // Check if the HTTP method matches the ability annotations. - $annotations = $ability->get_annotations(); + $annotations = $ability->get_meta_item( 'annotations' ); $is_readonly = ! empty( $annotations['readonly'] ); $method = $request->get_method(); diff --git a/packages/client/src/__tests__/api.test.ts b/packages/client/src/__tests__/api.test.ts index 678d354..1455328 100644 --- a/packages/client/src/__tests__/api.test.ts +++ b/packages/client/src/__tests__/api.test.ts @@ -264,7 +264,6 @@ describe( 'API functions', () => { name: 'test/read-only', label: 'Read-only Ability', description: 'Test read-only ability.', - annotations: { readonly: true }, input_schema: { type: 'object', properties: { @@ -273,6 +272,9 @@ describe( 'API functions', () => { }, }, output_schema: { type: 'object' }, + meta: { + annotations: { readonly: true }, + }, }; const mockGetAbility = jest.fn().mockResolvedValue( mockAbility ); @@ -300,9 +302,11 @@ describe( 'API functions', () => { name: 'test/read-only', label: 'Read-only Ability', description: 'Test read-only ability.', - annotations: { readonly: true }, input_schema: { type: 'object' }, output_schema: { type: 'object' }, + meta: { + annotations: { readonly: true }, + } }; const mockGetAbility = jest.fn().mockResolvedValue( mockAbility ); diff --git a/packages/client/src/api.ts b/packages/client/src/api.ts index 7de90e9..d56471d 100644 --- a/packages/client/src/api.ts +++ b/packages/client/src/api.ts @@ -176,7 +176,7 @@ async function executeServerAbility( ability: Ability, input: AbilityInput ): Promise< AbilityOutput > { - const method = !! ability.annotations?.readonly ? 'GET' : 'POST'; + const method = !! ability.meta?.annotations?.readonly ? 'GET' : 'POST'; let path = `/wp/v2/abilities/${ ability.name }/run`; const options: { diff --git a/packages/client/src/store/__tests__/reducer.test.ts b/packages/client/src/store/__tests__/reducer.test.ts index ccc8e0f..f3d65b0 100644 --- a/packages/client/src/store/__tests__/reducer.test.ts +++ b/packages/client/src/store/__tests__/reducer.test.ts @@ -126,8 +126,9 @@ describe( 'Store Reducer', () => { description: 'Full test ability.', input_schema: { type: 'object' }, output_schema: { type: 'object' }, - annotations: { readonly: true }, - meta: { category: 'test' }, + meta: { + category: 'test', + }, callback: () => Promise.resolve( {} ), permissionCallback: () => true, // Extra properties that should be filtered out @@ -154,7 +155,6 @@ describe( 'Store Reducer', () => { expect( ability.description ).toBe( 'Full test ability.' ); expect( ability.input_schema ).toEqual( { type: 'object' } ); expect( ability.output_schema ).toEqual( { type: 'object' } ); - expect( ability.annotations ).toEqual( { readonly: true } ); expect( ability.meta ).toEqual( { category: 'test' } ); expect( ability.callback ).toBeDefined(); expect( ability.permissionCallback ).toBeDefined(); diff --git a/packages/client/src/store/reducer.ts b/packages/client/src/store/reducer.ts index df2998d..8e05fc6 100644 --- a/packages/client/src/store/reducer.ts +++ b/packages/client/src/store/reducer.ts @@ -23,7 +23,6 @@ const ABILITY_KEYS = [ 'description', 'input_schema', 'output_schema', - 'annotations', 'meta', 'callback', 'permissionCallback', diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index ecbdd0b..5dd0466 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -68,22 +68,17 @@ export interface Ability { */ permissionCallback?: PermissionCallback; - /** - * Annotations for the ability. - * @see WP_Ability::get_annotations() - */ - annotations?: { - instructions?: string; - readonly?: boolean; - destructive?: boolean; - idempotent?: boolean; - }; - /** * Metadata about the ability. * @see WP_Ability::get_meta() */ meta?: { + annotations?: { + instructions?: string; + readonly?: boolean; + destructive?: boolean; + idempotent?: boolean; + }, [ key: string ]: any; }; } diff --git a/tests/unit/abilities-api/wpAbilitiesRegistry.php b/tests/unit/abilities-api/wpAbilitiesRegistry.php index 139f834..ba8d0dd 100644 --- a/tests/unit/abilities-api/wpAbilitiesRegistry.php +++ b/tests/unit/abilities-api/wpAbilitiesRegistry.php @@ -277,7 +277,7 @@ public function test_register_incorrect_output_schema_type() { * @expectedIncorrectUsage WP_Abilities_Registry::register */ public function test_register_invalid_annotations_type() { - self::$test_ability_args['annotations'] = false; + self::$test_ability_args['meta']['annotations'] = false; $result = $this->registry->register( self::$test_ability_name, self::$test_ability_args ); $this->assertNull( $result ); diff --git a/tests/unit/abilities-api/wpAbility.php b/tests/unit/abilities-api/wpAbility.php index 5209e09..e4720f4 100644 --- a/tests/unit/abilities-api/wpAbility.php +++ b/tests/unit/abilities-api/wpAbility.php @@ -26,15 +26,17 @@ public function set_up(): void { 'description' => 'The result of performing a math operation.', 'required' => true, ), - 'execute_callback' => static function (): int { + 'execute_callback' => static function (): int { return 0; }, 'permission_callback' => static function (): bool { return true; }, - 'annotations' => array( - 'readonly' => true, - 'destructive' => false, + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + ), ), ); } @@ -64,7 +66,7 @@ public function test_meta_get_non_existing_item_with_custom_default() { ); } - /** + /** * Tests getting all annotations when selective overrides are applied. */ public function test_get_merged_annotations_from_meta() { @@ -72,13 +74,13 @@ public function test_get_merged_annotations_from_meta() { $this->assertEquals( array_merge( - self::$test_ability_properties['annotations'], + self::$test_ability_properties['meta']['annotations'], array( 'instructions' => '', 'idempotent' => false, ), ), - $ability->get_annotations() + $ability->get_meta_item( 'annotations' ) ); } @@ -87,7 +89,7 @@ public function test_get_merged_annotations_from_meta() { */ public function test_get_default_annotations_from_meta() { $args = self::$test_ability_properties; - unset( $args['annotations'] ); + unset( $args['meta']['annotations'] ); $ability = new WP_Ability( self::$test_ability_name, $args ); @@ -98,7 +100,7 @@ public function test_get_default_annotations_from_meta() { 'destructive' => true, 'idempotent' => false, ), - $ability->get_annotations() + $ability->get_meta_item( 'annotations' ) ); } @@ -115,13 +117,15 @@ public function test_get_overridden_annotations_from_meta() { $args = array_merge( self::$test_ability_properties, array( - 'annotations' => $annotations, + 'meta' => array( + 'annotations' => $annotations, + ), ) ); $ability = new WP_Ability( self::$test_ability_name, $args ); - $this->assertSame( $annotations, $ability->get_annotations() ); + $this->assertSame( $annotations, $ability->get_meta_item( 'annotations' ) ); } /** @@ -131,12 +135,14 @@ public function test_annotations_from_meta_throws_exception() { $args = array_merge( self::$test_ability_properties, array( - 'annotations' => 5, + 'meta' => array( + 'annotations' => 5, + ), ) ); $this->expectException( InvalidArgumentException::class ); - $this->expectExceptionMessage( 'The ability properties should provide a valid `annotations` array.' ); + $this->expectExceptionMessage( 'The ability meta should provide a valid `annotations` array.' ); new WP_Ability( self::$test_ability_name, $args ); } @@ -193,6 +199,23 @@ public function test_show_in_rest_can_be_set_to_false() { ); } + /** + * Tests that `show_in_rest` can be set to false. + */ + public function test_show_in_rest_can_be_set_to_false() { + $args = array_merge( + self::$test_ability_properties, + array( + 'meta' => array( + 'show_in_rest' => false, + ), + ) + ); + $ability = new WP_Ability( self::$test_ability_name, $args ); + + $this->assertFalse( $ability->get_meta_item( 'show_in_rest' ), '`show_in_rest` metadata should be false.' ); + } + /** * Tests that invalid `show_in_rest` value throws an exception. */ diff --git a/tests/unit/abilities-api/wpRegisterAbility.php b/tests/unit/abilities-api/wpRegisterAbility.php index 8c37cb2..39df7de 100644 --- a/tests/unit/abilities-api/wpRegisterAbility.php +++ b/tests/unit/abilities-api/wpRegisterAbility.php @@ -60,11 +60,11 @@ public function set_up(): void { 'permission_callback' => static function (): bool { return true; }, - 'annotations' => array( - 'readonly' => true, - 'destructive' => false, - ), 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + ), 'category' => 'math', 'show_in_rest' => true, ), @@ -131,23 +131,28 @@ public function test_register_valid_ability(): void { $result = wp_register_ability( self::$test_ability_name, self::$test_ability_args ); + $expected_annotations = array_merge( + self::$test_ability_args['meta']['annotations'], + array( + 'instructions' => '', + 'idempotent' => false, + ), + ); + $expected_meta = array_merge( + self::$test_ability_args['meta'], + array( + 'annotations' => $expected_annotations, + 'show_in_rest' => true, + ) + ); + $this->assertInstanceOf( WP_Ability::class, $result ); $this->assertSame( self::$test_ability_name, $result->get_name() ); $this->assertSame( self::$test_ability_args['label'], $result->get_label() ); $this->assertSame( self::$test_ability_args['description'], $result->get_description() ); $this->assertSame( self::$test_ability_args['input_schema'], $result->get_input_schema() ); $this->assertSame( self::$test_ability_args['output_schema'], $result->get_output_schema() ); - $this->assertEquals( - array_merge( - array( - 'instructions' => '', - 'idempotent' => false, - ), - self::$test_ability_args['annotations'], - ), - $result->get_annotations() - ); - $this->assertEquals( self::$test_ability_args['meta'], $result->get_meta() ); + $this->assertEquals( $expected_meta, $result->get_meta() ); $this->assertTrue( $result->check_permissions( array( diff --git a/tests/unit/rest-api/wpRestAbilitiesListController.php b/tests/unit/rest-api/wpRestAbilitiesListController.php index d476f78..7d3a8a9 100644 --- a/tests/unit/rest-api/wpRestAbilitiesListController.php +++ b/tests/unit/rest-api/wpRestAbilitiesListController.php @@ -164,10 +164,10 @@ private function register_test_abilities(): void { 'permission_callback' => static function () { return current_user_can( 'read' ); }, - 'annotations' => array( - 'readonly' => true, - ), 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), 'category' => 'system', 'show_in_rest' => true, ), diff --git a/tests/unit/rest-api/wpRestAbilitiesRunController.php b/tests/unit/rest-api/wpRestAbilitiesRunController.php index 0f86310..8c0f319 100644 --- a/tests/unit/rest-api/wpRestAbilitiesRunController.php +++ b/tests/unit/rest-api/wpRestAbilitiesRunController.php @@ -163,10 +163,10 @@ private function register_test_abilities(): void { 'permission_callback' => static function () { return is_user_logged_in(); }, - 'annotations' => array( - 'readonly' => true, - ), 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), 'show_in_rest' => true, ), ) @@ -283,10 +283,10 @@ private function register_test_abilities(): void { return $input; }, 'permission_callback' => '__return_true', - 'annotations' => array( - 'readonly' => true, - ), 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), 'show_in_rest' => true, ), ) @@ -757,10 +757,10 @@ public function test_empty_input_handling(): void { return array( 'input_was_empty' => 0 === func_num_args() ); }, 'permission_callback' => '__return_true', - 'annotations' => array( - 'readonly' => true, - ), 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), 'show_in_rest' => true, ), ) From b2f6518fd22ddaca5c647b2df8ac4f954080b48b Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Wed, 8 Oct 2025 10:58:40 +0200 Subject: [PATCH 5/5] Docs: Expand information about new `annotations` property --- docs/3.registering-abilities.md | 21 +++++++++- docs/5.rest-api.md | 54 ++++++++++++++++++++++---- tests/unit/abilities-api/wpAbility.php | 17 -------- 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/docs/3.registering-abilities.md b/docs/3.registering-abilities.md index fd8e57f..e1250b3 100644 --- a/docs/3.registering-abilities.md +++ b/docs/3.registering-abilities.md @@ -28,6 +28,11 @@ The `$args` array accepts the following keys: - The callback should return a boolean (`true` if the user has permission, `false` otherwise), or a `WP_Error` object on failure. - If the input does not validate against the input schema, the permission callback will not be called, and a `WP_Error` will be returned instead. - `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the ability. + - `annotations` (`array`, **Optional**): An associative array of annotations providing hints about the ability's behavior characteristics. Supports the following keys: + - `instructions` (`string`, **Optional**): Custom instructions or guidance for using the ability (default: `''`). + - `readonly` (`boolean`, **Optional**): Whether the ability only reads data without modifying its environment (default: `false`). + - `destructive` (`boolean`, **Optional**): Whether the ability may perform destructive updates to its environment. If `true`, the ability may perform any type of modification, including deletions or other destructive changes. If `false`, the ability performs only additive updates (default: `true`). + - `idempotent` (`boolean`, **Optional**): Whether calling the ability repeatedly with the same arguments will have no additional effect on its environment (default: `false`). - `show_in_rest` (`boolean`, **Optional**): Whether to expose this ability via the REST API. Default: `false`. - When `true`, the ability will be listed in REST API responses and can be executed via REST endpoints. - When `false`, the ability will be hidden from REST API listings and cannot be executed via REST endpoints, but remains available for internal PHP usage. @@ -80,7 +85,13 @@ function my_plugin_register_site_info_ability() { 'url' => home_url() ); }, - 'permission_callback' => '__return_true' + 'permission_callback' => '__return_true', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false + ), + ), )); } ``` @@ -134,7 +145,13 @@ function my_plugin_register_update_option_ability() { }, 'permission_callback' => function() { return current_user_can( 'manage_options' ); - } + }, + 'meta' => array( + 'annotations' => array( + 'destructive' => false, + 'idempotent' => true + ), + ), )); } ``` diff --git a/docs/5.rest-api.md b/docs/5.rest-api.md index 7953dd8..f9ffd3f 100644 --- a/docs/5.rest-api.md +++ b/docs/5.rest-api.md @@ -42,7 +42,14 @@ Abilities are represented in JSON with the following structure: } } }, - "meta": {} + "meta": { + "annotations": { + "instructions": "", + "readonly": true, + "destructive": false, + "idempotent": false + } + } } ``` @@ -85,7 +92,14 @@ curl https://example.com/wp-json/wp/v2/abilities } } }, - "meta": {} + "meta": { + "annotations": { + "instructions": "", + "readonly": false, + "destructive": true, + "idempotent": false + } + } } ] ``` @@ -128,31 +142,55 @@ curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info } } }, - "meta": {} + "meta": { + "annotations": { + "instructions": "", + "readonly": true, + "destructive": false, + "idempotent": false + } + } } ``` ## Execute an Ability +Abilities are executed via the `/run` endpoint. The required HTTP method depends on the ability's `readonly` annotation: + +- **Read-only abilities** (`readonly: true`) must use **GET** +- **Regular abilities** (default) must use **POST** + +This distinction ensures read-only operations use safe HTTP methods that can be cached and don't modify server state. + ### Definition -`POST /wp/v2/abilities/(?P[a-z0-9-]+)/(?P[a-z0-9-]+)/run` +`GET|POST /wp/v2/abilities/(?P[a-z0-9-]+)/(?P[a-z0-9-]+)/run` ### Arguments - `namespace` _(string)_: The namespace part of the ability name. - `ability` _(string)_: The ability name part. - `input` _(integer|number|boolean|string|array|object|null)_: Optional input data for the ability as defined by its input schema. + - For **GET requests**: pass as `input` query parameter (URL-encoded JSON) + - For **POST requests**: pass in JSON body -### Example Request (No Input) +### Example Request (Read-only, GET) ```bash -curl -X POST https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info/run +# No input +curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info/run + +# With input (URL-encoded) +curl "https://example.com/wp-json/wp/v2/abilities/my-plugin/get-user-info/run?input=%7B%22user_id%22%3A1%7D" ``` -### Example Request (With Input) +### Example Request (Regular, POST) ```bash +# No input +curl -X POST https://example.com/wp-json/wp/v2/abilities/my-plugin/create-draft/run + +# With input curl -X POST \ -H "Content-Type: application/json" \ -d '{"input":{"option_name":"blogname","option_value":"New Site Name"}}' \ @@ -205,5 +243,5 @@ The API returns standard WordPress REST API error responses with these common co - `ability_invalid_output` - output validation failed according to the ability's schema. - `ability_invalid_execute_callback` - the ability's execute callback is not callable. - `rest_ability_not_found` - the requested ability is not registered. -- `rest_ability_invalid_method` - the requested HTTP method is not allowed for executing the selected ability. +- `rest_ability_invalid_method` - the requested HTTP method is not allowed for executing the selected ability (e.g., using GET on a read-only ability, or POST on a regular ability). - `rest_ability_cannot_execute` - the ability cannot be executed due to insufficient permissions. diff --git a/tests/unit/abilities-api/wpAbility.php b/tests/unit/abilities-api/wpAbility.php index e4720f4..8e439ff 100644 --- a/tests/unit/abilities-api/wpAbility.php +++ b/tests/unit/abilities-api/wpAbility.php @@ -199,23 +199,6 @@ public function test_show_in_rest_can_be_set_to_false() { ); } - /** - * Tests that `show_in_rest` can be set to false. - */ - public function test_show_in_rest_can_be_set_to_false() { - $args = array_merge( - self::$test_ability_properties, - array( - 'meta' => array( - 'show_in_rest' => false, - ), - ) - ); - $ability = new WP_Ability( self::$test_ability_name, $args ); - - $this->assertFalse( $ability->get_meta_item( 'show_in_rest' ), '`show_in_rest` metadata should be false.' ); - } - /** * Tests that invalid `show_in_rest` value throws an exception. */