Start of work to replace Guzzle with Artax

This commit is contained in:
Timothy Warren 2017-02-08 00:44:57 -05:00
parent 5f0f830aea
commit 5aafbc9cb2
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;
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
*

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

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']);
}
}