From deecb5a912aa7394b32ad256647caf778d37fccb Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 8 Feb 2017 00:44:57 -0500 Subject: [PATCH] Start of work to replace Guzzle with Artax --- src/API/APIClient.php | 39 +++++ src/API/APIRequestBuilder.php | 95 ++++++++---- src/API/Kitsu/ListItem.php | 57 +++++--- .../Transformer/AnimeListTransformer.php | 7 +- src/API/MAL/ListItem.php | 11 +- src/API/MAL/MALRequestBuilder.php | 4 +- src/API/MAL/MALTrait.php | 55 ++----- .../MAL/Transformer/AnimeListTransformer.php | 34 +++-- tests/API/APIRequestBuilderTest.php | 136 ++++++++++++++++++ 9 files changed, 323 insertions(+), 115 deletions(-) create mode 100644 src/API/APIClient.php create mode 100644 tests/API/APIRequestBuilderTest.php diff --git a/src/API/APIClient.php b/src/API/APIClient.php new file mode 100644 index 00000000..05d3b8a9 --- /dev/null +++ b/src/API/APIClient.php @@ -0,0 +1,39 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://github.com/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API; + +use Amp; +use Amp\Artax\{ + Client, + Response, + Request +} + +class APIClient { + + /** + * Get a syncronous response for a request + * + * @param Request $request + * @return Response + */ + static public function syncResponse(Request $request): Response + { + $client = new Client(); + return wait($client->request($request)); + } +} \ No newline at end of file diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php index 52fc91c2..0ea1f77a 100644 --- a/src/API/APIRequestBuilder.php +++ b/src/API/APIRequestBuilder.php @@ -17,8 +17,8 @@ namespace Aviat\AnimeClient\API; use Amp\Artax\{ - Client, - FormBody, + Client, + FormBody, Request }; use Aviat\Ion\Di\ContainerAware; @@ -30,56 +30,58 @@ use Psr\Log\LoggerAwareTrait; */ class APIRequestBuilder { use LoggerAwareTrait; - + /** * Url prefix for making url requests * @var string */ protected $baseUrl = ''; - + /** * Url path of the request * @var string */ protected $path = ''; - + /** * Query string for the request * @var string */ protected $query = ''; - + /** * Default request headers * @var array */ protected $defaultHeaders = []; - + /** * Valid HTTP request methos * @var array */ protected $validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']; - + /** * The current request * @var \Amp\Promise */ protected $request; - + /** - * Set body as form fields - * - * @param array $fields Mapping of field names to values + * Set a basic authentication header + * + * @param string $username + * @param string $password * @return self */ - public function setFormFields(array $fields): self + public function setBasicAuth(string $username, string $password): self { - $body = $this->fixBody((new FormBody)->addFields($createData)); - $this->setBody($body); + $authString = 'Basic ' . base64_encode($username . ':' . $password); + $this->setHeader('Authorization', $authString); + return $this; } - + /** * Set the request body * @@ -91,7 +93,21 @@ class APIRequestBuilder { $this->request->setBody($body); return $this; } - + + /** + * Set body as form fields + * + * @param array $fields Mapping of field names to values + * @return self + */ + public function setFormFields(array $fields): self + { + $this->setHeader("Content-Type", "application/x-www-form-urlencoded"); + $body = $this->fixBody((new FormBody)->addFields($fields)); + $this->setBody($body); + return $this; + } + /** * Set a request header * @@ -104,10 +120,10 @@ class APIRequestBuilder { $this->request->setHeader($name, $value); return $this; } - + /** * Set multiple request headers - * + * * name => value * * @param array $headers @@ -119,10 +135,10 @@ class APIRequestBuilder { { $this->setHeader($name, $value); } - + return $this; } - + /** * Append a query string in array format * @@ -131,10 +147,10 @@ class APIRequestBuilder { */ public function setQuery(array $params): self { - $this->query = http_build_query($params); + $this->query = http_build_query($params); return $this; } - + /** * Return the promise for the current request * @@ -143,9 +159,19 @@ class APIRequestBuilder { public function getFullRequest() { $this->buildUri(); + + if ($this->logger) + { + $this->logger->debug('API Request', [ + 'request_url' => $this->request->getUri(), + 'request_headers' => $this->request->getAllHeaders(), + 'request_body' => $this->request->getBody() + ]); + } + return $this->request; } - + /** * Create a new http request * @@ -159,16 +185,23 @@ class APIRequestBuilder { { throw new InvalidArgumentException('Invalid HTTP methods'); } - + $this->resetState(); - + $this->request ->setMethod($type) ->setProtocol('1.1'); + $this->path = $uri; + + if ( ! empty($this->defaultHeaders)) + { + $this->setHeaders($this->defaultHeaders); + } + return $this; } - + /** * Create the full request url * @@ -178,16 +211,16 @@ class APIRequestBuilder { { $url = (strpos($this->path, '//') !== FALSE) ? $this->path - : $this->baseUrl . $url; + : $this->baseUrl . $this->path; if ( ! empty($this->query)) { $url .= '?' . $this->query; } - + $this->request->setUri($url); } - + /** * Unencode the dual-encoded ampersands in the body * @@ -202,7 +235,7 @@ class APIRequestBuilder { $rawBody = \Amp\wait($formBody->getBody()); return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); } - + /** * Reset the class state for a new request * diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index 57787f15..51121471 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -37,30 +37,35 @@ class ListItem extends AbstractListItem { public function create(array $data): bool { - $response = $this->getResponse('POST', 'library-entries', [ - 'body' => Json::encode([ - 'data' => [ - 'type' => 'libraryEntries', - 'attributes' => [ - 'status' => $data['status'], - 'progress' => $data['progress'] ?? 0 + $body = [ + 'data' => [ + 'type' => 'libraryEntries', + 'attributes' => [ + 'status' => $data['status'], + 'progress' => $data['progress'] ?? 0 + ], + 'relationships' => [ + 'user' => [ + 'data' => [ + 'id' => $data['user_id'], + 'type' => 'users' + ] ], - 'relationships' => [ - 'user' => [ - 'data' => [ - 'id' => $data['user_id'], - 'type' => 'users' - ] - ], - 'media' => [ - 'data' => [ - 'id' => $data['id'], - 'type' => $data['type'] - ] + 'media' => [ + 'data' => [ + 'id' => $data['id'], + 'type' => $data['type'] ] ] ] - ]) + ] + ]; + + $request = $this->requestBuilder->newRequest('POST', 'library-entries') + ->setJsonBody($body) + ->getFullRequest(); + $response = $this->getResponse('POST', 'library-entries', [ + 'body' => Json::encode($body) ]); return ($response->getStatusCode() === 201); @@ -74,11 +79,19 @@ class ListItem extends AbstractListItem { public function get(string $id): array { - return $this->getRequest("library-entries/{$id}", [ + $request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}") + ->setQuery([ + 'include' => 'media,media.genres,media.mappings' + ]) + ->getFullRequest(); + /*return $this->getRequest("library-entries/{$id}", [ 'query' => [ 'include' => 'media,media.genres,media.mappings' ] - ]); + ]);*/ + + $response = \Amp\wait((new \Amp\Artax\Client)->request($request)); + return Json::decode($response->getBody()); } public function update(string $id, array $data): Response diff --git a/src/API/Kitsu/Transformer/AnimeListTransformer.php b/src/API/Kitsu/Transformer/AnimeListTransformer.php index 2591aab0..cd87707b 100644 --- a/src/API/Kitsu/Transformer/AnimeListTransformer.php +++ b/src/API/Kitsu/Transformer/AnimeListTransformer.php @@ -116,7 +116,6 @@ class AnimeListTransformer extends AbstractTransformer { 'mal_id' => $item['mal_id'] ?? null, 'data' => [ 'status' => $item['watching_status'], - 'rating' => $item['user_rating'] / 2, 'reconsuming' => $rewatching, 'reconsumeCount' => $item['rewatched'], 'notes' => $item['notes'], @@ -124,10 +123,10 @@ class AnimeListTransformer extends AbstractTransformer { 'private' => $privacy ] ]; - - if ((int) $untransformed['data']['rating'] === 0) + + if ( ! empty($item['user_rating'])) { - unset($untransformed['data']['rating']); + $untransformed['data']['rating'] = $item['user_rating'] / 2; } return $untransformed; diff --git a/src/API/MAL/ListItem.php b/src/API/MAL/ListItem.php index 715be392..8c87b166 100644 --- a/src/API/MAL/ListItem.php +++ b/src/API/MAL/ListItem.php @@ -30,7 +30,7 @@ class ListItem { use ContainerAware; use MALTrait; - public function create(array $data): bool + public function create(array $data) { $id = $data['id']; $createData = [ @@ -40,11 +40,20 @@ class ListItem { ]) ]; + // $config = $this->container->get('config'); + + /*$request = $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml") + ->setFormFields($createData) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) + ->getFullRequest();*/ + $response = $this->getResponse('POST', "animelist/add/{$id}.xml", [ 'body' => $this->fixBody((new FormBody)->addFields($createData)) ]); return $response->getBody() === 'Created'; + + // return $request; } public function delete(string $id): bool diff --git a/src/API/MAL/MALRequestBuilder.php b/src/API/MAL/MALRequestBuilder.php index ca0d88b1..326ad44a 100644 --- a/src/API/MAL/MALRequestBuilder.php +++ b/src/API/MAL/MALRequestBuilder.php @@ -23,7 +23,7 @@ use Aviat\AnimeClient\API\{ }; class MALRequestBuilder extends APIRequestBuilder { - + /** * The base url for api requests * @var string $base_url @@ -41,7 +41,7 @@ class MALRequestBuilder extends APIRequestBuilder { 'Content-type' => 'application/x-www-form-urlencoded', 'User-Agent' => "Tim's Anime Client/4.0" ]; - + /** * Valid HTTP request methos * @var array diff --git a/src/API/MAL/MALTrait.php b/src/API/MAL/MALTrait.php index 5d8e84dc..8769f632 100644 --- a/src/API/MAL/MALTrait.php +++ b/src/API/MAL/MALTrait.php @@ -27,7 +27,7 @@ use Aviat\Ion\Json; use InvalidArgumentException; trait MALTrait { - + /** * The request builder for the MAL API * @var MALRequestBuilder @@ -51,7 +51,7 @@ trait MALTrait { 'Content-type' => 'application/x-www-form-urlencoded', 'User-Agent' => "Tim's Anime Client/4.0" ]; - + /** * Set the request builder object * @@ -63,7 +63,7 @@ trait MALTrait { $this->requestBuilder = $requestBuilder; return $this; } - + /** * Unencode the dual-encoded ampersands in the body * @@ -78,7 +78,7 @@ trait MALTrait { $rawBody = \Amp\wait($formBody->getBody()); return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); } - + /** * Create a request object * @@ -89,47 +89,23 @@ trait MALTrait { */ public function setUpRequest(string $type, string $url, array $options = []) { - $this->defaultHeaders['User-Agent'] = $_SERVER['HTTP_USER_AGENT'] ?? $this->defaultHeaders; - - $type = strtoupper($type); - $validTypes = ['GET', 'POST', 'DELETE']; - - if ( ! in_array($type, $validTypes)) - { - throw new InvalidArgumentException('Invalid http request type'); - } - $config = $this->container->get('config'); - $logger = $this->container->getLogger('mal-request'); - $headers = array_merge($this->defaultHeaders, $options['headers'] ?? [], [ - 'Authorization' => 'Basic ' . - base64_encode($config->get(['mal','username']) . ':' .$config->get(['mal','password'])) - ]); + $request = $this->requestBuilder + ->newRequest($type, $url) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password'])); - $query = $options['query'] ?? []; - - $url = (strpos($url, '//') !== FALSE) - ? $url - : $this->baseUrl . $url; - - if ( ! empty($query)) + if (array_key_exists('query', $options)) { - $url .= '?' . http_build_query($query); + $request->setQuery($options['query']); } - $request = (new Request) - ->setMethod($type) - ->setUri($url) - ->setProtocol('1.1') - ->setAllHeaders($headers); - if (array_key_exists('body', $options)) { $request->setBody($options['body']); } - - return $request; + + return $request->getFullRequest(); } /** @@ -147,19 +123,16 @@ trait MALTrait { { $logger = $this->container->getLogger('mal-request'); } - + $request = $this->setUpRequest($type, $url, $options); $response = \Amp\wait((new Client)->request($request)); - $logger->debug('MAL api request', [ - 'url' => $url, + $logger->debug('MAL api response', [ 'status' => $response->getStatus(), 'reason' => $response->getReason(), + 'body' => $response->getBody(), 'headers' => $response->getAllHeaders(), 'requestHeaders' => $request->getAllHeaders(), - 'requestBody' => $request->hasBody() ? $request->getBody() : 'No request body', - 'requestBodyBeforeEncode' => $request->hasBody() ? urldecode($request->getBody()) : '', - 'body' => $response->getBody() ]); return $response; diff --git a/src/API/MAL/Transformer/AnimeListTransformer.php b/src/API/MAL/Transformer/AnimeListTransformer.php index 38cf8182..9054f2c1 100644 --- a/src/API/MAL/Transformer/AnimeListTransformer.php +++ b/src/API/MAL/Transformer/AnimeListTransformer.php @@ -56,27 +56,33 @@ class AnimeListTransformer extends AbstractTransformer { * @return array */ public function untransform(array $item): array - { - $rewatching = (array_key_exists('reconsuming', $item['data']) && $item['data']['reconsuming']); - + { $map = [ 'id' => $item['mal_id'], 'data' => [ - 'episode' => $item['data']['progress'], - // 'enable_rewatching' => $rewatching, - // 'times_rewatched' => $item['data']['reconsumeCount'], - // 'comments' => $item['data']['notes'], + 'episode' => $item['data']['progress'] ] ]; - if (array_key_exists('rating', $item['data'])) + switch(TRUE) { - $map['data']['score'] = $item['data']['rating'] * 2; - } - - if (array_key_exists('status', $item['data'])) - { - $map['data']['status'] = self::statusMap[$item['data']['status']]; + case array_key_exists('notes', $item['data']): + $map['data']['comments'] = $item['data']['notes']; + + case array_key_exists('rating', $item['data']): + $map['data']['score'] = $item['data']['rating'] * 2; + + case array_key_exists('reconsuming', $item['data']): + $map['data']['enable_rewatching'] = (bool) $item['data']['reconsuming']; + + case array_key_exists('reconsumeCount', $item['data']): + $map['data']['times_rewatched'] = $item['data']['reconsumeCount']; + + case array_key_exists('status', $item['data']): + $map['data']['status'] = self::statusMap[$item['data']['status']]; + + default: + break; } return $map; diff --git a/tests/API/APIRequestBuilderTest.php b/tests/API/APIRequestBuilderTest.php new file mode 100644 index 00000000..4b3b10aa --- /dev/null +++ b/tests/API/APIRequestBuilderTest.php @@ -0,0 +1,136 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://github.com/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Tests\API; + +use Amp; +use Amp\Artax\Client; +use Aviat\AnimeClient\API\APIRequestBuilder; +use Aviat\Ion\Json; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; + +class APIRequestBuilderTest extends TestCase { + + public function setUp() + { + $this->builder = new class extends APIRequestBuilder { + protected $baseUrl = 'https://httpbin.org/'; + + protected $defaultHeaders = ['User-Agent' => "Tim's Anime Client Testsuite / 4.0"]; + }; + + $this->builder->setLogger(new NullLogger); + } + + public function testGzipRequest() + { + $request = $this->builder->newRequest('GET', 'gzip') + ->getFullRequest(); + $response = Amp\wait((new Client)->request($request)); + $body = Json::decode($response->getBody()); + $this->assertEquals(1, $body['gzipped']); + } + + public function testInvalidRequestMethod() + { + $this->expectException(\InvalidArgumentException::class); + $this->builder->newRequest('FOO', 'gzip') + ->getFullRequest(); + } + + public function testRequestWithBasicAuth() + { + $request = $this->builder->newRequest('GET', 'headers') + ->setBasicAuth('username', 'password') + ->getFullRequest(); + + $response = Amp\wait((new Client)->request($request)); + $body = Json::decode($response->getBody()); + + $this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']); + } + + public function testRequestWithQueryString() + { + $query = [ + 'foo' => 'bar', + 'bar' => [ + 'foo' => 'bar' + ], + 'baz' => [ + 'bar' => 'foo' + ] + ]; + + $expected = [ + 'foo' => 'bar', + 'bar[foo]' => 'bar', + 'baz[bar]' => 'foo' + ]; + + $request = $this->builder->newRequest('GET', 'get') + ->setQuery($query) + ->getFullRequest(); + + $response = Amp\wait((new Client)->request($request)); + $body = Json::decode($response->getBody()); + + $this->assertEquals($expected, $body['args']); + } + + public function testFormValueRequest() + { + $formValues = [ + 'foo' => 'bar', + 'bar' => 'foo' + ]; + + $request = $this->builder->newRequest('POST', 'post') + ->setFormFields($formValues) + ->getFullRequest(); + + $response = Amp\wait((new Client)->request($request)); + $body = Json::decode($response->getBody()); + + $this->assertEquals($formValues, $body['form']); + } + + public function testFullUrlRequest() + { + $data = [ + 'foo' => [ + 'bar' => 1, + 'baz' => [2, 3, 4], + 'bar' => [ + 'a' => 1, + 'b' => 2 + ] + ] + ]; + + $request = $this->builder->newRequest('PUT', 'https://httpbin.org/put') + ->setHeader('Content-Type', 'application/json') + ->setBody(Json::encode($data)) + ->getFullRequest(); + + $response = Amp\wait((new Client)->request($request)); + $body = Json::decode($response->getBody()); + + $this->assertEquals($data, $body['json']); + } +} +