diff --git a/README.md b/README.md
index f2e9de8..f21107c 100644
--- a/README.md
+++ b/README.md
@@ -11,3 +11,63 @@
AnimeNewsNetwork.com API browser
================================
+
+Installation
+------------
+
+Pretty simple with [Composer](http://packagist.org), run:
+
+```sh
+composer require anime-db/anime-news-network-browser-bundle
+```
+
+Add AnimeDbAnimeNewsNetworkBrowserBundle to your application kernel
+
+```php
+// app/appKernel.php
+
+public function registerBundles()
+{
+ $bundles = array(
+ // ...
+ new AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\AnimeDbAnimeNewsNetworkBrowserBundle(),
+ );
+}
+```
+
+Configuration
+-------------
+
+```yml
+anime_db_anime_news_network_browser:
+ # Host name
+ # As a default used 'https://cdn.animenewsnetwork.com'
+ host: 'https://cdn.animenewsnetwork.com'
+
+ # Reports
+ # As a default used '/encyclopedia/reports.xml'
+ reports: '/encyclopedia/reports.xml'
+
+ # Anime/Manga Details
+ # As a default used '/encyclopedia/api.xml'
+ details: '/encyclopedia/api.xml'
+
+ # HTTP User-Agent
+ # No default value
+ client: 'My Custom Bot 1.0'
+```
+
+Usage
+-----
+
+First get browser
+
+```php
+$browser = $this->get('anime_db.anime_news_network.browser');
+```
+
+License
+-------
+
+This bundle is under the [GPL v3 license](http://opensource.org/licenses/GPL-3.0).
+See the complete license in the file: LICENSE
diff --git a/src/AnimeDbAnimeNewsNetworkBrowserBundle.php b/src/AnimeDbAnimeNewsNetworkBrowserBundle.php
new file mode 100644
index 0000000..7b5b663
--- /dev/null
+++ b/src/AnimeDbAnimeNewsNetworkBrowserBundle.php
@@ -0,0 +1,17 @@
+
+ * @copyright Copyright (c) 2011, Peter Gribanov
+ * @license http://opensource.org/licenses/GPL-3.0 GPL v3
+ */
+
+namespace AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle;
+
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+class AnimeDbAnimeNewsNetworkBrowserBundle extends Bundle
+{
+}
diff --git a/src/DependencyInjection/AnimeDbAnimeNewsNetworkBrowserExtension.php b/src/DependencyInjection/AnimeDbAnimeNewsNetworkBrowserExtension.php
new file mode 100644
index 0000000..a404c3d
--- /dev/null
+++ b/src/DependencyInjection/AnimeDbAnimeNewsNetworkBrowserExtension.php
@@ -0,0 +1,38 @@
+
+ * @copyright Copyright (c) 2011, Peter Gribanov
+ * @license http://opensource.org/licenses/GPL-3.0 GPL v3
+ */
+
+namespace AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\DependencyInjection\Loader;
+
+class AnimeDbAnimeNewsNetworkBrowserExtension extends Extension
+{
+ /**
+ * @param array $configs
+ * @param ContainerBuilder $container
+ */
+ public function load(array $configs, ContainerBuilder $container)
+ {
+ $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
+ $loader->load('services.yml');
+
+ $config = $this->processConfiguration(new Configuration(), $configs);
+
+ $container->getDefinition('anime_db.anime_news_network.browser')
+ ->replaceArgument(1, $config['host'])
+ ->replaceArgument(2, $config['reports'])
+ ->replaceArgument(3, $config['details'])
+ ->replaceArgument(4, $config['client'])
+ ;
+ }
+}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
new file mode 100644
index 0000000..126f123
--- /dev/null
+++ b/src/DependencyInjection/Configuration.php
@@ -0,0 +1,57 @@
+
+ * @copyright Copyright (c) 2011, Peter Gribanov
+ * @license http://opensource.org/licenses/GPL-3.0 GPL v3
+ */
+
+namespace AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\DependencyInjection;
+
+use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
+
+class Configuration implements ConfigurationInterface
+{
+ /**
+ * Config tree builder.
+ *
+ * Example config:
+ *
+ * anime_db_anime_news_network_browser:
+ * host: 'https://cdn.animenewsnetwork.com'
+ * reports: '/encyclopedia/reports.xml'
+ * details: '/encyclopedia/api.xml'
+ * client: 'My Custom Bot 1.0'
+ *
+ * @return ArrayNodeDefinition
+ */
+ public function getConfigTreeBuilder()
+ {
+ return (new TreeBuilder())
+ ->root('anime_db_anime_news_network_browser')
+ ->children()
+ ->scalarNode('host')
+ ->defaultValue('https://cdn.animenewsnetwork.com')
+ ->cannotBeEmpty()
+ ->end()
+ ->scalarNode('reports')
+ ->defaultValue('/encyclopedia/reports.xml')
+ ->cannotBeEmpty()
+ ->end()
+ ->scalarNode('details')
+ ->defaultValue('/encyclopedia/api.xml')
+ ->cannotBeEmpty()
+ ->end()
+ ->scalarNode('client')
+ ->defaultValue('')
+ ->cannotBeEmpty()
+ ->end()
+ ->end()
+ ->end()
+ ;
+ }
+}
diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml
new file mode 100644
index 0000000..9a29627
--- /dev/null
+++ b/src/Resources/config/services.yml
@@ -0,0 +1,8 @@
+services:
+ anime_db.anime_news_network.browser:
+ class: AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\Service\Browser
+ arguments: [ '@anime_db.anime_news_network.browser.client', ~, ~, ~, ~ ]
+
+ anime_db.anime_news_network.browser.client:
+ class: GuzzleHttp\Client
+ public: false
diff --git a/src/Service/Browser.php b/src/Service/Browser.php
new file mode 100644
index 0000000..205e3f3
--- /dev/null
+++ b/src/Service/Browser.php
@@ -0,0 +1,100 @@
+
+ * @copyright Copyright (c) 2011, Peter Gribanov
+ * @license http://opensource.org/licenses/GPL-3.0 GPL v3
+ */
+
+namespace AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\Service;
+
+use GuzzleHttp\Client;
+
+class Browser
+{
+ /**
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * @var string
+ */
+ private $host;
+
+ /**
+ * @var string
+ */
+ private $reports;
+
+ /**
+ * @var string
+ */
+ private $details;
+
+ /**
+ * @var string
+ */
+ private $app_client;
+
+ /**
+ * @param Client $client
+ * @param string $host
+ * @param string $reports
+ * @param string $details
+ * @param string $app_client
+ */
+ public function __construct(Client $client, $host, $reports, $details, $app_client)
+ {
+ $this->client = $client;
+ $this->host = $host;
+ $this->reports = $reports;
+ $this->details = $details;
+ $this->app_client = $app_client;
+ }
+
+ /**
+ * @param int $id
+ * @param array $options
+ *
+ * @return string
+ */
+ public function reports($id, array $options = [])
+ {
+ $options['id'] = $id;
+
+ return $this->request($this->host.$this->reports, $options);
+ }
+
+ /**
+ * @param array $options
+ *
+ * @return string
+ */
+ public function details(array $options)
+ {
+ return $this->request($this->host.$this->details, $options);
+ }
+
+ /**
+ * @param string $url
+ * @param array $options
+ *
+ * @return string
+ */
+ private function request($url, array $options)
+ {
+ if ($this->app_client) {
+ $options['headers'] = array_merge(
+ ['User-Agent' => $this->app_client],
+ isset($options['headers']) ? $options['headers'] : []
+ );
+ }
+
+ $response = $this->client->request('GET', $url, $options);
+
+ return $response->getBody()->getContents();
+ }
+}
diff --git a/tests/DependencyInjection/AnimeDbAnimeNewsNetworkBrowserExtensionTest.php b/tests/DependencyInjection/AnimeDbAnimeNewsNetworkBrowserExtensionTest.php
new file mode 100644
index 0000000..83e3883
--- /dev/null
+++ b/tests/DependencyInjection/AnimeDbAnimeNewsNetworkBrowserExtensionTest.php
@@ -0,0 +1,111 @@
+
+ * @copyright Copyright (c) 2011, Peter Gribanov
+ * @license http://opensource.org/licenses/GPL-3.0 GPL v3
+ */
+
+namespace AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\Tests\DependencyInjection;
+
+use AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\DependencyInjection\AnimeDbAnimeNewsNetworkBrowserExtension;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+
+class AnimeDbAnimeNewsNetworkBrowserExtensionTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject|ContainerBuilder
+ */
+ private $container;
+
+ /**
+ * @var AnimeDbAnimeNewsNetworkBrowserExtension
+ */
+ private $extension;
+
+ protected function setUp()
+ {
+ $this->container = $this->getMock(ContainerBuilder::class);
+ $this->extension = new AnimeDbAnimeNewsNetworkBrowserExtension();
+ }
+
+ /**
+ * @return array
+ */
+ public function config()
+ {
+ return [
+ [
+ [],
+ 'https://cdn.animenewsnetwork.com',
+ '/encyclopedia/reports.xml',
+ '/encyclopedia/api.xml',
+ '',
+ ],
+ [
+ [
+ 'anime_db_anime_news_network_browser' => [
+ 'host' => 'http://cdn.animenewsnetwork.com',
+ 'reports' => '/encyclopedia/reports.json',
+ 'details' => '/encyclopedia/api.json',
+ 'client' => 'My Custom Bot 1.0',
+ ],
+ ],
+ 'http://cdn.animenewsnetwork.com',
+ '/encyclopedia/reports.json',
+ '/encyclopedia/api.json',
+ 'My Custom Bot 1.0',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider config
+ *
+ * @param array $config
+ * @param string $host
+ * @param string $reports
+ * @param string $details
+ * @param string $client
+ */
+ public function testLoad(array $config, $host, $reports, $details, $client)
+ {
+ $browser = $this->getMock(Definition::class);
+ $browser
+ ->expects($this->at(0))
+ ->method('replaceArgument')
+ ->with(1, $host)
+ ->will($this->returnSelf())
+ ;
+ $browser
+ ->expects($this->at(1))
+ ->method('replaceArgument')
+ ->with(2, $reports)
+ ->will($this->returnSelf())
+ ;
+ $browser
+ ->expects($this->at(2))
+ ->method('replaceArgument')
+ ->with(3, $details)
+ ->will($this->returnSelf())
+ ;
+ $browser
+ ->expects($this->at(3))
+ ->method('replaceArgument')
+ ->with(4, $client)
+ ->will($this->returnSelf())
+ ;
+
+ $this->container
+ ->expects($this->once())
+ ->method('getDefinition')
+ ->with('anime_db.anime_news_network.browser')
+ ->will($this->returnValue($browser))
+ ;
+
+ $this->extension->load($config, $this->container);
+ }
+}
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
new file mode 100644
index 0000000..7107f95
--- /dev/null
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -0,0 +1,64 @@
+
+ * @copyright Copyright (c) 2011, Peter Gribanov
+ * @license http://opensource.org/licenses/GPL-3.0 GPL v3
+ */
+
+namespace AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\Tests\DependencyInjection;
+
+use AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\DependencyInjection\Configuration;
+use Symfony\Component\Config\Definition\ArrayNode;
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ScalarNode;
+
+class ConfigurationTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var Configuration
+ */
+ private $configuration;
+
+ protected function setUp()
+ {
+ $this->configuration = new Configuration();
+ }
+
+ public function testConfigTree()
+ {
+ $tree_builder = $this->configuration->getConfigTreeBuilder();
+
+ $this->assertInstanceOf(TreeBuilder::class, $tree_builder);
+
+ /* @var $tree ArrayNode */
+ $tree = $tree_builder->buildTree();
+
+ $this->assertInstanceOf(ArrayNode::class, $tree);
+ $this->assertEquals('anime_db_anime_news_network_browser', $tree->getName());
+
+ /* @var $children ScalarNode[] */
+ $children = $tree->getChildren();
+
+ $this->assertInternalType('array', $children);
+ $this->assertEquals(['host', 'reports', 'details', 'client'], array_keys($children));
+
+ $this->assertInstanceOf(ScalarNode::class, $children['host']);
+ $this->assertEquals('https://cdn.animenewsnetwork.com', $children['host']->getDefaultValue());
+ $this->assertFalse($children['host']->isRequired());
+
+ $this->assertInstanceOf(ScalarNode::class, $children['reports']);
+ $this->assertEquals('/encyclopedia/reports.xml', $children['reports']->getDefaultValue());
+ $this->assertFalse($children['reports']->isRequired());
+
+ $this->assertInstanceOf(ScalarNode::class, $children['details']);
+ $this->assertEquals('/encyclopedia/api.xml', $children['details']->getDefaultValue());
+ $this->assertFalse($children['details']->isRequired());
+
+ $this->assertInstanceOf(ScalarNode::class, $children['client']);
+ $this->assertEquals('', $children['client']->getDefaultValue());
+ $this->assertFalse($children['client']->isRequired());
+ }
+}
diff --git a/tests/Service/BrowserTest.php b/tests/Service/BrowserTest.php
new file mode 100644
index 0000000..e4931bd
--- /dev/null
+++ b/tests/Service/BrowserTest.php
@@ -0,0 +1,167 @@
+
+ * @copyright Copyright (c) 2011, Peter Gribanov
+ * @license http://opensource.org/licenses/GPL-3.0 GPL v3
+ */
+
+namespace AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\Tests\Service;
+
+use AnimeDb\Bundle\AnimeNewsNetworkBrowserBundle\Service\Browser;
+use GuzzleHttp\Client as HttpClient;
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\StreamInterface;
+
+class BrowserTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var string
+ */
+ private $host = 'example.org';
+
+ /**
+ * @var string
+ */
+ private $reports = '/encyclopedia/reports.xml';
+
+ /**
+ * @var string
+ */
+ private $details = '/encyclopedia/api.xml';
+
+ /**
+ * @var string
+ */
+ private $app_client = 'My Custom Bot 1.0';
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject|HttpClient
+ */
+ private $client;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject|StreamInterface
+ */
+ private $stream;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject|MessageInterface
+ */
+ private $message;
+
+ /**
+ * @var Browser
+ */
+ private $browser;
+
+ protected function setUp()
+ {
+ $this->client = $this->getMock(HttpClient::class);
+ $this->stream = $this->getMock(StreamInterface::class);
+ $this->message = $this->getMock(MessageInterface::class);
+
+ $this->browser = new Browser($this->client, $this->host, $this->reports, $this->details, $this->app_client);
+ }
+
+ /**
+ * @return array
+ */
+ public function appClients()
+ {
+ return [
+ [''],
+ ['Override User Agent'],
+ ];
+ }
+
+ /**
+ * @dataProvider appClients
+ *
+ * @param string $app_client
+ */
+ public function testReports($app_client)
+ {
+ $id = 155;
+ $params = ['bar' => 'baz'];
+ $options = $params + [
+ 'id' => $id,
+ 'headers' => [
+ 'User-Agent' => $this->app_client,
+ ],
+ ];
+
+ if ($app_client) {
+ $options['headers']['User-Agent'] = $app_client;
+ $params['headers']['User-Agent'] = $app_client;
+ }
+
+ $content = 'Hello, world!';
+
+ $this->stream
+ ->expects($this->once())
+ ->method('getContents')
+ ->will($this->returnValue($content))
+ ;
+
+ $this->message
+ ->expects($this->once())
+ ->method('getBody')
+ ->will($this->returnValue($this->stream))
+ ;
+
+ $this->client
+ ->expects($this->once())
+ ->method('request')
+ ->with('GET', $this->host.$this->reports, $options)
+ ->will($this->returnValue($this->message))
+ ;
+
+ $this->assertEquals($content, $this->browser->reports($id, $params));
+ }
+
+ /**
+ * @dataProvider appClients
+ *
+ * @param string $app_client
+ */
+ public function testDetails($app_client)
+ {
+ $params = ['bar' => 'baz'];
+ $options = $params + [
+ 'headers' => [
+ 'User-Agent' => $this->app_client,
+ ],
+ ];
+
+ if ($app_client) {
+ $options['headers']['User-Agent'] = $app_client;
+ $params['headers']['User-Agent'] = $app_client;
+ }
+
+ $content = 'Hello, world!';
+
+ $this->stream
+ ->expects($this->once())
+ ->method('getContents')
+ ->will($this->returnValue($content))
+ ;
+
+ $this->message
+ ->expects($this->once())
+ ->method('getBody')
+ ->will($this->returnValue($this->stream))
+ ;
+
+ $this->client
+ ->expects($this->once())
+ ->method('request')
+ ->with('GET', $this->host.$this->details, $options)
+ ->will($this->returnValue($this->message))
+ ;
+
+ $this->assertEquals($content, $this->browser->details($params));
+ }
+}