Start of work to replace Guzzle with Artax

This commit is contained in:
Timothy Warren 2017-02-08 00:44:57 -05:00
parent 02838c5024
commit deecb5a912
9 changed files with 323 additions and 115 deletions

39
src/API/APIClient.php Normal file
View File

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @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));
}
}

View File

@ -17,8 +17,8 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use Amp\Artax\{ use Amp\Artax\{
Client, Client,
FormBody, FormBody,
Request Request
}; };
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
@ -30,56 +30,58 @@ use Psr\Log\LoggerAwareTrait;
*/ */
class APIRequestBuilder { class APIRequestBuilder {
use LoggerAwareTrait; use LoggerAwareTrait;
/** /**
* Url prefix for making url requests * Url prefix for making url requests
* @var string * @var string
*/ */
protected $baseUrl = ''; protected $baseUrl = '';
/** /**
* Url path of the request * Url path of the request
* @var string * @var string
*/ */
protected $path = ''; protected $path = '';
/** /**
* Query string for the request * Query string for the request
* @var string * @var string
*/ */
protected $query = ''; protected $query = '';
/** /**
* Default request headers * Default request headers
* @var array * @var array
*/ */
protected $defaultHeaders = []; protected $defaultHeaders = [];
/** /**
* Valid HTTP request methos * Valid HTTP request methos
* @var array * @var array
*/ */
protected $validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']; protected $validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
/** /**
* The current request * The current request
* @var \Amp\Promise * @var \Amp\Promise
*/ */
protected $request; protected $request;
/** /**
* Set body as form fields * Set a basic authentication header
* *
* @param array $fields Mapping of field names to values * @param string $username
* @param string $password
* @return self * @return self
*/ */
public function setFormFields(array $fields): self public function setBasicAuth(string $username, string $password): self
{ {
$body = $this->fixBody((new FormBody)->addFields($createData)); $authString = 'Basic ' . base64_encode($username . ':' . $password);
$this->setBody($body); $this->setHeader('Authorization', $authString);
return $this; return $this;
} }
/** /**
* Set the request body * Set the request body
* *
@ -91,7 +93,21 @@ class APIRequestBuilder {
$this->request->setBody($body); $this->request->setBody($body);
return $this; 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 * Set a request header
* *
@ -104,10 +120,10 @@ class APIRequestBuilder {
$this->request->setHeader($name, $value); $this->request->setHeader($name, $value);
return $this; return $this;
} }
/** /**
* Set multiple request headers * Set multiple request headers
* *
* name => value * name => value
* *
* @param array $headers * @param array $headers
@ -119,10 +135,10 @@ class APIRequestBuilder {
{ {
$this->setHeader($name, $value); $this->setHeader($name, $value);
} }
return $this; return $this;
} }
/** /**
* Append a query string in array format * Append a query string in array format
* *
@ -131,10 +147,10 @@ class APIRequestBuilder {
*/ */
public function setQuery(array $params): self public function setQuery(array $params): self
{ {
$this->query = http_build_query($params); $this->query = http_build_query($params);
return $this; return $this;
} }
/** /**
* Return the promise for the current request * Return the promise for the current request
* *
@ -143,9 +159,19 @@ class APIRequestBuilder {
public function getFullRequest() public function getFullRequest()
{ {
$this->buildUri(); $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; return $this->request;
} }
/** /**
* Create a new http request * Create a new http request
* *
@ -159,16 +185,23 @@ class APIRequestBuilder {
{ {
throw new InvalidArgumentException('Invalid HTTP methods'); throw new InvalidArgumentException('Invalid HTTP methods');
} }
$this->resetState(); $this->resetState();
$this->request $this->request
->setMethod($type) ->setMethod($type)
->setProtocol('1.1'); ->setProtocol('1.1');
$this->path = $uri;
if ( ! empty($this->defaultHeaders))
{
$this->setHeaders($this->defaultHeaders);
}
return $this; return $this;
} }
/** /**
* Create the full request url * Create the full request url
* *
@ -178,16 +211,16 @@ class APIRequestBuilder {
{ {
$url = (strpos($this->path, '//') !== FALSE) $url = (strpos($this->path, '//') !== FALSE)
? $this->path ? $this->path
: $this->baseUrl . $url; : $this->baseUrl . $this->path;
if ( ! empty($this->query)) if ( ! empty($this->query))
{ {
$url .= '?' . $this->query; $url .= '?' . $this->query;
} }
$this->request->setUri($url); $this->request->setUri($url);
} }
/** /**
* Unencode the dual-encoded ampersands in the body * Unencode the dual-encoded ampersands in the body
* *
@ -202,7 +235,7 @@ class APIRequestBuilder {
$rawBody = \Amp\wait($formBody->getBody()); $rawBody = \Amp\wait($formBody->getBody());
return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8');
} }
/** /**
* Reset the class state for a new request * Reset the class state for a new request
* *

View File

@ -37,30 +37,35 @@ class ListItem extends AbstractListItem {
public function create(array $data): bool public function create(array $data): bool
{ {
$response = $this->getResponse('POST', 'library-entries', [ $body = [
'body' => Json::encode([ 'data' => [
'data' => [ 'type' => 'libraryEntries',
'type' => 'libraryEntries', 'attributes' => [
'attributes' => [ 'status' => $data['status'],
'status' => $data['status'], 'progress' => $data['progress'] ?? 0
'progress' => $data['progress'] ?? 0 ],
'relationships' => [
'user' => [
'data' => [
'id' => $data['user_id'],
'type' => 'users'
]
], ],
'relationships' => [ 'media' => [
'user' => [ 'data' => [
'data' => [ 'id' => $data['id'],
'id' => $data['user_id'], 'type' => $data['type']
'type' => 'users'
]
],
'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); return ($response->getStatusCode() === 201);
@ -74,11 +79,19 @@ class ListItem extends AbstractListItem {
public function get(string $id): array 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' => [ 'query' => [
'include' => 'media,media.genres,media.mappings' '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 public function update(string $id, array $data): Response

View File

@ -116,7 +116,6 @@ class AnimeListTransformer extends AbstractTransformer {
'mal_id' => $item['mal_id'] ?? null, 'mal_id' => $item['mal_id'] ?? null,
'data' => [ 'data' => [
'status' => $item['watching_status'], 'status' => $item['watching_status'],
'rating' => $item['user_rating'] / 2,
'reconsuming' => $rewatching, 'reconsuming' => $rewatching,
'reconsumeCount' => $item['rewatched'], 'reconsumeCount' => $item['rewatched'],
'notes' => $item['notes'], 'notes' => $item['notes'],
@ -124,10 +123,10 @@ class AnimeListTransformer extends AbstractTransformer {
'private' => $privacy '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; return $untransformed;

View File

@ -30,7 +30,7 @@ class ListItem {
use ContainerAware; use ContainerAware;
use MALTrait; use MALTrait;
public function create(array $data): bool public function create(array $data)
{ {
$id = $data['id']; $id = $data['id'];
$createData = [ $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", [ $response = $this->getResponse('POST', "animelist/add/{$id}.xml", [
'body' => $this->fixBody((new FormBody)->addFields($createData)) 'body' => $this->fixBody((new FormBody)->addFields($createData))
]); ]);
return $response->getBody() === 'Created'; return $response->getBody() === 'Created';
// return $request;
} }
public function delete(string $id): bool public function delete(string $id): bool

View File

@ -23,7 +23,7 @@ use Aviat\AnimeClient\API\{
}; };
class MALRequestBuilder extends APIRequestBuilder { class MALRequestBuilder extends APIRequestBuilder {
/** /**
* The base url for api requests * The base url for api requests
* @var string $base_url * @var string $base_url
@ -41,7 +41,7 @@ class MALRequestBuilder extends APIRequestBuilder {
'Content-type' => 'application/x-www-form-urlencoded', 'Content-type' => 'application/x-www-form-urlencoded',
'User-Agent' => "Tim's Anime Client/4.0" 'User-Agent' => "Tim's Anime Client/4.0"
]; ];
/** /**
* Valid HTTP request methos * Valid HTTP request methos
* @var array * @var array

View File

@ -27,7 +27,7 @@ use Aviat\Ion\Json;
use InvalidArgumentException; use InvalidArgumentException;
trait MALTrait { trait MALTrait {
/** /**
* The request builder for the MAL API * The request builder for the MAL API
* @var MALRequestBuilder * @var MALRequestBuilder
@ -51,7 +51,7 @@ trait MALTrait {
'Content-type' => 'application/x-www-form-urlencoded', 'Content-type' => 'application/x-www-form-urlencoded',
'User-Agent' => "Tim's Anime Client/4.0" 'User-Agent' => "Tim's Anime Client/4.0"
]; ];
/** /**
* Set the request builder object * Set the request builder object
* *
@ -63,7 +63,7 @@ trait MALTrait {
$this->requestBuilder = $requestBuilder; $this->requestBuilder = $requestBuilder;
return $this; return $this;
} }
/** /**
* Unencode the dual-encoded ampersands in the body * Unencode the dual-encoded ampersands in the body
* *
@ -78,7 +78,7 @@ trait MALTrait {
$rawBody = \Amp\wait($formBody->getBody()); $rawBody = \Amp\wait($formBody->getBody());
return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8');
} }
/** /**
* Create a request object * Create a request object
* *
@ -89,47 +89,23 @@ trait MALTrait {
*/ */
public function setUpRequest(string $type, string $url, array $options = []) 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'); $config = $this->container->get('config');
$logger = $this->container->getLogger('mal-request');
$headers = array_merge($this->defaultHeaders, $options['headers'] ?? [], [ $request = $this->requestBuilder
'Authorization' => 'Basic ' . ->newRequest($type, $url)
base64_encode($config->get(['mal','username']) . ':' .$config->get(['mal','password'])) ->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password']));
]);
$query = $options['query'] ?? []; if (array_key_exists('query', $options))
$url = (strpos($url, '//') !== FALSE)
? $url
: $this->baseUrl . $url;
if ( ! empty($query))
{ {
$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)) if (array_key_exists('body', $options))
{ {
$request->setBody($options['body']); $request->setBody($options['body']);
} }
return $request; return $request->getFullRequest();
} }
/** /**
@ -147,19 +123,16 @@ trait MALTrait {
{ {
$logger = $this->container->getLogger('mal-request'); $logger = $this->container->getLogger('mal-request');
} }
$request = $this->setUpRequest($type, $url, $options); $request = $this->setUpRequest($type, $url, $options);
$response = \Amp\wait((new Client)->request($request)); $response = \Amp\wait((new Client)->request($request));
$logger->debug('MAL api request', [ $logger->debug('MAL api response', [
'url' => $url,
'status' => $response->getStatus(), 'status' => $response->getStatus(),
'reason' => $response->getReason(), 'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getAllHeaders(), 'headers' => $response->getAllHeaders(),
'requestHeaders' => $request->getAllHeaders(), 'requestHeaders' => $request->getAllHeaders(),
'requestBody' => $request->hasBody() ? $request->getBody() : 'No request body',
'requestBodyBeforeEncode' => $request->hasBody() ? urldecode($request->getBody()) : '',
'body' => $response->getBody()
]); ]);
return $response; return $response;

View File

@ -56,27 +56,33 @@ class AnimeListTransformer extends AbstractTransformer {
* @return array * @return array
*/ */
public function untransform(array $item): array public function untransform(array $item): array
{ {
$rewatching = (array_key_exists('reconsuming', $item['data']) && $item['data']['reconsuming']);
$map = [ $map = [
'id' => $item['mal_id'], 'id' => $item['mal_id'],
'data' => [ 'data' => [
'episode' => $item['data']['progress'], 'episode' => $item['data']['progress']
// 'enable_rewatching' => $rewatching,
// 'times_rewatched' => $item['data']['reconsumeCount'],
// 'comments' => $item['data']['notes'],
] ]
]; ];
if (array_key_exists('rating', $item['data'])) switch(TRUE)
{ {
$map['data']['score'] = $item['data']['rating'] * 2; case array_key_exists('notes', $item['data']):
} $map['data']['comments'] = $item['data']['notes'];
if (array_key_exists('status', $item['data'])) case array_key_exists('rating', $item['data']):
{ $map['data']['score'] = $item['data']['rating'] * 2;
$map['data']['status'] = self::statusMap[$item['data']['status']];
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; return $map;

View File

@ -0,0 +1,136 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @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']);
}
}