Skip to content
Merged
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
8 changes: 6 additions & 2 deletions .github/workflows/WP_6_6.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,9 @@ jobs:
environment_github: true
run: composer all

- name: Codecov
run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_REST }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_REST }}
files: ./clover.xml
fail_ci_if_error: false
8 changes: 6 additions & 2 deletions .github/workflows/WP_6_7.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,9 @@ jobs:
environment_github: true
run: composer all

- name: Codecov
run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_REST }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_REST }}
files: ./clover.xml
fail_ci_if_error: false
8 changes: 6 additions & 2 deletions .github/workflows/WP_6_8.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,9 @@ jobs:
environment_github: true
run: composer all

- name: Codecov
run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_REST }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_REST }}
files: ./clover.xml
fail_ci_if_error: false
8 changes: 6 additions & 2 deletions .github/workflows/WP_6_9.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,9 @@ jobs:
environment_github: true
run: composer all

- name: Codecov
run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_REST }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_REST }}
files: ./clover.xml
fail_ci_if_error: false
85 changes: 85 additions & 0 deletions .scrutinizer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
checks:
php:
code_rating: true
duplication: true
fix_php_opening_tag: false
remove_php_closing_tag: false
one_class_per_file: false
side_effects_or_types: false
no_mixed_inline_html: false
require_braces_around_control_structures: false
php5_style_constructor: false
no_global_keyword: false
avoid_usage_of_logical_operators: false
psr2_class_declaration: false
no_underscore_prefix_in_properties: false
no_underscore_prefix_in_methods: false
blank_line_after_namespace_declaration: false
single_namespace_per_use: false
psr2_switch_declaration: false
psr2_control_structure_declaration: false
avoid_superglobals: false
security_vulnerabilities: false
no_exit: false

build:
dependencies:
override:
- 'composer install --no-interaction --prefer-dist'
nodes:
analysis:
project_setup:
override:
- 'true'
tests:
override:
- php-scrutinizer-run

tools:
php_analyzer:
enabled: true
filter:
excluded_paths: ['tests/*', 'docs/*', 'template/*', 'node_modules/*', 'vendor/*']
config:
checkstyle:
enabled: true
naming:
isser_method_name: ^.*$
utility_class_name: ^.*$
doc_comment_fixes:
enabled: false
reflection_fixes:
enabled: false
use_statement_fixes:
enabled: false
simplify_boolean_return:
enabled: true
php_changetracking: true
php_cpd: true
php_cs_fixer: false
php_mess_detector: true
php_pdepend: true
sensiolabs_security_checker: true

filter:
paths:
- 'src/*'
excluded_paths:
- 'tests/*'
- 'docs/*'
- 'docs-gen/*'
- 'node_modules/*'
- 'vendor/*'
- 'template/*'

