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

@ -68,15 +68,17 @@ class APIRequestBuilder {
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;
} }
@ -92,6 +94,20 @@ class APIRequestBuilder {
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
* *
@ -143,6 +159,16 @@ 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;
} }
@ -166,6 +192,13 @@ class APIRequestBuilder {
->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;
} }
@ -178,7 +211,7 @@ 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))
{ {

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'],
@ -125,9 +124,9 @@ class AnimeListTransformer extends AbstractTransformer {
] ]
]; ];
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

@ -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();
} }
/** /**
@ -151,15 +127,12 @@ trait MALTrait {
$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

@ -57,26 +57,32 @@ class AnimeListTransformer extends AbstractTransformer {
*/ */
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']);
}
}