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
58 changes: 56 additions & 2 deletions includes/abilities/class-scf-internal-post-type-abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,66 @@ private function get_entity_schema() {
$schema = $validator->load_schema( $this->schema_name() );

// Convert hook_name to camelCase for schema definition key (post_type → postType).
$def_key = lcfirst( str_replace( ' ', '', ucwords( $this->entity_name() ) ) );
$this->entity_schema = json_decode( wp_json_encode( $schema->definitions->$def_key ), true );
$def_key = lcfirst( str_replace( ' ', '', ucwords( $this->entity_name() ) ) );
$entity = json_decode( wp_json_encode( $schema->definitions->$def_key ), true );
$definitions = json_decode( wp_json_encode( $schema->definitions ), true );

// Resolve $ref references for WordPress Abilities API compatibility.
$this->entity_schema = $this->resolve_schema_refs( $entity, $definitions );
}
return $this->entity_schema;
}

/**
* Recursively resolves $ref references in a schema.
*
* WordPress Abilities API doesn't understand JSON Schema $ref,
* so we need to inline referenced definitions.
*
* @param array $schema The schema or schema fragment to process.
* @param array $definitions All available definitions from the schema.
* @return array Schema with $ref resolved.
*/
private function resolve_schema_refs( $schema, $definitions ) {
if ( ! is_array( $schema ) ) {
return $schema;
}

// If this is a $ref, resolve it.
if ( isset( $schema['$ref'] ) ) {
$ref = $schema['$ref'];
// Extract definition name from "#/definitions/name".
if ( preg_match( '#^\#/definitions/(.+)$#', $ref, $matches ) ) {
$def_name = $matches[1];
if ( isset( $definitions[ $def_name ] ) ) {
// Recursively resolve refs in the referenced definition.
return $this->resolve_schema_refs( $definitions[ $def_name ], $definitions );
}
}

// Log warning for unresolvable $ref.
_doing_it_wrong(
__METHOD__,
esc_html(
sprintf(
/* translators: %s: The unresolvable JSON Schema $ref value */
__( 'Could not resolve schema $ref: %s', 'secure-custom-fields' ),
$ref
)
),
'6.8.0'
);
return $schema;
}

// Recursively process all array elements.
foreach ( $schema as $key => $value ) {
$schema[ $key ] = $this->resolve_schema_refs( $value, $definitions );
}

return $schema;
}

/**
* Gets the SCF identifier schema.
*
Expand Down
122 changes: 122 additions & 0 deletions tests/php/includes/abilities/test-scf-internal-post-type-abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -1006,4 +1006,126 @@ public function test_list_callback_success() {
$this->assertIsArray( $result );
$this->assertCount( 2, $result );
}

// Schema $ref resolution tests.

/**
* Test resolve_schema_refs resolves simple $ref
*/
public function test_resolve_schema_refs_resolves_simple_ref() {
$reflection = new ReflectionClass( $this->abilities );
$method = $reflection->getMethod( 'resolve_schema_refs' );
$method->setAccessible( true );

$schema = array(
'$ref' => '#/definitions/testDef',
);

$definitions = array(
'testDef' => array(
'type' => 'object',
'properties' => array(
'name' => array( 'type' => 'string' ),
),
),
);

$result = $method->invoke( $this->abilities, $schema, $definitions );

$this->assertEquals( 'object', $result['type'] );
$this->assertArrayHasKey( 'properties', $result );
$this->assertArrayHasKey( 'name', $result['properties'] );
}

/**
* Test resolve_schema_refs resolves nested $ref (ref pointing to ref)
*/
public function test_resolve_schema_refs_resolves_nested_refs() {
$reflection = new ReflectionClass( $this->abilities );
$method = $reflection->getMethod( 'resolve_schema_refs' );
$method->setAccessible( true );

$schema = array(
'type' => 'array',
'items' => array(
'$ref' => '#/definitions/outerDef',
),
);

$definitions = array(
'outerDef' => array(
'type' => 'array',
'items' => array(
'$ref' => '#/definitions/innerDef',
),
),
'innerDef' => array(
'type' => 'object',
'required' => array( 'param', 'operator', 'value' ),
'properties' => array(
'param' => array( 'type' => 'string' ),
'operator' => array( 'type' => 'string' ),
'value' => array( 'type' => 'string' ),
),
),
);

$result = $method->invoke( $this->abilities, $schema, $definitions );

// Outer array should be preserved.
$this->assertEquals( 'array', $result['type'] );

// Items should be resolved (outerDef -> array with items).
$this->assertEquals( 'array', $result['items']['type'] );

// Nested items should be fully resolved (innerDef -> object).
$this->assertEquals( 'object', $result['items']['items']['type'] );
$this->assertContains( 'param', $result['items']['items']['required'] );
$this->assertArrayHasKey( 'properties', $result['items']['items'] );
}

/**
* Test resolve_schema_refs passes through non-ref schemas unchanged
*/
public function test_resolve_schema_refs_passthrough_non_ref() {
$reflection = new ReflectionClass( $this->abilities );
$method = $reflection->getMethod( 'resolve_schema_refs' );
$method->setAccessible( true );

$schema = array(
'type' => 'object',
'properties' => array(
'name' => array( 'type' => 'string' ),
'age' => array( 'type' => 'integer' ),
),
);

$definitions = array();

$result = $method->invoke( $this->abilities, $schema, $definitions );

$this->assertEquals( $schema, $result );
}

/**
* Test resolve_schema_refs triggers _doing_it_wrong for unresolvable $ref
*
* @expectedIncorrectUsage SCF_Internal_Post_Type_Abilities::resolve_schema_refs
*/
public function test_resolve_schema_refs_triggers_doing_it_wrong_for_missing_definition() {
$reflection = new ReflectionClass( $this->abilities );
$method = $reflection->getMethod( 'resolve_schema_refs' );
$method->setAccessible( true );

$schema = array(
'$ref' => '#/definitions/nonExistent',
);

$definitions = array();

$result = $method->invoke( $this->abilities, $schema, $definitions );

// Should return original schema when definition not found.
$this->assertEquals( $schema, $result );
}
}
Loading