coding_style:
php:
indentation:
general:
use_tabs: true
size: 4
spaces:
before_parentheses:
closure_definition: true
around_operators:
concatenation: true
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ A fluent PHP builder for WordPress REST API JSON schemas. Converts verbose neste
[![WordPress 6.7 Test Suite [PHP8.0-8.4]](https://github.com/Pink-Crab/WP_Rest_Schema/actions/workflows/WP_6_7.yaml/badge.svg)](https://github.com/Pink-Crab/WP_Rest_Schema/actions/workflows/WP_6_7.yaml)
[![WordPress 6.8 Test Suite [PHP8.0-8.4]](https://github.com/Pink-Crab/WP_Rest_Schema/actions/workflows/WP_6_8.yaml/badge.svg)](https://github.com/Pink-Crab/WP_Rest_Schema/actions/workflows/WP_6_8.yaml)
[![WordPress 6.9 Test Suite [PHP8.0-8.4]](https://github.com/Pink-Crab/WP_Rest_Schema/actions/workflows/WP_6_9.yaml/badge.svg)](https://github.com/Pink-Crab/WP_Rest_Schema/actions/workflows/WP_6_9.yaml)
[![codecov](https://codecov.io/gh/Pink-Crab/WP_Rest_Schema/graph/badge.svg?token=KER9nANRi2)](https://codecov.io/gh/Pink-Crab/WP_Rest_Schema)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Pink-Crab/WP_Rest_Schema/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Pink-Crab/WP_Rest_Schema/?branch=master)

****

Expand Down Expand Up @@ -74,11 +76,13 @@ $ composer require pinkcrab/wp-rest-schema
| Boolean | `Boolean_Type` | *No type-specific methods* |
| Null | `Null_Type` | *No type-specific methods* |
| Array | `Array_Type` | `string_item()`, `integer_item()`, `min_items()`, `max_items()`, `unique_items()`, `any_of()`, `one_of()` |
| Object | `Object_Type` | `string_property()`, `integer_property()`, `additional_properties()`, `pattern_properties()`, `min_properties()`, `max_properties()` |
| Object | `Object_Type` | `string_property()`, `integer_property()`, `additional_properties()`, `pattern_properties()`, `min_properties()`, `max_properties()`, `required_properties()` |
| OneOf | `One_Of_Type` | `variant( Argument )` — schema-root `oneOf` combinator (exactly one variant must match) |
| AnyOf | `Any_Of_Type` | `variant( Argument )` — schema-root `anyOf` combinator (any variant matches) |

> `Boolean_Type` and `Null_Type` inherit all shared methods from the base `Argument` class but have no additional type-specific methods. See [Argument](docs/Argument.md) for the full shared API.

All types share a common set of methods for `description`, `default`, `required`, `readonly`, `title`, `format`, `expected` (enum), `context`, `validation`, `sanitization`, and `union types`. See [Argument (Base Class)](docs/Argument.md) for details.
All types share a common set of methods for `description`, `default`, `required`, `readonly`, `title`, `format`, `expected` (enum, accepts mixed), `context`, `validation`, `sanitization`, `arg_options` (WP controller pass-through), and `union types`. See [Argument (Base Class)](docs/Argument.md) for details.

****

Expand Down Expand Up @@ -211,5 +215,6 @@ http://www.opensource.org/licenses/mit-license.html

## Change Log

* 1.0.0 - Combinators (`One_Of_Type` / `Any_Of_Type`), null/falsy defaults, array items last-wins, `arg_options`, `required_properties`, mixed enums, `Schema::get_context_param()`, output-keys canary test; deprecated `name()` and `all_of()`.
* 1.0.0-RC1 - Added Schema builder, readonly/title support, field() alias, for_route() helper, additionalProperties redesign, FORMAT_URI fix, PHPStan level 9, WPCS 3, PHP 8.0+/WP 6.6+
* 0.1.0 - Initial version
77 changes: 72 additions & 5 deletions docs/Argument.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@ $arg = String_Type::field( 'value' )
// type becomes ['string', 'null']
```

### Combinators vs unions — `One_Of_Type` / `Any_Of_Type`

Use `union_with_type()` when the value can be one of several primitive types but shares the SAME attributes (`minLength`, `minimum`, etc.). Use `One_Of_Type` / `Any_Of_Type` when each variant needs its OWN distinct attributes.

**Union — shared attributes, multi-primitive type array:**

```php
String_Type::on( 'slug' )
->min_length( 3 )
->union_with_type( 'null' );

// Emits: ['slug' => ['type' => ['string','null'], 'minLength' => 3]]
```

Here `minLength: 3` applies to both members of the union.

**Combinator — distinct per-variant attributes:**

```php
One_Of_Type::on( 'thing' )
->variant( String_Type::on( 'thing' )->min_length( 3 ) )
->variant( Integer_Type::on( 'thing' )->minimum( 1 ) );

// Emits: ['thing' => ['oneOf' => [
// ['type' => 'string', 'minLength' => 3],
// ['type' => 'integer', 'minimum' => 1],
// ]]]
```

`minLength` only applies to the string variant; `minimum` only to the integer variant. Union can't express that.

`One_Of_Type` requires EXACTLY ONE variant to match (`rest_find_one_matching_schema`). `Any_Of_Type` accepts ANY variant that matches (`rest_find_any_matching_schema`). Both emit with no outer `type` — the combinator keyword sits at the schema root.

### `description( string $description ): self`

Set a human-readable description for the argument.
Expand All @@ -64,13 +97,27 @@ $arg = String_Type::field( 'email' )

### `default( mixed $default ): self`

Set a default value for the argument.
Set a default value for the argument. Accepts `string`, `int`, `float`, `bool`, `array`, `object`, or `null` — whatever matches the schema's declared `type` (or one of its union members).

```php
$arg = Integer_Type::field( 'page' )
->default( 1 );
```

Falsy and null values are fully supported — `default(false)`, `default(0)`, `default('')`, `default(null)` all count as explicit defaults and emit as `'default' => <value>` in the parsed schema. `has_default()` will return `true` after any of them.

```php
$flag = Boolean_Type::field( 'enabled' )->default( false );
// Emits: ['enabled' => ['type' => 'boolean', 'default' => false]]

$nullable = String_Type::field( 'slug' )
->union_with_type( Argument::TYPE_NULL )
->default( null );
// Emits: ['slug' => ['type' => ['string','null'], 'default' => null]]
```

An argument with no `default()` call will NOT emit a `default` key at all.

### `required( bool $required = true ): self`

Mark the argument as required. Defaults to `true` when called without a parameter.
Expand Down Expand Up @@ -109,10 +156,17 @@ $arg = String_Type::field( 'email' )

Set the allowed values (maps to `enum` in the schema). Multiple calls are additive.

Accepts any JSON-comparable value — strings, ints, floats, booleans, `null`, arrays, and objects — matching WP's `in_array(..., $enum, true)` semantics.

```php
$arg = String_Type::field( 'status' )
->expected( 'publish', 'draft', 'pending' );

// Null + string mix (for union schemas that include 'null'):
$arg = String_Type::field( 'slug' )
->union_with_type( 'null' )
->expected( 'home', 'about', null );

// Can also be called multiple times
$arg = String_Type::field( 'status' )
->expected( 'publish' )
Expand All @@ -128,15 +182,28 @@ $arg = Integer_Type::field( 'id' )
->context( 'view', 'edit', 'embed' );
```

### `name( string $name ): self`
### ~~`name( string $name ): self`~~ *(deprecated)*

> **Deprecated in 1.0.0** — calling `name()` triggers `E_USER_DEPRECATED` and does nothing.
>
> `name` was never a valid JSON Schema / WP REST keyword and was leaking from internal child-indexing into the parsed output. Removed from emission; the setter is kept as a no-op for backwards compatibility and will be removed in a future release. For user-facing display labels use [`title()`](#title-string-title-self) instead.

### `arg_options( array $options ): self`

Set a display name for the argument.
Attach a raw `arg_options` array to the argument. The array is emitted verbatim under the `arg_options` key in the parsed schema, letting WP REST controllers override `sanitize_callback` / `validate_callback` (or any other WP arg option) per property without replacing the callbacks on the Argument itself.

```php
$arg = String_Type::field( 'email' )
->name( 'Email Address' );
$arg = String_Type::field( 'slug' )
->arg_options(
array(
'sanitize_callback' => 'sanitize_title',
'validate_callback' => 'rest_validate_request_arg',
)
);
```

See `WP_REST_Controller::add_additional_fields_schema()` in WP core for how this merges into route args.

### `validation( callable $callback ): self`

Set a custom validation callback. Maps to `validate_callback` in the schema.
Expand Down
9 changes: 9 additions & 0 deletions docs/Array-Type.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ $custom = String_Type::field( 'value' )->min_length( 1 );
$list = Array_Type::field( 'items' )->item( $custom );
```

### Multiple item calls — last wins or combinator

WP treats `items` as a SINGLE schema applied to every element; JSON Schema tuple form is not honoured. The library therefore handles repeated `*_item()` calls as follows:

- **No combinator set (default `allOf`):** the LAST-added item wins and `items` is emitted as a single schema. Prior item calls are discarded.
- **Combinator set (`one_of()` / `any_of()`):** all item schemas are kept and emitted as a list under the combinator key, producing valid WP output such as `'items' => [ 'oneOf' => [ {schemaA}, {schemaB} ] ]`.

For heterogeneous arrays at the SCHEMA ROOT (not nested inside `items`), use [`One_Of_Type`](Argument.md#combinators-vs-unions--one_of_type--any_of_type) / [`Any_Of_Type`](Argument.md#combinators-vs-unions--one_of_type--any_of_type) instead.

---

## Constraint Methods
Expand Down
25 changes: 23 additions & 2 deletions docs/Object-Type.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,36 @@ $obj = Object_Type::field( 'settings' )
->max_properties( 10 );
```

### `required_properties( string ...$names ): self`

Mark specific property names as required at the parent-object level, producing the draft-4 style `required: ['a','b']` array used by most WP core `get_item_schema()` implementations.

```php
$obj = Object_Type::field( 'user' )
->integer_property( 'id' )
->string_property( 'email' )
->string_property( 'nickname' )
->required_properties( 'id', 'email' );

// Emits:
// ['user' => [
// 'type' => 'object',
// 'properties' => [ 'id' => [...], 'email' => [...], 'nickname' => [...] ],
// 'required' => [ 'id', 'email' ],
// ]]
```

Coexists with per-property `->required(true)` booleans — both forms are honoured by WP and may appear side-by-side if both are set.

---

## Element Relationships

Control how properties relate to each other. The default relationship is `allOf`.

### `all_of(): self`
### ~~`all_of(): self`~~ *(deprecated)*

All property schemas must match (default).
> **Deprecated in 1.0.0** — no-op. `allOf` is already the default relationship, and WP's REST validator does not honour `allOf` as a combinator anyway. Use `one_of()` / `any_of()` or the root-level [`One_Of_Type` / `Any_Of_Type`](Argument.md#combinators-vs-unions--one_of_type--any_of_type) classes instead.

### `any_of(): self`

Expand Down
33 changes: 33 additions & 0 deletions docs/Schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,39 @@ $schema = Schema::on( 'settings' )
->additional_properties_schema( String_Type::on( 'value' ) );
```

### `required_properties( string ...$names ): self`

Mark property names as required at the parent-object level. Forwards to [`Object_Type::required_properties()`](Object-Type.md#required_properties-string-names-self). Emits a draft-4 style `required: ['a','b']` array in the parsed schema.

```php
$schema = Schema::on( 'user' )
->integer_property( 'id' )
->string_property( 'email' )
->required_properties( 'id', 'email' );
```

### `get_context_param( array $args = array() ): array`

Build the `context` collection param descriptor, mirroring `WP_REST_Controller::get_context_param()`. Derives `enum` from the union of `context` values set on the schema's properties (unique, reverse-sorted) and returns the standard param shape.

```php
$schema = Schema::on( 'post' )
->integer_property( 'id', fn( $p ) => $p->context( 'view', 'edit', 'embed' ) )
->string_property( 'title', fn( $p ) => $p->context( 'view', 'edit' ) );

$param = $schema->get_context_param();
// [
// 'description' => 'Scope under which the request is made; ...',
// 'type' => 'string',
// 'sanitize_callback' => 'sanitize_key',
// 'validate_callback' => 'rest_validate_request_arg',
// 'enum' => ['view', 'embed', 'edit'],
// ]

// With overrides:
$param = $schema->get_context_param( array( 'default' => 'view' ) );
```

### `to_array(): array`

Convert the schema to the array format WordPress expects.
Expand Down
Loading
Loading