diff --git a/inc/config/http-query-context/http-query-context.php b/inc/config/http-query-context/http-query-context.php index 0ebe9b98..5950a65d 100644 --- a/inc/config/http-query-context/http-query-context.php +++ b/inc/config/http-query-context/http-query-context.php @@ -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; } diff --git a/inc/config/query-context/query-context.php b/inc/config/query-context/query-context.php index cbf24866..4c3a05ad 100644 --- a/inc/config/query-context/query-context.php +++ b/inc/config/query-context/query-context.php @@ -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 { @@ -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; } @@ -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; + } } diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index 7353eacd..e5128dff 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -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. @@ -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; + case 'price': return sprintf( '$%s', number_format( $field_value_single, 2 ) ); @@ -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; diff --git a/tests/inc/config/QueryContextTest.php b/tests/inc/config/QueryContextTest.php new file mode 100644 index 00000000..b7f07fb3 --- /dev/null +++ b/tests/inc/config/QueryContextTest.php @@ -0,0 +1,103 @@ +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 = '
Paragraph 1
Paragraph 2
'; + $expected_json = '{"title":"Test Page","content":["Paragraph 1","Paragraph 2"]}'; + + $this->assertEquals( $expected_json, $custom_query_context->process_response( $html_data, [] ) ); + } +} diff --git a/tests/inc/config/QueryRunnerTest.php b/tests/inc/config/QueryRunnerTest.php index 46b459d5..56c42a68 100644 --- a/tests/inc/config/QueryRunnerTest.php +++ b/tests/inc/config/QueryRunnerTest.php @@ -7,8 +7,8 @@ use Psr\Http\Message\ResponseInterface; use RemoteDataBlocks\Config\QueryRunner; use RemoteDataBlocks\Config\QueryRunnerInterface; -use RemoteDataBlocks\Config\HttpQueryContext; use RemoteDataBlocks\Config\HttpDatasourceConfig; +use RemoteDataBlocks\Config\QueryContext; use RemoteDataBlocks\HttpClient; use WP_Error; @@ -21,6 +21,8 @@ class QueryRunnerTest extends TestCase { protected function setUp(): void { parent::setUp(); + $this->http_client = $this->createMock( HttpClient::class ); + $this->http_datasource = new class() implements HttpDatasourceConfig { private $endpoint = 'https://api.example.com'; private $headers = [ 'Content-Type' => 'application/json' ]; @@ -46,15 +48,16 @@ public function set_headers( array $headers ): void { } }; - $this->query_context = new class($this->http_datasource) implements HttpQueryContext { + $this->query_context = new class($this->http_datasource, $this->http_client) extends QueryContext { private $http_datasource; private $http_client; private $request_method = 'GET'; private $request_body = [ 'query' => 'test' ]; + private $response_data = null; - public function __construct( HttpDatasourceConfig $http_datasource ) { + public function __construct( HttpDatasourceConfig $http_datasource, HttpClient $http_client ) { $this->http_datasource = $http_datasource; - $this->http_client = new HttpClient(); + $this->http_client = $http_client; } public function get_endpoint( array $input_variables = [] ): string { @@ -89,8 +92,12 @@ public function get_query_runner(): QueryRunnerInterface { return new QueryRunner( $this, $this->http_client ); } - public function set_http_client( HttpClient $http_client ): void { - $this->http_client = $http_client; + public function process_response( string $raw_response_data, array $input_variables ): string|array|object|null { + if ( null !== $this->response_data ) { + return $this->response_data; + } + + return $raw_response_data; } public function set_request_method( string $method ): void { @@ -101,11 +108,12 @@ public function set_request_body( array $body ): void { $this->request_body = $body; } + public function set_response_data( string|array|object|null $data ): void { + $this->response_data = $data; + } + public array $output_variables = []; }; - - $this->http_client = $this->createMock( HttpClient::class ); - $this->query_context->set_http_client( $this->http_client ); } public function testExecuteSuccessfulRequest() { @@ -181,7 +189,7 @@ public function testExecuteSuccessfulResponse() { $input_variables = [ 'key' => 'value' ]; $response_body = $this->createMock( \Psr\Http\Message\StreamInterface::class ); - $response_body->method( 'getContents' )->willReturn( wp_json_encode( [ 'data' => 'test' ] ) ); + $response_body->method( 'getContents' )->willReturn( wp_json_encode( [ 'test' => 'test value' ] ) ); $response = new Response( 200, [], $response_body ); @@ -192,6 +200,7 @@ public function testExecuteSuccessfulResponse() { 'mappings' => [ 'test' => [ 'name' => 'Test Field', + 'path' => '$.test', 'type' => 'string', ], ], @@ -204,5 +213,151 @@ public function testExecuteSuccessfulResponse() { $this->assertArrayHasKey( 'is_collection', $result ); $this->assertArrayHasKey( 'results', $result ); $this->assertFalse( $result['is_collection'] ); + + $expected_result = [ + 'result' => [ + 'test' => [ + 'name' => 'Test Field', + 'path' => '$.test', + 'type' => 'string', + 'value' => 'test value', + ], + ], + ]; + + $this->assertIsArray( $result['results'] ); + $this->assertCount( 1, $result['results'] ); + $this->assertEquals( $expected_result, $result['results'][0] ); + } + + public function testExecuteSuccessfulResponseWithJsonStringResponseData() { + $response_body = $this->createMock( \Psr\Http\Message\StreamInterface::class ); + $response = new Response( 200, [], $response_body ); + + $this->http_client->method( 'request' )->willReturn( $response ); + + $this->query_context->set_response_data( '{"test":"overridden in process_response as JSON string"}' ); + $this->query_context->output_variables = [ + 'is_collection' => false, + 'mappings' => [ + 'test' => [ + 'name' => 'Test Field', + 'path' => '$.test', + 'type' => 'string', + ], + ], + ]; + + $query_runner = $this->query_context->get_query_runner(); + $result = $query_runner->execute( [] ); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'is_collection', $result ); + $this->assertArrayHasKey( 'results', $result ); + $this->assertFalse( $result['is_collection'] ); + + $expected_result = [ + 'result' => [ + 'test' => [ + 'name' => 'Test Field', + 'path' => '$.test', + 'type' => 'string', + 'value' => 'overridden in process_response as JSON string', + ], + ], + ]; + + $this->assertIsArray( $result['results'] ); + $this->assertCount( 1, $result['results'] ); + $this->assertEquals( $expected_result, $result['results'][0] ); + } + + public function testExecuteSuccessfulResponseWithArrayResponseData() { + $response_body = $this->createMock( \Psr\Http\Message\StreamInterface::class ); + $response = new Response( 200, [], $response_body ); + + $this->http_client->method( 'request' )->willReturn( $response ); + + $this->query_context->set_response_data( [ 'test' => 'overridden in process_response as array' ] ); + $this->query_context->output_variables = [ + 'is_collection' => false, + 'mappings' => [ + 'test' => [ + 'name' => 'Test Field', + 'path' => '$.test', + 'type' => 'string', + ], + ], + ]; + + $query_runner = $this->query_context->get_query_runner(); + $result = $query_runner->execute( [] ); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'is_collection', $result ); + $this->assertArrayHasKey( 'results', $result ); + $this->assertFalse( $result['is_collection'] ); + + $expected_result = [ + 'result' => [ + 'test' => [ + 'name' => 'Test Field', + 'path' => '$.test', + 'type' => 'string', + 'value' => 'overridden in process_response as array', + ], + ], + ]; + + $this->assertIsArray( $result['results'] ); + $this->assertCount( 1, $result['results'] ); + $this->assertEquals( $expected_result, $result['results'][0] ); + } + + public function testExecuteSuccessfulResponseWithObjectResponseData() { + $response_body = $this->createMock( \Psr\Http\Message\StreamInterface::class ); + $response = new Response( 200, [], $response_body ); + + $response = new Response( 200, [], $response_body ); + + $this->http_client->method( 'request' )->willReturn( $response ); + + $response_data = new \stdClass(); + $response_data->test = 'overridden in process_response as object'; + + $this->query_context->set_response_data( $response_data ); + $this->query_context->output_variables = [ + 'is_collection' => false, + 'mappings' => [ + 'test' => [ + 'name' => 'Test Field', + 'path' => '$.test', + 'type' => 'string', + ], + ], + ]; + + $query_runner = $this->query_context->get_query_runner(); + $result = $query_runner->execute( [] ); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'is_collection', $result ); + $this->assertArrayHasKey( 'results', $result ); + $this->assertFalse( $result['is_collection'] ); + + $expected_result = [ + 'result' => [ + 'test' => [ + 'name' => 'Test Field', + 'path' => '$.test', + 'type' => 'string', + 'value' => 'overridden in process_response as object', + ], + ], + ]; + + $this->assertIsArray( $result['results'] ); + $this->assertCount( 1, $result['results'] ); + $this->assertEquals( $expected_result, $result['results'][0] ); } }