diff --git a/docs/1.intro.md b/docs/1.intro.md index 5b9b327..c270a16 100644 --- a/docs/1.intro.md +++ b/docs/1.intro.md @@ -77,8 +77,10 @@ function my_plugin_register_ability(){ 'permission_callback' => function( $input ) { return current_user_can( 'manage_options' ); }, - 'show_in_rest' => true, - )); + 'meta' => array( + 'show_in_rest' => true, + ), + ) ); } ``` diff --git a/docs/2.getting-started.md b/docs/2.getting-started.md index f024301..73be8fd 100644 --- a/docs/2.getting-started.md +++ b/docs/2.getting-started.md @@ -122,9 +122,8 @@ function my_plugin_register_abilities() { 'execute_callback' => 'my_plugin_get_site_title', 'permission_callback' => '__return_true', // Everyone can access this 'meta' => array( - 'category' => 'site-info', - ), - 'show_in_rest' => true, // Optional: expose via REST API + 'category' => 'site-info', + 'show_in_rest' => true, // Optional: expose via REST API ) ); } diff --git a/docs/3.registering-abilities.md b/docs/3.registering-abilities.md index 04d491a..fd8e57f 100644 --- a/docs/3.registering-abilities.md +++ b/docs/3.registering-abilities.md @@ -27,10 +27,10 @@ The `$args` array accepts the following keys: - The callback receives one optional argument: it can have any type as defined in the input schema (e.g., `array`, `object`, `string`, etc.). - 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. -- `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. - `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the ability. + - `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. ## Ability ID Convention diff --git a/docs/5.rest-api.md b/docs/5.rest-api.md index da3546a..7953dd8 100644 --- a/docs/5.rest-api.md +++ b/docs/5.rest-api.md @@ -8,12 +8,12 @@ Access to all Abilities REST API endpoints requires an authenticated user (see t ## Controlling REST API Exposure -By default, registered abilities are **not** exposed via the REST API. You can control whether an individual ability appears in the REST API by using the `show_in_rest` argument when registering the ability: +By default, registered abilities are **not** exposed via the REST API. You can control whether an individual ability appears in the REST API by using the `show_in_rest` meta when registering the ability: - `show_in_rest => true`: The ability is listed in REST API responses and can be executed via REST endpoints. - `show_in_rest => false` (default): The ability is hidden from REST API listings and cannot be executed via REST endpoints. The ability remains available for internal PHP usage via `wp_execute_ability()`. -Abilities with `show_in_rest => false` will return a `rest_ability_not_found` error if accessed via REST endpoints. +Abilities with meta `show_in_rest => false` will return a `rest_ability_not_found` error if accessed via REST endpoints. ## Schema diff --git a/includes/abilities-api.php b/includes/abilities-api.php index fd68e50..6d6c3c3 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`, `show_in_rest`, and `ability_class`. + * `permission_callback`, `annotations`, `meta`, and `ability_class`. * @return ?\WP_Ability An instance of registered ability on success, null on failure. * * @phpstan-param array{ @@ -35,9 +35,11 @@ * permission_callback?: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, - * annotations?: array, - * meta?: array, - * show_in_rest?: bool, + * annotations?: array, + * meta?: array{ + * show_in_rest?: bool, + * ..., + * }, * 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 3161727..f3c2503 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -57,9 +57,11 @@ final class WP_Abilities_Registry { * permission_callback?: callable( mixed $input= ): (bool|\WP_Error), * input_schema?: array, * output_schema?: array, - * annotations?: array, - * meta?: array, - * show_in_rest?: bool, + * annotations?: array, + * meta?: array{ + * show_in_rest?: bool, + * ... + * }, * 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 e4b6839..fc422f0 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -19,6 +19,15 @@ * @see WP_Abilities_Registry */ class WP_Ability { + + /** + * The default value for the `show_in_rest` meta. + * + * @since n.e.x.t + * @var bool + */ + protected const DEFAULT_SHOW_IN_REST = false; + /** * The default ability annotations. * They are not guaranteed to provide a faithful description of ability behavior. @@ -114,15 +123,7 @@ class WP_Ability { * @since 0.1.0 * @var array */ - protected $meta = array(); - - /** - * Whether to show the ability in the REST API. - * - * @since n.e.x.t - * @var bool - */ - protected $show_in_rest = false; + protected $meta; /** * Constructor. @@ -138,7 +139,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`, `meta`, and `show_in_rest`. + * `permission_callback`, `annotations`, and `meta`. */ public function __construct( string $name, array $args ) { $this->name = $name; @@ -186,9 +187,11 @@ 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, - * show_in_rest?: bool, + * annotations?: array, + * meta?: array{ + * show_in_rest?: bool, + * ... + * }, * ..., * } $args */ @@ -243,17 +246,23 @@ protected function prepare_properties( array $args ): array { ); } - if ( isset( $args['show_in_rest'] ) && ! is_bool( $args['show_in_rest'] ) ) { + if ( isset( $args['meta']['show_in_rest'] ) && ! is_bool( $args['meta']['show_in_rest'] ) ) { throw new \InvalidArgumentException( - esc_html__( 'The ability properties should provide a valid `show_in_rest` boolean.' ) + esc_html__( 'The ability meta should provide a valid `show_in_rest` boolean.' ) ); } - // Set defaults for optional args. + // Set defaults for optional meta. $args['annotations'] = wp_parse_args( $args['annotations'] ?? array(), static::$default_annotations ); + $args['meta'] = wp_parse_args( + $args['meta'] ?? array(), + array( + 'show_in_rest' => self::DEFAULT_SHOW_IN_REST, + ) + ); return $args; } @@ -337,14 +346,16 @@ public function get_meta(): array { } /** - * Checks whether the ability should be shown in the REST API. + * Retrieves a specific metadata item for the ability. * * @since n.e.x.t * - * @return bool True if the ability should be shown in the REST API, false otherwise. + * @param string $key The metadata key to retrieve. + * @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. */ - public function show_in_rest(): bool { - return $this->show_in_rest; + public function get_meta_item( string $key, $default_value = null ) { + return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value; } /** 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 8e43729..7ab279e 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 @@ -97,7 +97,7 @@ public function get_items( $request ) { $abilities = array_filter( wp_get_abilities(), static function ( $ability ) { - return $ability->show_in_rest(); + return $ability->get_meta_item( 'show_in_rest' ); } ); @@ -155,7 +155,7 @@ static function ( $ability ) { */ public function get_item( $request ) { $ability = wp_get_ability( $request->get_param( 'name' ) ); - if ( ! $ability || ! $ability->show_in_rest() ) { + if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) { return new \WP_Error( 'rest_ability_not_found', __( 'Ability not found.' ), 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 76675d3..eabd02c 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 @@ -154,7 +154,7 @@ public function run_ability( $request ) { */ public function run_ability_permissions_check( $request ) { $ability = wp_get_ability( $request->get_param( 'name' ) ); - if ( ! $ability || ! $ability->show_in_rest() ) { + if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) { return new \WP_Error( 'rest_ability_not_found', __( 'Ability not found.' ), diff --git a/tests/unit/abilities-api/wpAbilitiesRegistry.php b/tests/unit/abilities-api/wpAbilitiesRegistry.php index 85fbc57..139f834 100644 --- a/tests/unit/abilities-api/wpAbilitiesRegistry.php +++ b/tests/unit/abilities-api/wpAbilitiesRegistry.php @@ -60,9 +60,9 @@ public function set_up(): void { return true; }, 'meta' => array( - 'category' => 'math', + 'category' => 'math', + 'show_in_rest' => true, ), - 'show_in_rest' => true, ); } @@ -307,7 +307,7 @@ public function test_register_invalid_meta_type() { * @expectedIncorrectUsage WP_Abilities_Registry::register */ public function test_register_invalid_show_in_rest_type() { - self::$test_ability_args['show_in_rest'] = 5; + self::$test_ability_args['meta']['show_in_rest'] = 5; $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 2768df0..6b57157 100644 --- a/tests/unit/abilities-api/wpAbility.php +++ b/tests/unit/abilities-api/wpAbility.php @@ -117,27 +117,80 @@ public function test_annotations_throws_exception() { } /** - * Tests that `show_in_rest` defaults to false when not provided. + * Tests that getting non-existing metadata item returns default value. */ - public function test_show_in_rest_defaults_to_false() { + public function test_meta_get_non_existing_item_returns_default() { $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertFalse( $ability->show_in_rest(), '`show_in_rest` should default to false.' ); + $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. + */ + public function test_meta_show_in_rest_defaults_to_false() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertFalse( + $ability->get_meta_item( 'show_in_rest' ), + '`show_in_rest` metadata should default to false.' + ); } /** - * Tests that `show_in_rest` can be set to true. + * Tests that `show_in_rest` metadata can be set to true. */ - public function test_show_in_rest_can_be_set_to_true() { + public function test_meta_show_in_rest_can_be_set_to_true() { $args = array_merge( self::$test_ability_properties, array( - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + $ability = new WP_Ability( self::$test_ability_name, $args ); + + $this->assertTrue( + $ability->get_meta_item( 'show_in_rest' ), + '`show_in_rest` metadata should be true.' + ); + } + + /** + * 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->assertTrue( $ability->show_in_rest(), '`show_in_rest` should be true.' ); + $this->assertFalse( + $ability->get_meta_item( 'show_in_rest' ), + '`show_in_rest` metadata should be false.' + ); } /** @@ -147,12 +200,14 @@ public function test_show_in_rest_throws_exception() { $args = array_merge( self::$test_ability_properties, array( - 'show_in_rest' => 5, + 'meta' => array( + 'show_in_rest' => 5, + ), ) ); $this->expectException( InvalidArgumentException::class ); - $this->expectExceptionMessage( 'The ability properties should provide a valid `show_in_rest` boolean.' ); + $this->expectExceptionMessage( 'The ability meta should provide a valid `show_in_rest` boolean.' ); new WP_Ability( self::$test_ability_name, $args ); } diff --git a/tests/unit/abilities-api/wpRegisterAbility.php b/tests/unit/abilities-api/wpRegisterAbility.php index ecf0d48..8c37cb2 100644 --- a/tests/unit/abilities-api/wpRegisterAbility.php +++ b/tests/unit/abilities-api/wpRegisterAbility.php @@ -65,9 +65,9 @@ public function set_up(): void { 'destructive' => false, ), 'meta' => array( - 'category' => 'math', + 'category' => 'math', + 'show_in_rest' => true, ), - 'show_in_rest' => true, ); } @@ -147,8 +147,7 @@ public function test_register_valid_ability(): void { ), $result->get_annotations() ); - $this->assertSame( self::$test_ability_args['meta'], $result->get_meta() ); - $this->assertSame( self::$test_ability_args['show_in_rest'], $result->show_in_rest() ); + $this->assertEquals( self::$test_ability_args['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 6c16e0b..d476f78 100644 --- a/tests/unit/rest-api/wpRestAbilitiesListController.php +++ b/tests/unit/rest-api/wpRestAbilitiesListController.php @@ -122,9 +122,9 @@ private function register_test_abilities(): void { return current_user_can( 'read' ); }, 'meta' => array( - 'category' => 'math', + 'category' => 'math', + 'show_in_rest' => true, ), - 'show_in_rest' => true, ) ); @@ -168,9 +168,9 @@ private function register_test_abilities(): void { 'readonly' => true, ), 'meta' => array( - 'category' => 'system', + 'category' => 'system', + 'show_in_rest' => true, ), - 'show_in_rest' => true, ) ); @@ -198,7 +198,9 @@ private function register_test_abilities(): void { return "Result from ability {$i}"; }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); } @@ -472,7 +474,9 @@ public function test_ability_name_with_valid_special_characters(): void { return array( 'success' => true ); }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); diff --git a/tests/unit/rest-api/wpRestAbilitiesRunController.php b/tests/unit/rest-api/wpRestAbilitiesRunController.php index fda7282..0f86310 100644 --- a/tests/unit/rest-api/wpRestAbilitiesRunController.php +++ b/tests/unit/rest-api/wpRestAbilitiesRunController.php @@ -121,7 +121,9 @@ private function register_test_abilities(): void { 'permission_callback' => static function () { return current_user_can( 'edit_posts' ); }, - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -164,7 +166,9 @@ private function register_test_abilities(): void { 'annotations' => array( 'readonly' => true, ), - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -192,7 +196,9 @@ private function register_test_abilities(): void { // Only allow if secret matches return isset( $input['secret'] ) && 'valid_secret' === $input['secret']; }, - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -219,7 +225,9 @@ private function register_test_abilities(): void { return null; }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -233,7 +241,9 @@ private function register_test_abilities(): void { return new \WP_Error( 'test_error', 'This is a test error' ); }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -250,7 +260,9 @@ private function register_test_abilities(): void { return 'not a number'; // Invalid - schema expects number }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -274,7 +286,9 @@ private function register_test_abilities(): void { 'annotations' => array( 'readonly' => true, ), - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); } @@ -335,7 +349,9 @@ public function test_regular_ability_requires_post(): void { return 'success'; }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -627,7 +643,9 @@ public function test_output_validation_failure_returns_error(): void { return array( 'wrong_field' => 'value' ); }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -669,7 +687,9 @@ public function test_input_validation_failure_returns_error(): void { return array( 'status' => 'success' ); }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -704,7 +724,9 @@ public function test_ability_without_annotations_defaults_to_post_method(): void return array( 'executed' => true ); }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -738,7 +760,9 @@ public function test_empty_input_handling(): void { 'annotations' => array( 'readonly' => true, ), - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -751,7 +775,9 @@ public function test_empty_input_handling(): void { return array( 'input_was_empty' => 0 === func_num_args() ); }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -824,7 +850,9 @@ public function test_php_type_strings_in_input(): void { return array( 'echo' => $input ); }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -868,7 +896,9 @@ public function test_mixed_encoding_in_input(): void { return array( 'echo' => $input ); }, 'permission_callback' => '__return_true', - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) ); @@ -928,7 +958,9 @@ public function test_invalid_http_methods( string $method ): void { return array( 'success' => true ); }, 'permission_callback' => '__return_true', // No permission requirements - 'show_in_rest' => true, + 'meta' => array( + 'show_in_rest' => true, + ), ) );