diff --git a/README.md b/README.md index 9d389b4..dbc626b 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,10 @@ use \Ovh\Api; use GuzzleHttp\Client; // Instantiate a custom Guzzle HTTP client and tweak it -$client = new Client(); -$client->setDefaultOption('timeout', 1); -$client->setDefaultOption('headers', ['User-Agent' => 'api_client']); +$client = new Client([ + 'timeout' => 1, + 'headers' => ['User-Agent' => 'api_client'], +]); // Api credentials can be retrieved from the urls specified in the "Supported endpoints" section below. // Inject the custom HTTP client as the 5th argument of the constructor diff --git a/src/Api.php b/src/Api.php index be72110..5cf7324 100644 --- a/src/Api.php +++ b/src/Api.php @@ -264,6 +264,76 @@ protected function getTarget($path) : string return $endpoint . $path; } + /** + * @param mixed $value + * + * @return mixed + */ + private function normalizeQueryValue($value) + { + if (is_array($value)) { + foreach ($value as $key => $item) { + $value[$key] = $this->normalizeQueryValue($item); + } + + return $value; + } + + if ($value === false) { + return 'false'; + } + + if ($value === true) { + return 'true'; + } + + if (is_float($value) && !is_finite($value)) { + return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); + } + + return $value; + } + + /** + * @param array $headers + * + * @return array + */ + private function normalizeHeaders(array $headers): array + { + foreach ($headers as $name => $value) { + if (is_array($value)) { + if ($value === []) { + unset($headers[$name]); + continue; + } + + foreach ($value as $index => $item) { + if ($item === null || is_scalar($item) || (is_object($item) && method_exists($item, '__toString'))) { + if (is_float($item) && !is_finite($item)) { + $item = is_nan($item) ? 'NAN' : ($item > 0 ? 'INF' : '-INF'); + } + + $value[$index] = (string)$item; + } + } + + $headers[$name] = $value; + continue; + } + + if ($value === null || is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) { + if (is_float($value) && !is_finite($value)) { + $value = is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); + } + + $headers[$name] = (string)$value; + } + } + + return $headers; + } + /** * This is the main method of this wrapper. It will * sign a given query and return its result. @@ -307,13 +377,8 @@ protected function rawCall($method, $path, $content = null, $is_authenticated = $query = array_merge($query, (array)$content); - // rewrite query args to properly dump true/false parameters foreach ($query as $key => $value) { - if ($value === false) { - $query[$key] = "false"; - } elseif ($value === true) { - $query[$key] = "true"; - } + $query[$key] = $this->normalizeQueryValue($value); } $query = \GuzzleHttp\Psr7\Query::build($query); @@ -331,6 +396,7 @@ protected function rawCall($method, $path, $content = null, $is_authenticated = if (!is_array($headers)) { $headers = []; } + $headers = $this->normalizeHeaders($headers); $headers['Content-Type'] = 'application/json; charset=utf-8'; if ($is_authenticated) { @@ -342,7 +408,7 @@ protected function rawCall($method, $path, $content = null, $is_authenticated = if (!isset($this->time_delta)) { $this->calculateTimeDelta(); } - $now = time() + $this->time_delta; + $now = (string)(time() + $this->time_delta); $headers['X-Ovh-Timestamp'] = $now; diff --git a/tests/ApiTest.php b/tests/ApiTest.php index 4e16bdd..8ade525 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -180,7 +180,7 @@ public function testClientCreation() public function testTimeDeltaCompute() { $client = new MockClient( - new Response(200, [], time() - 10), + new Response(200, [], (string)(time() - 10)), new Response(200, [], '{}'), ); @@ -326,6 +326,55 @@ public function testGetBooleanQueryArgs() $this->assertSame('https://eu.api.ovh.com/1.0/me/api/credential?dryRun=true¬DryRun=false', $req->getUri()->__toString()); } + /** + * Test GET non-finite float query args + */ + public function testGetNonFiniteFloatQueryArgs() + { + $client = new MockClient(new Response(200, [], '{}')); + + $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client); + $api->get('/me/api/credential', ['nan' => NAN, 'inf' => INF, 'negativeInf' => -INF], null, false); + + $calls = $client->calls; + $this->assertCount(1, $calls); + + $req = $calls[0]['request']; + $this->assertSame('GET', $req->getMethod()); + $this->assertSame('https://eu.api.ovh.com/1.0/me/api/credential?nan=NAN&inf=INF&negativeInf=-INF', $req->getUri()->__toString()); + } + + /** + * Test custom scalar headers + */ + public function testCustomScalarHeaders() + { + $client = new MockClient(new Response(200, [], '{}')); + + $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client); + $api->get('/me', null, [ + 'X-Int' => 123, + 'X-Float' => 1.5, + 'X-Bool-True' => true, + 'X-Bool-False' => false, + 'X-Null' => null, + 'X-Array' => [123, true, null], + 'X-Empty-Array' => [], + ], false); + + $calls = $client->calls; + $this->assertCount(1, $calls); + + $req = $calls[0]['request']; + $this->assertSame('123', $req->getHeaderLine('X-Int')); + $this->assertSame('1.5', $req->getHeaderLine('X-Float')); + $this->assertSame('1', $req->getHeaderLine('X-Bool-True')); + $this->assertSame('', $req->getHeaderLine('X-Bool-False')); + $this->assertSame('', $req->getHeaderLine('X-Null')); + $this->assertSame(['123', '1', ''], $req->getHeader('X-Array')); + $this->assertFalse($req->hasHeader('X-Empty-Array')); + } + /** * Test valid provided endpoint */