Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
19 changed files with 638 additions and 68 deletions
Showing only changes of commit e2e27c2311 - Show all commits

View File

@ -10,6 +10,7 @@ use Aura\Web\WebFactory;
use Aura\Router\RouterFactory;
use Aura\Session\SessionFactory;
use Aviat\Ion\Di\Container;
use Aviat\AnimeClient\Auth\HummingbirdAuth;
// -----------------------------------------------------------------------------
// Setup DI container
@ -54,6 +55,7 @@ return function(array $config_array = []) {
$container->set('session', $session);
$container->set('url-generator', new UrlGenerator($container));
$container->set('auth', new HummingbirdAuth($container));
// -------------------------------------------------------------------------
// Dispatcher

View File

@ -10,6 +10,8 @@ namespace Aviat\AnimeClient;
*/
class Config {
use \Aviat\Ion\ArrayWrapper;
/**
* Config object
*
@ -30,11 +32,16 @@ class Config {
/**
* Get a config value
*
* @param string $key
* @param array|string $key
* @return mixed
*/
public function get($key)
{
if (is_array($key))
{
return $this->get_deep_key($key, FALSE);
}
if (array_key_exists($key, $this->map))
{
return $this->map[$key];
@ -43,16 +50,84 @@ class Config {
return NULL;
}
/**
* Return a reference to an arbitrary key on the config map
* @param array $key
* @param bool $create Whether to create the missing array keys
* @return mixed
*/
protected function &get_deep_key(array $key, $create = TRUE)
{
$pos =& $this->map;
// Create the start of the array if it doesn't exist
if ($create && ! is_array($pos))
{
$pos = [];
}
elseif ( ! is_array($pos))
{
return NULL;
}
// Iterate through the levels of the array,
// create the levels if they don't exist
foreach($key as $level)
{
if ($create && empty($pos) && ! is_array($pos))
{
$pos = [];
$pos[$level] = [];
}
$pos =& $pos[$level];
}
return $pos;
}
/**
* Remove a config value
*
* @param string|array $key
* @return void
*/
public function delete($key)
{
$pos =& $this->map;
if (is_array($key))
{
$pos =& $this->arr($this->map)->get_deep_key($key);
}
else
{
$pos =& $this->map[$key];
}
unset($pos);
}
/**
* Set a config value
*
* @param string $key
* @param string|array $key
* @param mixed $value
* @return Config
*/
public function set($key, $value)
{
$this->map[$key] = $value;
$pos =& $this->map;
if (is_array($key))
{
$pos =& $this->get_deep_key($key);
$pos = $value;
}
else
{
$pos[$key] = $value;
}
return $this;
}
}

View File

@ -43,11 +43,6 @@ class Anime extends BaseController {
{
parent::__construct($container);
if ($this->config->get('show_anime_collection') === FALSE)
{
unset($this->nav_routes['Collection']);
}
$this->model = new AnimeModel($container);
$this->collection_model = new AnimeCollectionModel($container);
$this->base_data = array_merge($this->base_data, [
@ -109,7 +104,8 @@ class Anime extends BaseController {
'completed' => AnimeWatchingStatus::COMPLETED
];
$title = $this->config->get('whose_list') . "'s Anime List · {$type_title_map[$type]}";
$title = $this->config->get('whose_list') .
"'s Anime List · {$type_title_map[$type]}";
$view_map = [
'' => 'cover',

View File

@ -19,13 +19,6 @@ class MenuGenerator extends UrlGenerator {
*/
protected $helper;
/**
* Menu config array
*
* @var array
*/
protected $menus;
/**
* Request object
*
@ -41,7 +34,6 @@ class MenuGenerator extends UrlGenerator {
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->menus = $this->config->get('menus');
$this->helper = $container->get('html-helper');
$this->request = $container->get('request');
}
@ -51,11 +43,11 @@ class MenuGenerator extends UrlGenerator {
*
* @return array
*/
protected function parse_config()
protected function parse_config(array $menus)
{
$parsed = [];
foreach ($this->menus as $name => $menu)
foreach ($menus as $name => $menu)
{
$parsed[$name] = [];
foreach ($menu['items'] as $path_name => $partial_path)
@ -76,7 +68,8 @@ class MenuGenerator extends UrlGenerator {
*/
public function generate($menu)
{
$parsed_config = $this->parse_config();
$menus = $this->config->get('menus');
$parsed_config = $this->parse_config($menus);
// Bail out early on invalid menu
if ( ! $this->arr($parsed_config)->has_key($menu))

View File

@ -7,12 +7,21 @@ namespace Aviat\AnimeClient\Model;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\ResponseInterface;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Model as BaseModel;
/**
* Base model for api interaction
*
* @method ResponseInterface get(string $uri, array $options);
* @method ResponseInterface delete(string $uri, array $options);
* @method ResponseInterface head(string $uri, array $options);
* @method ResponseInterface options(string $uri, array $options);
* @method ResponseInterface patch(string $uri, array $options);
* @method ResponseInterface post(string $uri, array $options);
* @method ResponseInterface put(string $uri, array $options);
*/
class API extends BaseModel {
@ -58,6 +67,35 @@ class API extends BaseModel {
]);
}
/**
* Magic methods to call guzzle api client
*
* @param string $method
* @param array $args
* @return ResponseInterface|null
*/
public function __call($method, $args)
{
$valid_methods = [
'get',
'delete',
'head',
'options',
'patch',
'post',
'put'
];
if ( ! in_array($method, $valid_methods))
{
return NULL;
}
array_unshift($args, strtoupper($method));
$response = call_user_func_array([$this->client, 'request'], $args);
return $response;
}
/**
* Attempt login via the api
*
@ -68,7 +106,7 @@ class API extends BaseModel {
*/
public function authenticate($username, $password)
{
$result = $this->client->post('https://hummingbird.me/api/v1/users/authenticate', [
$result = $this->post('https://hummingbird.me/api/v1/users/authenticate', [
'body' => [
'username' => $username,
'password' => $password

View File

@ -5,7 +5,6 @@
namespace Aviat\AnimeClient\Model;
use Aviat\AnimeClient\Model\API;
use Aviat\AnimeClient\Hummingbird\Enum\AnimeWatchingStatus;
use Aviat\AnimeClient\Hummingbird\Transformer\AnimeListTransformer;
@ -50,11 +49,11 @@ class Anime extends API {
// @TODO use Hummingbird Auth class
$data['auth_token'] = '';
$result = $this->client->post("libraries/{$data['id']}", [
$response = $this->client->post("libraries/{$data['id']}", [
'body' => $data
]);
return $result->json();
return json_decode($result->getBody(), TRUE);
}
/**
@ -140,7 +139,7 @@ class Anime extends API {
]
];
$response = $this->client->get('search/anime', $config);
$response = $this->get('search/anime', $config);
$errorHandler->addDataTable('anime_search_response', (array)$response);
if ($response->getStatusCode() != 200)
@ -148,7 +147,7 @@ class Anime extends API {
throw new RuntimeException($response->getEffectiveUrl());
}
return $response->json();
return json_decode($response->getBody(), TRUE);
}
/**
@ -170,7 +169,7 @@ class Anime extends API {
}
$username = $this->config->get('hummingbird_username');
$response = $this->client->get("users/{$username}/library", $config);
$response = $this->get("users/{$username}/library", $config);
$output = $this->_check_cache($status, $response);
foreach ($output as &$row)

View File

@ -6,7 +6,6 @@
namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Model\DB;
use Aviat\AnimeClient\Model\Anime as AnimeModel;
/**
@ -43,8 +42,15 @@ class AnimeCollection extends DB {
$db_file_name = $this->db_config['collection']['file'];
if ($db_file_name !== ':memory:')
{
$db_file = file_get_contents($db_file_name);
$this->valid_database = (strpos($db_file, 'SQLite format 3') === 0);
if ( ! file_exists($db_file_name))
{
$this->valid_data = FALSE;
}
else
{
$db_file = file_get_contents($db_file_name);
$this->valid_database = (strpos($db_file, 'SQLite format 3') === 0);
}
}
else
{

View File

@ -49,12 +49,12 @@ class Manga extends API {
unset($data['id']);
// @TODO update with auth key from auth class
$result = $this->client->put("manga_library_entries/{$id}", [
$result = $this->put("manga_library_entries/{$id}", [
'cookies' => ['token' => ''],
'json' => ['manga_library_entry' => $data]
]);
return $result->json();
return json_decode($result->getBody(), TRUE);
}
/**
@ -90,7 +90,7 @@ class Manga extends API {
/**
* Retrieve the list from the hummingbird api
*
*
* @param string $status
* @return array
*/
@ -104,7 +104,7 @@ class Manga extends API {
'allow_redirects' => FALSE
];
$response = $this->client->get('manga_library_entries', $config);
$response = $this->get('manga_library_entries', $config);
$data = $this->_check_cache($response);
$output = $this->map_by_status($data);

View File

@ -61,7 +61,7 @@ class ArrayType {
*
* @param array $arr
*/
public function __construct(array $arr)
public function __construct(array &$arr)
{
$this->arr =& $arr;
}
@ -154,5 +154,41 @@ class ArrayType {
{
return in_array($value, $this->arr, $strict);
}
/**
* Return the array
*
* @return array
*/
public function get()
{
return $this->arr;
}
/**
* Return a reference to the value of an arbitrary key on the array
*
* @param array $key
* @return mixed
*/
public function &get_deep_key(array $key)
{
$pos =& $this->arr;
// Create the start of the array if it doesn't exist
if ( ! is_array($pos))
{
return NULL;
}
// Iterate through the levels of the array,
// create the levels if they don't exist
foreach($key as $level)
{
$pos =& $pos[$level];
}
return $pos;
}
}
// End of ArrayType.php

View File

@ -26,6 +26,7 @@ class HttpView extends BaseView {
/**
* Send the appropriate response
*
* @codeCoverageIgnore
* @return void
*/
protected function output()

View File

@ -20,6 +20,83 @@ class ConfigTest extends AnimeClient_TestCase {
$this->assertNull($this->config->get('baz'));
}
public function testConfigSet()
{
$this->config->set('foo', 'foobar');
$this->assertEquals('foobar', $this->config->get('foo'));
$this->config->set(['apple', 'sauce', 'is'], 'great');
$this->assertEquals('great', $this->config->get(['apple', 'sauce', 'is']));
}
public function dataConfigDelete()
{
return [
'top level delete' => [
'key' => 'apple',
'assertKeys' => [
[
'path' => ['apple', 'sauce', 'is'],
'expected' => NULL
],
[
'path' => ['apple', 'sauce'],
'expected' => NULL
],
[
'path' => 'apple',
'expected' => NULL
]
]
],
'mid level delete' => [
'key' => ['apple', 'sauce'],
'assertKeys' => [
[
'path' => ['apple', 'sauce', 'is'],
'expected' => NULL
],
[
'path' => ['apple', 'sauce'],
'expected' => NULL
],
[
'path' => 'apple',
'expected' => []
]
]
],
'deep delete' => [
'key' => ['apple', 'sauce', 'is'],
'assertKeys' => [
[
'path' => ['apple', 'sauce', 'is'],
'expected' => NULL
],
[
'path' => ['apple', 'sauce'],
'expected' => NULL
]
]
]
];
}
/**
* @dataProvider dataConfigDelete
*/
public function testConfigDelete($key, $assertKeys)
{
$this->markTestIncomplete();
$this->config->set(['apple', 'sauce', 'is'], 'great');
$this->config->delete($key);
foreach($assertKeys as $pair)
{
$this->assertEquals($pair['expected'], $this->config->get($pair['path']));
}
}
public function testGetNonExistentConfigItem()
{
$this->assertNull($this->config->get('foobar'));

View File

@ -1,5 +1,8 @@
<?php
use \Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller\Anime as AnimeController;
use Aviat\AnimeClient\Controller\Manga as MangaController;
use Aviat\AnimeClient\Controller\Collection as CollectionController;
use \Aura\Web\WebFactory;
use \Aura\Router\RouterFactory;
@ -20,9 +23,34 @@ class ControllerTest extends AnimeClient_TestCase {
$this->container->set('request', $web_factory->newRequest());
$this->container->set('response', $web_factory->newResponse());
$this->BaseController = new Controller($this->container);
}
public function testControllersSanity()
{
$config = $this->container->get('config');
$config->set(['database', 'collection'], [
'type' => 'sqlite',
'database' => '',
'file' => ":memory:"
]);
$this->container->set('config', $config);
$this->assertInstanceOf(
'Aviat\AnimeClient\Controller',
new AnimeController($this->container)
);
$this->assertInstanceOf(
'Aviat\AnimeClient\Controller',
new MangaController($this->container)
);
$this->assertInstanceOf(
'Aviat\AnimeClient\Controller',
new CollectionController($this->container)
);
}
public function testBaseControllerSanity()
{
$this->assertTrue(is_object($this->BaseController));

View File

@ -16,23 +16,7 @@ class MenuGeneratorTest extends AnimeClient_TestCase {
public function setUp()
{
parent::setUp();
$config = $this->container->get('config');
$config->set('menus', [
'anime_list' => [
'route_prefix' => '/anime',
'items' => [
'watching' => '/watching',
'plan_to_watch' => '/plan_to_watch',
'on_hold' => '/on_hold',
'dropped' => '/dropped',
'completed' => '/completed',
'all' => '/all'
]
],
]);
$this->generator = new MenuGenerator($this->container);
}
public function testSanity()
@ -44,6 +28,19 @@ class MenuGeneratorTest extends AnimeClient_TestCase {
public function testParseConfig()
{
$friend = new Friend($this->generator);
$menus = [
'anime_list' => [
'route_prefix' => '/anime',
'items' => [
'watching' => '/watching',
'plan_to_watch' => '/plan_to_watch',
'on_hold' => '/on_hold',
'dropped' => '/dropped',
'completed' => '/completed',
'all' => '/all'
]
],
];
$expected = [
'anime_list' => [
'Watching' => '/anime/watching',
@ -54,6 +51,6 @@ class MenuGeneratorTest extends AnimeClient_TestCase {
'All' => '/anime/all'
]
];
$this->assertEquals($expected, $friend->parse_config());
$this->assertEquals($expected, $friend->parse_config($menus));
}
}

View File

@ -6,6 +6,8 @@ use Aviat\AnimeClient\Model\API as BaseApiModel;
class MockBaseApiModel extends BaseApiModel {
protected $base_url = 'https://httpbin.org/';
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
@ -19,9 +21,15 @@ class MockBaseApiModel extends BaseApiModel {
class BaseApiModelTest extends AnimeClient_TestCase {
public function setUp()
{
parent::setUp();
$this->model = new MockBaseApiModel($this->container);
}
public function testBaseApiModelSanity()
{
$baseApiModel = new MockBaseApiModel($this->container);
$baseApiModel = $this->model;
// Some basic type checks for class memebers
$this->assertInstanceOf('\Aviat\AnimeClient\Model', $baseApiModel);
@ -34,4 +42,186 @@ class BaseApiModelTest extends AnimeClient_TestCase {
$this->assertTrue(empty($baseApiModel->base_url));
}
public function dataClient()
{
$host = gethostname();
$ip = gethostbyname($host);
$user_agent = "Tim's Anime Client/2.0";
$headers = [
'User-Agent' => $user_agent
];
return [
'invalid' => [
'method' => 'foo',
'uri' => '',
'options' => [],
'expected' => NULL,
'is_json' => FALSE,
],
'get' => [
'method' => 'get',
'uri' => '/get',
'options' => [
'query' => [
'foo' => 'bar'
],
'headers' => $headers
],
'expected' => [
'args' => [
'foo' => 'bar'
],
'headers' => [
'Host' => 'httpbin.org',
'User-Agent' => $user_agent
],
'origin' => $ip,
'url' => 'https://httpbin.org/get?foo=bar'
],
'is_json' => TRUE
],
'post' => [
'method' => 'post',
'uri' => '/post',
'options' => [
'form_params' => [
'foo' => 'bar',
'baz' => 'foobar'
],
'headers' => $headers
],
'expected' => [
'args' => [],
'data' => '',
'files' => [],
'form' => [
'foo' => 'bar',
'baz' => 'foobar'
],
'headers' => [
'Host' => 'httpbin.org',
'User-Agent' => $user_agent,
'Content-Length' => '18',
'Content-Type' => 'application/x-www-form-urlencoded'
],
'json' => NULL,
'origin' => $ip,
'url' => 'https://httpbin.org/post'
],
'is_json' => TRUE
],
'put' => [
'method' => 'put',
'uri' => '/put',
'options' => [
'form_params' => [
'foo' => 'bar',
'baz' => 'foobar'
],
'headers' => $headers
],
'expected' => [
'args' => [],
'data' => '',
'files' => [],
'form' => [
'foo' => 'bar',
'baz' => 'foobar'
],
'headers' => [
'Host' => 'httpbin.org',
'User-Agent' => $user_agent,
'Content-Length' => '18',
'Content-Type' => 'application/x-www-form-urlencoded'
],
'json' => NULL,
'origin' => $ip,
'url' => 'https://httpbin.org/put'
],
'is_json' => TRUE
],
'patch' => [
'method' => 'patch',
'uri' => '/patch',
'options' => [
'form_params' => [
'foo' => 'bar',
'baz' => 'foobar'
],
'headers' => $headers
],
'expected' => [
'args' => [],
'data' => '',
'files' => [],
'form' => [
'foo' => 'bar',
'baz' => 'foobar'
],
'headers' => [
'Host' => 'httpbin.org',
'User-Agent' => $user_agent,
'Content-Length' => '18',
'Content-Type' => 'application/x-www-form-urlencoded'
],
'json' => NULL,
'origin' => $ip,
'url' => 'https://httpbin.org/patch'
],
'is_json' => TRUE
],
'delete' => [
'method' => 'delete',
'uri' => '/delete',
'options' => [
'form_params' => [
'foo' => 'bar',
'baz' => 'foobar'
],
'headers' => $headers
],
'expected' => [
'args' => [],
'data' => '',
'files' => [],
'form' => [
'foo' => 'bar',
'baz' => 'foobar'
],
'headers' => [
'Host' => 'httpbin.org',
'User-Agent' => $user_agent,
'Content-Length' => '18',
'Content-Type' => 'application/x-www-form-urlencoded'
],
'json' => NULL,
'origin' => $ip,
'url' => 'https://httpbin.org/delete'
],
'is_json' => TRUE
]
];
}
/**
* @dataProvider dataClient
*/
public function testClient($method, $uri, $options, $expected, $is_json)
{
$result = $this->model->$method($uri, $options);
if (is_null($result))
{
$this->assertNull($expected);
return;
}
$actual = ($is_json)
? json_decode($result->getBody(), TRUE)
: (string) $result->getBody();
$this->assertEquals($expected, $actual);
}
}

View File

@ -0,0 +1,52 @@
<?php
include_once __DIR__ . "/../ViewTest.php";
use Aviat\Ion\Friend;
use Aviat\Ion\View;
use Aviat\Ion\View\HtmlView;
class TestHtmlView extends HtmlView {
protected function output() {
$reflect = new ReflectionClass($this);
$properties = $reflect->getProperties();
$props = [];
foreach($properties as $reflectProp)
{
$reflectProp->setAccessible(TRUE);
$props[$reflectProp->getName()] = $reflectProp->getValue($this);
}
$view = new TestView($this->container);
$friend = new Friend($view);
foreach($props as $name => $val)
{
$friend->__set($name, $val);
}
$friend->output();
}
}
class HtmlViewTest extends ViewTest {
protected $template_path = __DIR__ . "/../../test_views/";
public function setUp()
{
parent::setUp();
$this->view = new TestHtmlView($this->container);
}
public function testRenderTemplate()
{
$path = $this->template_path . 'test_view.php';
$expected = '<tag>foo</tag>';
$actual = $this->view->render_template($path, [
'var' => 'foo'
]);
$this->assertEquals($expected, $actual);
}
}

View File

@ -0,0 +1,46 @@
<?php
include_once __DIR__ . "/../ViewTest.php";
use Aviat\Ion\Friend;
use Aviat\Ion\View\HttpView;
class TestHttpView extends HttpView {
protected function output() {
$reflect = new ReflectionClass($this);
$properties = $reflect->getProperties();
$props = [];
foreach($properties as $reflectProp)
{
$reflectProp->setAccessible(TRUE);
$props[$reflectProp->getName()] = $reflectProp->getValue($this);
}
$view = new TestView($this->container);
$friend = new Friend($view);
foreach($props as $name => $val)
{
$friend->__set($name, $val);
}
$friend->output();
}
}
class HttpViewTest extends ViewTest {
public function setUp()
{
parent::setUp();
$this->view = new TestHttpView($this->container);
$this->friend = new Friend($this->view);
}
public function testRedirect()
{
$this->friend->redirect('/foo', 303);
$this->assertEquals('/foo', $this->friend->response->headers->get('Location'));
$this->assertEquals(303, $this->friend->response->status->getCode());
}
}

View File

@ -0,0 +1,44 @@
<?php
use Aviat\Ion\Friend;
use Aviat\Ion\View\JsonView;
include_once __DIR__ . "/../ViewTest.php";
class TestJsonView extends JsonView {
public function __destruct() {}
}
class JsonViewTest extends ViewTest {
public function setUp()
{
parent::setUp();
$this->view = new TestJsonView($this->container);
$this->friend = new Friend($this->view);
}
public function testSetOutput()
{
// Extend view class to remove destructor which does output
$view = new TestJsonView($this->container);
// Json encode non-string
$content = ['foo' => 'bar'];
$expected = json_encode($content);
$this->view->setOutput($content);
$this->assertEquals($expected, $this->view->getOutput());
// Directly set string
$content = '{}';
$expected = '{}';
$this->view->setOutput($content);
$this->assertEquals($expected, $this->view->getOutput());
}
public function testOutput()
{
$this->assertEquals('application/json', $this->friend->contentType);
}
}

View File

@ -3,8 +3,6 @@
use Aura\Web\WebFactory;
use Aviat\Ion\Friend;
use Aviat\Ion\View;
use Aviat\Ion\Di\Container;
class TestView extends View {
@ -16,16 +14,6 @@ class ViewTest extends AnimeClient_TestCase {
{
parent::setUp();
$web_factory = new WebFactory([
'_GET' => $_GET,
'_POST' => $_POST,
'_COOKIE' => $_COOKIE,
'_SERVER' => $_SERVER,
'_FILES' => $_FILES
]);
$this->container->set('request', $web_factory->newRequest());
$this->container->set('response', $web_factory->newResponse());
$this->view = new TestView($this->container);
$this->friend = new Friend($this->view);
}
@ -54,8 +42,9 @@ class ViewTest extends AnimeClient_TestCase {
{
$this->friend->contentType = 'text/html';
$this->friend->__destruct();
$this->assertEquals($this->friend->response->content->getType(), $this->friend->contentType);
$this->assertEquals($this->friend->response->content->getCharset(), 'utf-8');
$this->assertEquals($this->friend->response->content->get(), $this->friend->getOutput());
$content =& $this->friend->response->content;
$this->assertEquals($content->getType(), $this->friend->contentType);
$this->assertEquals($content->getCharset(), 'utf-8');
$this->assertEquals($content->get(), $this->friend->getOutput());
}
}

View File

@ -0,0 +1 @@
<tag><?= $var ?></tag>