Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional raw response processing to query context #24

Merged
merged 16 commits into from
Sep 6, 2024
2 changes: 2 additions & 0 deletions inc/config/http-query-context/http-query-context.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ public function get_request_headers( array $input_variables ): array;
public function get_request_body( array $input_variables ): array|null;
public function get_query_name(): string;
public function get_query_runner(): QueryRunnerInterface;
public function is_response_data_collection(): bool;
public function process_response( string $raw_response_data, array $input_variables ): string|array|object|null;
}
25 changes: 24 additions & 1 deletion inc/config/query-context/query-context.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public function get_request_method(): string {
/**
* Override this method to specify custom request headers for this query.
*
* @param array $input_variables The input variables for this query.
* @return array
*/
public function get_request_headers( array $input_variables ): array {
Expand All @@ -142,7 +143,7 @@ public function get_request_headers( array $input_variables ): array {
* @param array $input_variables The input variables for this query.
* @return array|null
*/
public function get_request_body( array $input_variables ): array|null {
public function get_request_body( array $input_variables ): ?array {
return null;
}

Expand All @@ -160,4 +161,26 @@ public function get_query_name(): string {
public function get_query_runner(): QueryRunnerInterface {
return new QueryRunner( $this );
}

/**
* Override this method to process the raw response data from the query before
* it is passed to the query runner and the output variables are extracted. The
* result can be a JSON string, a PHP associative array, a PHP object, or null.
*
* @param string $raw_response_data The raw response data.
* @param array $input_variables The input variables for this query.
* @return string|array|object|null
*/
public function process_response( string $raw_response_data, array $input_variables ): string|array|object|null {
return $raw_response_data;
}

/**
* Authoritative truth of whether output is expected to be a collection.
*
* @return bool
*/
final public function is_response_data_collection(): bool {
return $this->output_variables['is_collection'] ?? false;
}
}
30 changes: 24 additions & 6 deletions inc/config/query-runner/query-runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,19 @@ public function execute( array $input_variables ): array|WP_Error {
}

// The body is a stream... if we need to read it in chunks, etc. we can do so here.
$response_data = $response->getBody()->getContents();
$raw_response_data = $response->getBody()->getContents();

$is_collection = $this->query_context->output_variables['is_collection'] ?? false;

if ( isset( $response_data['errors'][0]['message'] ) ) {
if ( isset( $raw_response_data['errors'][0]['message'] ) ) {
$logger = LoggerManager::instance();
$logger->warning( sprintf( 'Query error: %s', esc_html( $response_data['errors'][0]['message'] ) ) );
$logger->warning( sprintf( 'Query error: %s', esc_html( $raw_response_data['errors'][0]['message'] ) ) );
}

// Optionally process the raw response data using query context custom logic.
$response_data = $this->query_context->process_response( $raw_response_data, $input_variables );

// Determine if the response data is expected to be a collection.
$is_collection = $this->query_context->is_response_data_collection();

// This method always returns an array, even if it's a single item. This
// ensures a consistent response shape. The requestor is expected to inspect
// is_collection and unwrap if necessary.
Expand All @@ -126,6 +130,12 @@ private function get_field_value( array|string $field_value, string $default_val
: ( $field_value[0] ?? $default_value );

switch ( $field_type ) {
case 'base64':
return base64_decode( $field_value_single );

case 'html':
return $field_value_single;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirmed, as expected, core escaping happens further on in execution--somewhere closer to output.

Screenshot 2024-09-06 at 2 21 14 PM Screenshot 2024-09-06 at 2 21 01 PM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

escapin late :)


case 'price':
return sprintf( '$%s', number_format( $field_value_single, 2 ) );

Expand All @@ -136,7 +146,15 @@ private function get_field_value( array|string $field_value, string $default_val
return $field_value_single;
}

private function map_fields( $response_data, $is_collection = false ): array|null {
/**
* Map fields from the response data using the output variables defined by
* the query.
*
* @param string|array|object|null $response_data The response data to map. Can be JSON string, PHP associative array, PHP object, or null.
* @param bool $is_collection Whether the response data is a collection.
* @return array|null The mapped fields.
*/
private function map_fields( string|array|object|null $response_data, bool $is_collection ): ?array {
$root = $response_data;
$output_variables = $this->query_context->output_variables;

Expand Down
103 changes: 103 additions & 0 deletions tests/inc/config/QueryContextTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace RemoteDataBlocks\Tests\Config;

use PHPUnit\Framework\TestCase;
use RemoteDataBlocks\Config\QueryContext;
use RemoteDataBlocks\Test\TestDatasource;
use GuzzleHttp\Psr7\Response;

class QueryContextTest extends TestCase {

private $datasource;
private $query_context;

protected function setUp(): void {
$this->datasource = new TestDatasource();
$this->query_context = new QueryContext( $this->datasource );
}

public function testGetEndpoint() {
$result = $this->query_context->get_endpoint( [] );
$this->assertEquals( 'https://example.com', $result );
}

public function testGetImageUrl() {
$result = $this->query_context->get_image_url();
$this->assertNull( $result );
}

public function testGetMetadata() {
$mock_response = new Response( 200, [ 'Age' => '60' ] );
$results = [ [ 'id' => 1 ], [ 'id' => 2 ] ];

$metadata = $this->query_context->get_metadata( $mock_response, $results );

$this->assertArrayHasKey( 'last_updated', $metadata );
$this->assertArrayHasKey( 'total_count', $metadata );
$this->assertEquals( 'Last updated', $metadata['last_updated']['name'] );
$this->assertEquals( 'string', $metadata['last_updated']['type'] );
$this->assertEquals( 'Total count', $metadata['total_count']['name'] );
$this->assertEquals( 'number', $metadata['total_count']['type'] );
$this->assertEquals( 2, $metadata['total_count']['value'] );
}

public function testGetRequestMethod() {
$this->assertEquals( 'GET', $this->query_context->get_request_method() );
}

public function testGetRequestHeaders() {
$result = $this->query_context->get_request_headers( [] );
$this->assertEquals( [ 'Content-Type' => 'application/json' ], $result );
}

public function testGetRequestBody() {
$this->assertNull( $this->query_context->get_request_body( [] ) );
}

public function testGetQueryName() {
$this->assertEquals( 'Query', $this->query_context->get_query_name() );
}

public function testIsResponseDataCollection() {
$this->assertFalse( $this->query_context->is_response_data_collection() );

$this->query_context->output_variables['is_collection'] = true;
$this->assertTrue( $this->query_context->is_response_data_collection() );
}

public function testDefaultProcessResponse() {
$raw_data = '{"key": "value"}';
$this->assertEquals( $raw_data, $this->query_context->process_response( $raw_data, [] ) );
}

public function testCustomProcessResponse() {
$custom_query_context = new class($this->datasource) extends QueryContext {
public function process_response( string $raw_response_data, array $input_variables ): string {
// Convert HTML to JSON
$dom = new \DOMDocument();
$dom->loadHTML( $raw_response_data, LIBXML_NOERROR );
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$title = $dom->getElementsByTagName( 'title' )->item( 0 )->nodeValue;
$paragraphs = $dom->getElementsByTagName( 'p' );
$content = [];
foreach ( $paragraphs as $p ) {
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$content[] = $p->nodeValue;
}

$data = [
'title' => $title,
'content' => $content,
];

return wp_json_encode( $data );
}
};

$html_data = '<html><head><title>Test Page</title></head><body><p>Paragraph 1</p><p>Paragraph 2</p></body></html>';
$expected_json = '{"title":"Test Page","content":["Paragraph 1","Paragraph 2"]}';

$this->assertEquals( $expected_json, $custom_query_context->process_response( $html_data, [] ) );
}
}
Loading