From 82bf4f5c13be2f165075e18cb68190de58d11900 Mon Sep 17 00:00:00 2001 From: Hew Date: Sun, 1 Sep 2024 11:23:26 -0400 Subject: [PATCH 01/15] Move response processing to query context --- .../http-query-context/http-query-context.php | 4 +- inc/config/query-context/query-context.php | 70 +++++++++++++++++- inc/config/query-runner/query-runner.php | 74 +------------------ tests/inc/config/QueryRunnerTest.php | 19 ++--- 4 files changed, 83 insertions(+), 84 deletions(-) diff --git a/inc/config/http-query-context/http-query-context.php b/inc/config/http-query-context/http-query-context.php index 0ebe9b98..f7cf6826 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 get_results( string $response_data, array $input_variables ): array; + public function is_collection(): bool; +} \ No newline at end of file diff --git a/inc/config/query-context/query-context.php b/inc/config/query-context/query-context.php index cbf24866..7b7a5e65 100644 --- a/inc/config/query-context/query-context.php +++ b/inc/config/query-context/query-context.php @@ -10,6 +10,7 @@ namespace RemoteDataBlocks\Config; use Psr\Http\Message\ResponseInterface; +use JsonPath\JsonObject; defined( 'ABSPATH' ) || exit(); @@ -160,4 +161,71 @@ public function get_query_name(): string { public function get_query_runner(): QueryRunnerInterface { return new QueryRunner( $this ); } -} + + public function get_results( string $response_data, array $input_variables ): array { + $root = $response_data; + $output_variables = $this->output_variables; + + if ( ! empty( $output_variables['root_path'] ) ) { + $json = new JsonObject( $root ); + $root = $json->get( $output_variables['root_path'] ); + } else { + $root = $this->is_collection() ? $root : [ $root ]; + } + + if ( empty( $root ) || empty( $output_variables['mappings'] ) ) { + return $root; + } + + // Loop over the returned items in the query result. + return array_map( function ( $item ) use ( $output_variables ) { + $json = new JsonObject( $item ); + + // Loop over the output variables and extract the values from the item. + $result = array_map( function ( $mapping ) use ( $json ) { + if ( array_key_exists( 'generate', $mapping ) && is_callable( $mapping['generate'] ) ) { + $field_value_single = $mapping['generate']( json_decode( $json->getJson(), true ) ); + } else { + $field_path = $mapping['path'] ?? null; + $field_value = $field_path ? $json->get( $field_path ) : ''; + + // JSONPath always returns values in an array, even if there's only one value. + // Because we're mostly interested in single values for field mapping, unwrap the array if it's only one item. + $field_value_single = self::get_field_value( $field_value, $mapping['defaultValue'] ?? '', $mapping['type'] ); + } + + return array_merge( $mapping, [ + 'value' => $field_value_single, + ] ); + }, $output_variables['mappings'] ); + + // Nest result property to reserve additional meta in the future. + return [ + 'result' => $result, + ]; + }, $root ); + } + + private function get_field_value( array|string $field_value, string $default_value = '', string $field_type = 'string' ): string { + $field_value_single = is_array( $field_value ) && count( $field_value ) > 1 + ? $field_value + : ( $field_value[0] ?? $default_value ); + + switch ( $field_type ) { + case 'base64': + return base64_decode( $field_value_single ); + + case 'price': + return sprintf( '$%s', number_format( $field_value_single, 2 ) ); + + case 'string': + return wp_strip_all_tags( $field_value_single ); + } + + return $field_value_single; + } + + public function is_collection(): bool { + return $this->output_variables['is_collection'] ?? false; + } +} \ No newline at end of file diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index 7353eacd..45d4d78b 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -11,7 +11,6 @@ use Exception; use GuzzleHttp\RequestOptions; -use JsonPath\JsonObject; use RemoteDataBlocks\HttpClient; use RemoteDataBlocks\Logging\LoggerManager; use WP_Error; @@ -101,82 +100,17 @@ 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(); - $is_collection = $this->query_context->output_variables['is_collection'] ?? false; - if ( isset( $response_data['errors'][0]['message'] ) ) { $logger = LoggerManager::instance(); $logger->warning( sprintf( 'Query error: %s', esc_html( $response_data['errors'][0]['message'] ) ) ); } - // 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. - $results = $this->map_fields( $response_data, $is_collection ); + $results = $this->query_context->get_results( $response_data, $input_variables ); return [ - 'is_collection' => $is_collection, + 'is_collection' => $this->query_context->is_collection(), 'metadata' => $this->query_context->get_metadata( $response, $results ), 'results' => $results, ]; - } - - private function get_field_value( array|string $field_value, string $default_value = '', string $field_type = 'string' ): string { - $field_value_single = is_array( $field_value ) && count( $field_value ) > 1 - ? $field_value - : ( $field_value[0] ?? $default_value ); - - switch ( $field_type ) { - case 'price': - return sprintf( '$%s', number_format( $field_value_single, 2 ) ); - - case 'string': - return wp_strip_all_tags( $field_value_single ); - } - - return $field_value_single; - } - - private function map_fields( $response_data, $is_collection = false ): array|null { - $root = $response_data; - $output_variables = $this->query_context->output_variables; - - if ( ! empty( $output_variables['root_path'] ) ) { - $json = new JsonObject( $root ); - $root = $json->get( $output_variables['root_path'] ); - } else { - $root = $is_collection ? $root : [ $root ]; - } - - if ( empty( $root ) || empty( $output_variables['mappings'] ) ) { - return $root; - } - - // Loop over the returned items in the query result. - return array_map( function ( $item ) use ( $output_variables ) { - $json = new JsonObject( $item ); - - // Loop over the output variables and extract the values from the item. - $result = array_map( function ( $mapping ) use ( $json ) { - if ( array_key_exists( 'generate', $mapping ) && is_callable( $mapping['generate'] ) ) { - $field_value_single = $mapping['generate']( json_decode( $json->getJson(), true ) ); - } else { - $field_path = $mapping['path'] ?? null; - $field_value = $field_path ? $json->get( $field_path ) : ''; - - // JSONPath always returns values in an array, even if there's only one value. - // Because we're mostly interested in single values for field mapping, unwrap the array if it's only one item. - $field_value_single = self::get_field_value( $field_value, $mapping['defaultValue'] ?? '', $mapping['type'] ); - } - - return array_merge( $mapping, [ - 'value' => $field_value_single, - ] ); - }, $output_variables['mappings'] ); - - // Nest result property to reserve additional meta in the future. - return [ - 'result' => $result, - ]; - }, $root ); - } -} + } +} \ No newline at end of file diff --git a/tests/inc/config/QueryRunnerTest.php b/tests/inc/config/QueryRunnerTest.php index 46b459d5..cbd86fa5 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,15 @@ 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' ]; - 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,10 +91,6 @@ 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 set_request_method( string $method ): void { $this->request_method = $method; } @@ -103,9 +101,6 @@ public function set_request_body( array $body ): void { public array $output_variables = []; }; - - $this->http_client = $this->createMock( HttpClient::class ); - $this->query_context->set_http_client( $this->http_client ); } public function testExecuteSuccessfulRequest() { @@ -205,4 +200,4 @@ public function testExecuteSuccessfulResponse() { $this->assertArrayHasKey( 'results', $result ); $this->assertFalse( $result['is_collection'] ); } -} +} \ No newline at end of file From 66dcee6d7f501a06942ddab24ec4511cce2a1ef7 Mon Sep 17 00:00:00 2001 From: Hew Date: Sun, 1 Sep 2024 11:24:35 -0400 Subject: [PATCH 02/15] Lint --- inc/config/http-query-context/http-query-context.php | 2 +- inc/config/query-context/query-context.php | 2 +- inc/config/query-runner/query-runner.php | 2 +- tests/inc/config/QueryRunnerTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inc/config/http-query-context/http-query-context.php b/inc/config/http-query-context/http-query-context.php index f7cf6826..4c474f01 100644 --- a/inc/config/http-query-context/http-query-context.php +++ b/inc/config/http-query-context/http-query-context.php @@ -22,4 +22,4 @@ public function get_query_name(): string; public function get_query_runner(): QueryRunnerInterface; public function get_results( string $response_data, array $input_variables ): array; public function is_collection(): bool; -} \ No newline at end of file +} diff --git a/inc/config/query-context/query-context.php b/inc/config/query-context/query-context.php index 7b7a5e65..0633a064 100644 --- a/inc/config/query-context/query-context.php +++ b/inc/config/query-context/query-context.php @@ -228,4 +228,4 @@ private function get_field_value( array|string $field_value, string $default_val public function is_collection(): bool { return $this->output_variables['is_collection'] ?? false; } -} \ No newline at end of file +} diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index 45d4d78b..28613049 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -113,4 +113,4 @@ public function execute( array $input_variables ): array|WP_Error { 'results' => $results, ]; } -} \ No newline at end of file +} diff --git a/tests/inc/config/QueryRunnerTest.php b/tests/inc/config/QueryRunnerTest.php index cbd86fa5..2167533c 100644 --- a/tests/inc/config/QueryRunnerTest.php +++ b/tests/inc/config/QueryRunnerTest.php @@ -200,4 +200,4 @@ public function testExecuteSuccessfulResponse() { $this->assertArrayHasKey( 'results', $result ); $this->assertFalse( $result['is_collection'] ); } -} \ No newline at end of file +} From 10de8e4bb67ae7a26de0c6eb429ae3e1bf285ced Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 13:19:09 -0400 Subject: [PATCH 03/15] Adjust approach --- .../http-query-context/http-query-context.php | 2 +- inc/config/query-context/query-context.php | 68 +-------------- inc/config/query-runner/query-runner.php | 84 +++++++++++++++++-- 3 files changed, 84 insertions(+), 70 deletions(-) diff --git a/inc/config/http-query-context/http-query-context.php b/inc/config/http-query-context/http-query-context.php index 4c474f01..be600150 100644 --- a/inc/config/http-query-context/http-query-context.php +++ b/inc/config/http-query-context/http-query-context.php @@ -20,6 +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 get_results( string $response_data, array $input_variables ): array; public function is_collection(): bool; + public function process_response( string $raw_response_data, array $input_variables ): string; } diff --git a/inc/config/query-context/query-context.php b/inc/config/query-context/query-context.php index 0633a064..5f9ad15a 100644 --- a/inc/config/query-context/query-context.php +++ b/inc/config/query-context/query-context.php @@ -10,7 +10,6 @@ namespace RemoteDataBlocks\Config; use Psr\Http\Message\ResponseInterface; -use JsonPath\JsonObject; defined( 'ABSPATH' ) || exit(); @@ -162,70 +161,11 @@ public function get_query_runner(): QueryRunnerInterface { return new QueryRunner( $this ); } - public function get_results( string $response_data, array $input_variables ): array { - $root = $response_data; - $output_variables = $this->output_variables; - - if ( ! empty( $output_variables['root_path'] ) ) { - $json = new JsonObject( $root ); - $root = $json->get( $output_variables['root_path'] ); - } else { - $root = $this->is_collection() ? $root : [ $root ]; - } - - if ( empty( $root ) || empty( $output_variables['mappings'] ) ) { - return $root; - } - - // Loop over the returned items in the query result. - return array_map( function ( $item ) use ( $output_variables ) { - $json = new JsonObject( $item ); - - // Loop over the output variables and extract the values from the item. - $result = array_map( function ( $mapping ) use ( $json ) { - if ( array_key_exists( 'generate', $mapping ) && is_callable( $mapping['generate'] ) ) { - $field_value_single = $mapping['generate']( json_decode( $json->getJson(), true ) ); - } else { - $field_path = $mapping['path'] ?? null; - $field_value = $field_path ? $json->get( $field_path ) : ''; - - // JSONPath always returns values in an array, even if there's only one value. - // Because we're mostly interested in single values for field mapping, unwrap the array if it's only one item. - $field_value_single = self::get_field_value( $field_value, $mapping['defaultValue'] ?? '', $mapping['type'] ); - } - - return array_merge( $mapping, [ - 'value' => $field_value_single, - ] ); - }, $output_variables['mappings'] ); - - // Nest result property to reserve additional meta in the future. - return [ - 'result' => $result, - ]; - }, $root ); - } - - private function get_field_value( array|string $field_value, string $default_value = '', string $field_type = 'string' ): string { - $field_value_single = is_array( $field_value ) && count( $field_value ) > 1 - ? $field_value - : ( $field_value[0] ?? $default_value ); - - switch ( $field_type ) { - case 'base64': - return base64_decode( $field_value_single ); - - case 'price': - return sprintf( '$%s', number_format( $field_value_single, 2 ) ); - - case 'string': - return wp_strip_all_tags( $field_value_single ); - } - - return $field_value_single; - } - public function is_collection(): bool { return $this->output_variables['is_collection'] ?? false; } + + public function process_response( string $raw_response_data, array $input_variables ): string { + return $raw_response_data; + } } diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index 28613049..72aecea3 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -11,6 +11,7 @@ use Exception; use GuzzleHttp\RequestOptions; +use JsonPath\JsonObject; use RemoteDataBlocks\HttpClient; use RemoteDataBlocks\Logging\LoggerManager; use WP_Error; @@ -98,19 +99,92 @@ 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(); - 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'] ) ) ); } - $results = $this->query_context->get_results( $response_data, $input_variables ); + $response_data = $this->query_context->process_response( $raw_response_data, $input_variables ); + + // 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. + $results = $this->map_fields( $response_data ); return [ 'is_collection' => $this->query_context->is_collection(), 'metadata' => $this->query_context->get_metadata( $response, $results ), 'results' => $results, ]; - } + } + + private function get_field_value( array|string $field_value, string $default_value = '', string $field_type = 'string' ): string { + $field_value_single = is_array( $field_value ) && count( $field_value ) > 1 + ? $field_value + : ( $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 ) ); + + case 'string': + return wp_strip_all_tags( $field_value_single ); + } + + return $field_value_single; + } + + private function map_fields( $response_data ): array|null { + $root = $response_data; + $output_variables = $this->query_context->output_variables; + + if ( ! empty( $output_variables['root_path'] ) ) { + $json = new JsonObject( $root ); + $root = $json->get( $output_variables['root_path'] ); + } else { + $root = $this->query_context->is_collection() ? $root : [ $root ]; + } + + if ( empty( $root ) || empty( $output_variables['mappings'] ) ) { + return $root; + } + + // Loop over the returned items in the query result. + return array_map( function ( $item ) use ( $output_variables ) { + $json = new JsonObject( $item ); + + // Loop over the output variables and extract the values from the item. + $result = array_map( function ( $mapping ) use ( $json ) { + if ( array_key_exists( 'generate', $mapping ) && is_callable( $mapping['generate'] ) ) { + $field_value_single = $mapping['generate']( json_decode( $json->getJson(), true ) ); + } else { + $field_path = $mapping['path'] ?? null; + $field_value = $field_path ? $json->get( $field_path ) : ''; + + error_log( print_r( $field_value, true ) ); + error_log( print_r( $mapping, true ) ); + // JSONPath always returns values in an array, even if there's only one value. + // Because we're mostly interested in single values for field mapping, unwrap the array if it's only one item. + $field_value_single = self::get_field_value( $field_value, $mapping['defaultValue'] ?? '', $mapping['type'] ); + } + + return array_merge( $mapping, [ + 'value' => $field_value_single, + ] ); + }, $output_variables['mappings'] ); + + // Nest result property to reserve additional meta in the future. + return [ + 'result' => $result, + ]; + }, $root ); + } } From 9764d9e44edd4bdd498c6fd93115aa1aa8add14e Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 13:39:21 -0400 Subject: [PATCH 04/15] Ya ya, was working on it --- inc/config/query-runner/query-runner.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index 72aecea3..b824827a 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -169,8 +169,6 @@ private function map_fields( $response_data ): array|null { $field_path = $mapping['path'] ?? null; $field_value = $field_path ? $json->get( $field_path ) : ''; - error_log( print_r( $field_value, true ) ); - error_log( print_r( $mapping, true ) ); // JSONPath always returns values in an array, even if there's only one value. // Because we're mostly interested in single values for field mapping, unwrap the array if it's only one item. $field_value_single = self::get_field_value( $field_value, $mapping['defaultValue'] ?? '', $mapping['type'] ); From d59a147a6142801853f1d60c261fb075c1b04e80 Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 13:41:03 -0400 Subject: [PATCH 05/15] Add comment --- inc/config/query-runner/query-runner.php | 1 + 1 file changed, 1 insertion(+) diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index b824827a..5223c13a 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -106,6 +106,7 @@ public function execute( array $input_variables ): array|WP_Error { $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 ); // This method always returns an array, even if it's a single item. This From f3c637d3ce05509b941a508ec9d9fd559906d005 Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 15:39:21 -0400 Subject: [PATCH 06/15] Add tests --- tests/inc/config/QueryContextTest.php | 113 ++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 tests/inc/config/QueryContextTest.php diff --git a/tests/inc/config/QueryContextTest.php b/tests/inc/config/QueryContextTest.php new file mode 100644 index 00000000..72976aef --- /dev/null +++ b/tests/inc/config/QueryContextTest.php @@ -0,0 +1,113 @@ +datasource = new TestDatasource(); + $this->queryContext = new QueryContext($this->datasource); + } + + public function testGetEndpoint() + { + $result = $this->queryContext->get_endpoint([]); + $this->assertEquals('https://example.com', $result); + } + + public function testGetImageUrl() + { + $result = $this->queryContext->get_image_url(); + $this->assertNull($result); + } + + public function testGetMetadata() + { + $mockResponse = new Response(200, ['Age' => '60']); + $results = [['id' => 1], ['id' => 2]]; + + $metadata = $this->queryContext->get_metadata($mockResponse, $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->queryContext->get_request_method()); + } + + public function testGetRequestHeaders() + { + $result = $this->queryContext->get_request_headers([]); + $this->assertEquals(['Content-Type' => 'application/json'], $result); + } + + public function testGetRequestBody() + { + $this->assertNull($this->queryContext->get_request_body([])); + } + + public function testGetQueryName() + { + $this->assertEquals('Query', $this->queryContext->get_query_name()); + } + + public function testIsCollection() + { + $this->assertFalse($this->queryContext->is_collection()); + + $this->queryContext->output_variables['is_collection'] = true; + $this->assertTrue($this->queryContext->is_collection()); + } + + public function testDefaultProcessResponse() + { + $rawData = '{"key": "value"}'; + $this->assertEquals($rawData, $this->queryContext->process_response($rawData, [])); + } + + public function testCustomProcessResponse() + { + $customQueryContext = 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); + $title = $dom->getElementsByTagName('title')->item(0)->nodeValue; + $paragraphs = $dom->getElementsByTagName('p'); + $content = []; + foreach ($paragraphs as $p) { + $content[] = $p->nodeValue; + } + + $data = [ + 'title' => $title, + 'content' => $content + ]; + + return json_encode($data); + } + }; + + $htmlData = 'Test Page

Paragraph 1

Paragraph 2

'; + $expectedJson = '{"title":"Test Page","content":["Paragraph 1","Paragraph 2"]}'; + + $this->assertEquals($expectedJson, $customQueryContext->process_response($htmlData, [])); + } +} \ No newline at end of file From 2471cb0bb49368656c52af6f6d179ddabfecfec2 Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 15:40:02 -0400 Subject: [PATCH 07/15] Lint --- tests/inc/config/QueryContextTest.php | 196 ++++++++++++-------------- 1 file changed, 92 insertions(+), 104 deletions(-) diff --git a/tests/inc/config/QueryContextTest.php b/tests/inc/config/QueryContextTest.php index 72976aef..a436a7e1 100644 --- a/tests/inc/config/QueryContextTest.php +++ b/tests/inc/config/QueryContextTest.php @@ -7,107 +7,95 @@ use RemoteDataBlocks\Test\TestDatasource; use GuzzleHttp\Psr7\Response; -class QueryContextTest extends TestCase -{ - private $datasource; - private $queryContext; - - protected function setUp(): void - { - $this->datasource = new TestDatasource(); - $this->queryContext = new QueryContext($this->datasource); - } - - public function testGetEndpoint() - { - $result = $this->queryContext->get_endpoint([]); - $this->assertEquals('https://example.com', $result); - } - - public function testGetImageUrl() - { - $result = $this->queryContext->get_image_url(); - $this->assertNull($result); - } - - public function testGetMetadata() - { - $mockResponse = new Response(200, ['Age' => '60']); - $results = [['id' => 1], ['id' => 2]]; - - $metadata = $this->queryContext->get_metadata($mockResponse, $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->queryContext->get_request_method()); - } - - public function testGetRequestHeaders() - { - $result = $this->queryContext->get_request_headers([]); - $this->assertEquals(['Content-Type' => 'application/json'], $result); - } - - public function testGetRequestBody() - { - $this->assertNull($this->queryContext->get_request_body([])); - } - - public function testGetQueryName() - { - $this->assertEquals('Query', $this->queryContext->get_query_name()); - } - - public function testIsCollection() - { - $this->assertFalse($this->queryContext->is_collection()); - - $this->queryContext->output_variables['is_collection'] = true; - $this->assertTrue($this->queryContext->is_collection()); - } - - public function testDefaultProcessResponse() - { - $rawData = '{"key": "value"}'; - $this->assertEquals($rawData, $this->queryContext->process_response($rawData, [])); - } - - public function testCustomProcessResponse() - { - $customQueryContext = 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); - $title = $dom->getElementsByTagName('title')->item(0)->nodeValue; - $paragraphs = $dom->getElementsByTagName('p'); - $content = []; - foreach ($paragraphs as $p) { - $content[] = $p->nodeValue; - } - - $data = [ - 'title' => $title, - 'content' => $content - ]; - - return json_encode($data); - } - }; - - $htmlData = 'Test Page

Paragraph 1

Paragraph 2

'; - $expectedJson = '{"title":"Test Page","content":["Paragraph 1","Paragraph 2"]}'; - - $this->assertEquals($expectedJson, $customQueryContext->process_response($htmlData, [])); - } -} \ No newline at end of file +class QueryContextTest extends TestCase { + + private $datasource; + private $queryContext; + + protected function setUp(): void { + $this->datasource = new TestDatasource(); + $this->queryContext = new QueryContext( $this->datasource ); + } + + public function testGetEndpoint() { + $result = $this->queryContext->get_endpoint( [] ); + $this->assertEquals( 'https://example.com', $result ); + } + + public function testGetImageUrl() { + $result = $this->queryContext->get_image_url(); + $this->assertNull( $result ); + } + + public function testGetMetadata() { + $mockResponse = new Response( 200, [ 'Age' => '60' ] ); + $results = [ [ 'id' => 1 ], [ 'id' => 2 ] ]; + + $metadata = $this->queryContext->get_metadata( $mockResponse, $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->queryContext->get_request_method() ); + } + + public function testGetRequestHeaders() { + $result = $this->queryContext->get_request_headers( [] ); + $this->assertEquals( [ 'Content-Type' => 'application/json' ], $result ); + } + + public function testGetRequestBody() { + $this->assertNull( $this->queryContext->get_request_body( [] ) ); + } + + public function testGetQueryName() { + $this->assertEquals( 'Query', $this->queryContext->get_query_name() ); + } + + public function testIsCollection() { + $this->assertFalse( $this->queryContext->is_collection() ); + + $this->queryContext->output_variables['is_collection'] = true; + $this->assertTrue( $this->queryContext->is_collection() ); + } + + public function testDefaultProcessResponse() { + $rawData = '{"key": "value"}'; + $this->assertEquals( $rawData, $this->queryContext->process_response( $rawData, [] ) ); + } + + public function testCustomProcessResponse() { + $customQueryContext = 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 ); + $title = $dom->getElementsByTagName( 'title' )->item( 0 )->nodeValue; + $paragraphs = $dom->getElementsByTagName( 'p' ); + $content = []; + foreach ( $paragraphs as $p ) { + $content[] = $p->nodeValue; + } + + $data = [ + 'title' => $title, + 'content' => $content, + ]; + + return json_encode( $data ); + } + }; + + $htmlData = 'Test Page

Paragraph 1

Paragraph 2

'; + $expectedJson = '{"title":"Test Page","content":["Paragraph 1","Paragraph 2"]}'; + + $this->assertEquals( $expectedJson, $customQueryContext->process_response( $htmlData, [] ) ); + } +} From f9c9fa77dbb7516158ea2f9f9f385f25f251d0c3 Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 15:48:21 -0400 Subject: [PATCH 08/15] more lint --- tests/inc/config/QueryContextTest.php | 48 ++++++++++++++------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/inc/config/QueryContextTest.php b/tests/inc/config/QueryContextTest.php index a436a7e1..b096cf84 100644 --- a/tests/inc/config/QueryContextTest.php +++ b/tests/inc/config/QueryContextTest.php @@ -10,28 +10,28 @@ class QueryContextTest extends TestCase { private $datasource; - private $queryContext; + private $query_context; protected function setUp(): void { - $this->datasource = new TestDatasource(); - $this->queryContext = new QueryContext( $this->datasource ); + $this->datasource = new TestDatasource(); + $this->query_context = new QueryContext( $this->datasource ); } public function testGetEndpoint() { - $result = $this->queryContext->get_endpoint( [] ); + $result = $this->query_context->get_endpoint( [] ); $this->assertEquals( 'https://example.com', $result ); } public function testGetImageUrl() { - $result = $this->queryContext->get_image_url(); + $result = $this->query_context->get_image_url(); $this->assertNull( $result ); } public function testGetMetadata() { - $mockResponse = new Response( 200, [ 'Age' => '60' ] ); - $results = [ [ 'id' => 1 ], [ 'id' => 2 ] ]; + $mock_response = new Response( 200, [ 'Age' => '60' ] ); + $results = [ [ 'id' => 1 ], [ 'id' => 2 ] ]; - $metadata = $this->queryContext->get_metadata( $mockResponse, $results ); + $metadata = $this->query_context->get_metadata( $mock_response, $results ); $this->assertArrayHasKey( 'last_updated', $metadata ); $this->assertArrayHasKey( 'total_count', $metadata ); @@ -43,44 +43,46 @@ public function testGetMetadata() { } public function testGetRequestMethod() { - $this->assertEquals( 'GET', $this->queryContext->get_request_method() ); + $this->assertEquals( 'GET', $this->query_context->get_request_method() ); } public function testGetRequestHeaders() { - $result = $this->queryContext->get_request_headers( [] ); + $result = $this->query_context->get_request_headers( [] ); $this->assertEquals( [ 'Content-Type' => 'application/json' ], $result ); } public function testGetRequestBody() { - $this->assertNull( $this->queryContext->get_request_body( [] ) ); + $this->assertNull( $this->query_context->get_request_body( [] ) ); } public function testGetQueryName() { - $this->assertEquals( 'Query', $this->queryContext->get_query_name() ); + $this->assertEquals( 'Query', $this->query_context->get_query_name() ); } public function testIsCollection() { - $this->assertFalse( $this->queryContext->is_collection() ); + $this->assertFalse( $this->query_context->is_collection() ); - $this->queryContext->output_variables['is_collection'] = true; - $this->assertTrue( $this->queryContext->is_collection() ); + $this->query_context->output_variables['is_collection'] = true; + $this->assertTrue( $this->query_context->is_collection() ); } public function testDefaultProcessResponse() { - $rawData = '{"key": "value"}'; - $this->assertEquals( $rawData, $this->queryContext->process_response( $rawData, [] ) ); + $raw_data = '{"key": "value"}'; + $this->assertEquals( $raw_data, $this->query_context->process_response( $raw_data, [] ) ); } public function testCustomProcessResponse() { - $customQueryContext = new class($this->datasource) extends QueryContext { + $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 ); + $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; } @@ -89,13 +91,13 @@ public function process_response( string $raw_response_data, array $input_variab 'content' => $content, ]; - return json_encode( $data ); + return wp_json_encode( $data ); } }; - $htmlData = 'Test Page

Paragraph 1

Paragraph 2

'; - $expectedJson = '{"title":"Test Page","content":["Paragraph 1","Paragraph 2"]}'; + $html_data = 'Test Page

Paragraph 1

Paragraph 2

'; + $expected_json = '{"title":"Test Page","content":["Paragraph 1","Paragraph 2"]}'; - $this->assertEquals( $expectedJson, $customQueryContext->process_response( $htmlData, [] ) ); + $this->assertEquals( $expected_json, $custom_query_context->process_response( $html_data, [] ) ); } } From 9385d43945bb910fe6adcaf6bdadb035c10fb639 Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 15:53:27 -0400 Subject: [PATCH 09/15] WS fix, wut. --- tests/inc/config/QueryContextTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/inc/config/QueryContextTest.php b/tests/inc/config/QueryContextTest.php index b096cf84..9b99672c 100644 --- a/tests/inc/config/QueryContextTest.php +++ b/tests/inc/config/QueryContextTest.php @@ -13,8 +13,8 @@ class QueryContextTest extends TestCase { private $query_context; protected function setUp(): void { - $this->datasource = new TestDatasource(); - $this->query_context = new QueryContext( $this->datasource ); + $this->datasource = new TestDatasource(); + $this->query_context = new QueryContext( $this->datasource ); } public function testGetEndpoint() { From b524fc31d20f488303549d979490c1b970814fbc Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 16:32:44 -0400 Subject: [PATCH 10/15] More comments --- inc/config/query-context/query-context.php | 4 ++++ inc/config/query-runner/query-runner.php | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/inc/config/query-context/query-context.php b/inc/config/query-context/query-context.php index 5f9ad15a..0e7c0919 100644 --- a/inc/config/query-context/query-context.php +++ b/inc/config/query-context/query-context.php @@ -165,6 +165,10 @@ public function is_collection(): bool { return $this->output_variables['is_collection'] ?? false; } + /** + * Override this method to process the raw response data from the query + * before it is mapped into a result by the query runner. + */ public function process_response( string $raw_response_data, array $input_variables ): string { return $raw_response_data; } diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index 5223c13a..4d40780d 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -143,6 +143,12 @@ private function get_field_value( array|string $field_value, string $default_val return $field_value_single; } + /** + * Map the fields from the response data to the output variables. + * + * @param string $response_data The response data in JSON format from the query. + * @return array|null + */ private function map_fields( $response_data ): array|null { $root = $response_data; $output_variables = $this->query_context->output_variables; From 83670216c2cc48bdfab622f412fa947e51f33a00 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Fri, 6 Sep 2024 16:24:08 -0400 Subject: [PATCH 11/15] Improve type annotations to reflect JsonObject capabilities --- .../http-query-context/http-query-context.php | 2 +- inc/config/query-context/query-context.php | 19 +++++++++++++++---- inc/config/query-runner/query-runner.php | 9 +++++---- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/inc/config/http-query-context/http-query-context.php b/inc/config/http-query-context/http-query-context.php index be600150..21ac6328 100644 --- a/inc/config/http-query-context/http-query-context.php +++ b/inc/config/http-query-context/http-query-context.php @@ -21,5 +21,5 @@ 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_collection(): bool; - public function process_response( string $raw_response_data, array $input_variables ): string; + 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 0e7c0919..d64b3651 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; } @@ -161,15 +162,25 @@ public function get_query_runner(): QueryRunnerInterface { return new QueryRunner( $this ); } + /** + * Override this method to determine whether a response is a collection at runtime. + * + * @return bool + */ public function is_collection(): bool { return $this->output_variables['is_collection'] ?? false; } /** - * Override this method to process the raw response data from the query - * before it is mapped into a result by the query runner. + * 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 { + public function process_response( string $raw_response_data, array $input_variables ): string|array|object|null { return $raw_response_data; } } diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index 4d40780d..14755903 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -144,12 +144,13 @@ private function get_field_value( array|string $field_value, string $default_val } /** - * Map the fields from the response data to the output variables. + * Map fields from the response data using the output variables defined by + * the query. * - * @param string $response_data The response data in JSON format from the query. - * @return array|null + * @param string|array|object|null $response_data The response data to map. Can be JSON string, PHP associative array, PHP object, or null. + * @return array|null The mapped fields. */ - private function map_fields( $response_data ): array|null { + private function map_fields( string|array|object|null $response_data ): ?array { $root = $response_data; $output_variables = $this->query_context->output_variables; From f6f9771caa9b112e6733abcc0064f6917d7251d5 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Fri, 6 Sep 2024 16:53:57 -0400 Subject: [PATCH 12/15] Add additional test cases asserting flexible response data types --- tests/inc/config/QueryRunnerTest.php | 144 +++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/tests/inc/config/QueryRunnerTest.php b/tests/inc/config/QueryRunnerTest.php index 2167533c..cf57f454 100644 --- a/tests/inc/config/QueryRunnerTest.php +++ b/tests/inc/config/QueryRunnerTest.php @@ -53,6 +53,7 @@ public function set_headers( array $headers ): void { private $http_client; private $request_method = 'GET'; private $request_body = [ 'query' => 'test' ]; + private $response_data = null; public function __construct( HttpDatasourceConfig $http_datasource, HttpClient $http_client ) { $this->http_datasource = $http_datasource; @@ -91,6 +92,14 @@ public function get_query_runner(): QueryRunnerInterface { return new QueryRunner( $this, $this->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 { $this->request_method = $method; } @@ -99,6 +108,10 @@ 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 = []; }; } @@ -200,4 +213,135 @@ public function testExecuteSuccessfulResponse() { $this->assertArrayHasKey( 'results', $result ); $this->assertFalse( $result['is_collection'] ); } + + 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] ); + } } From 84b05de45e5f278fb1c2076137273cadb211933f Mon Sep 17 00:00:00 2001 From: chriszarate Date: Fri, 6 Sep 2024 17:10:38 -0400 Subject: [PATCH 13/15] Improve standard processing test --- tests/inc/config/QueryRunnerTest.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/inc/config/QueryRunnerTest.php b/tests/inc/config/QueryRunnerTest.php index cf57f454..56c42a68 100644 --- a/tests/inc/config/QueryRunnerTest.php +++ b/tests/inc/config/QueryRunnerTest.php @@ -189,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 ); @@ -200,6 +200,7 @@ public function testExecuteSuccessfulResponse() { 'mappings' => [ 'test' => [ 'name' => 'Test Field', + 'path' => '$.test', 'type' => 'string', ], ], @@ -212,6 +213,21 @@ 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() { From 318afbf6d8d2d1b39242473e24e44308a721b746 Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 17:37:52 -0400 Subject: [PATCH 14/15] Adjust verbiage, single call --- .../http-query-context/http-query-context.php | 2 +- inc/config/query-context/query-context.php | 18 +++++++++--------- inc/config/query-runner/query-runner.php | 12 ++++++++---- tests/inc/config/QueryContextTest.php | 6 +++--- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/inc/config/http-query-context/http-query-context.php b/inc/config/http-query-context/http-query-context.php index 21ac6328..5950a65d 100644 --- a/inc/config/http-query-context/http-query-context.php +++ b/inc/config/http-query-context/http-query-context.php @@ -20,6 +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_collection(): bool; + 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 d64b3651..4c3a05ad 100644 --- a/inc/config/query-context/query-context.php +++ b/inc/config/query-context/query-context.php @@ -162,15 +162,6 @@ public function get_query_runner(): QueryRunnerInterface { return new QueryRunner( $this ); } - /** - * Override this method to determine whether a response is a collection at runtime. - * - * @return bool - */ - public function is_collection(): bool { - return $this->output_variables['is_collection'] ?? false; - } - /** * 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 @@ -183,4 +174,13 @@ public function is_collection(): bool { 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 14755903..da6b031b 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -109,13 +109,16 @@ public function execute( array $input_variables ): array|WP_Error { // 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. - $results = $this->map_fields( $response_data ); + $results = $this->map_fields( $response_data, $is_collection); return [ - 'is_collection' => $this->query_context->is_collection(), + 'is_collection' => $is_collection, 'metadata' => $this->query_context->get_metadata( $response, $results ), 'results' => $results, ]; @@ -148,9 +151,10 @@ private function get_field_value( array|string $field_value, string $default_val * 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 ): ?array { + private function map_fields( string|array|object|null $response_data, bool $is_collection ): ?array { $root = $response_data; $output_variables = $this->query_context->output_variables; @@ -158,7 +162,7 @@ private function map_fields( string|array|object|null $response_data ): ?array { $json = new JsonObject( $root ); $root = $json->get( $output_variables['root_path'] ); } else { - $root = $this->query_context->is_collection() ? $root : [ $root ]; + $root = $is_collection ? $root : [ $root ]; } if ( empty( $root ) || empty( $output_variables['mappings'] ) ) { diff --git a/tests/inc/config/QueryContextTest.php b/tests/inc/config/QueryContextTest.php index 9b99672c..b7f07fb3 100644 --- a/tests/inc/config/QueryContextTest.php +++ b/tests/inc/config/QueryContextTest.php @@ -59,11 +59,11 @@ public function testGetQueryName() { $this->assertEquals( 'Query', $this->query_context->get_query_name() ); } - public function testIsCollection() { - $this->assertFalse( $this->query_context->is_collection() ); + 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_collection() ); + $this->assertTrue( $this->query_context->is_response_data_collection() ); } public function testDefaultProcessResponse() { From 8dd1472137f28cdee073ea91af5e85dd92e7f240 Mon Sep 17 00:00:00 2001 From: Hew Date: Fri, 6 Sep 2024 17:38:21 -0400 Subject: [PATCH 15/15] Lint --- inc/config/query-runner/query-runner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/config/query-runner/query-runner.php b/inc/config/query-runner/query-runner.php index da6b031b..e5128dff 100644 --- a/inc/config/query-runner/query-runner.php +++ b/inc/config/query-runner/query-runner.php @@ -115,7 +115,7 @@ public function execute( array $input_variables ): array|WP_Error { // 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. - $results = $this->map_fields( $response_data, $is_collection); + $results = $this->map_fields( $response_data, $is_collection ); return [ 'is_collection' => $is_collection,