Skip to content

Commit

Permalink
Merge pull request #64 from magento-research/feature/url-resolver
Browse files Browse the repository at this point in the history
URL Resolver
  • Loading branch information
dpatil-magento authored Apr 29, 2019
2 parents 6a705e6 + 0537842 commit b009fbc
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 20 deletions.
13 changes: 13 additions & 0 deletions src/Resolver/AbstractResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ abstract class AbstractResolver implements ResolverInterface
*/
protected $iterator;

/**
* Return list of previous indicators.
*
* Given that the UPWARD specification is a living document, it's possible that indicators may change, but
* to maintain backward compatibility we should still support those past indicators.
*
* @return string[]
*/
public function getDeprecatedIndicators()
{
return [];
}

/**
* {@inheritdoc}
*/
Expand Down
20 changes: 17 additions & 3 deletions src/Resolver/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@

class Service extends AbstractResolver
{
public const DEPRECATED_URL_INDICATOR = 'url';

/**
* {@inheritdoc}
*/
public function getDeprecatedIndicators()
{
return [self::DEPRECATED_URL_INDICATOR];
}

/**
* {@inheritdoc}
*/
public function getIndicator(): string
{
return 'url';
return 'endpoint';
}

/**
Expand All @@ -37,7 +47,7 @@ public function isValid(Definition $definition): bool
}
}

return parent::isValid($definition);
return $definition->has($this->getIndicator()) xor $definition->has(self::DEPRECATED_URL_INDICATOR);
}

/**
Expand All @@ -49,7 +59,11 @@ public function resolve($definition)
throw new \InvalidArgumentException('$definition must be an instance of ' . Definition::class);
}

$url = $this->getIterator()->get('url', $definition);
$urlParameter = $definition->has($this->getIndicator())
? $this->getIndicator()
: self::DEPRECATED_URL_INDICATOR;

$url = $this->getIterator()->get($urlParameter, $definition);
$query = $this->getIterator()->get('query', $definition);
$method = $definition->has('method') ? $this->getIterator()->get('method', $definition) : 'POST';
$variables = $definition->has('variables') ? $this->getIterator()->get('variables', $definition) : [];
Expand Down
8 changes: 4 additions & 4 deletions src/Resolver/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ public function resolve($definition)
$renderData = [];

if ($definition->has('provide')) {
foreach ($definition->get('provide') as $index => $definition) {
$key = \is_int($index) ? $definition : $index;
$renderData[$key] = $definition instanceof Definition
foreach ($definition->get('provide') as $index => $provideDefinition) {
$key = \is_int($index) ? $provideDefinition : $index;
$renderData[$key] = $provideDefinition instanceof Definition
? $this->getIterator()->get('provide.' . $index)
: $this->getIterator()->get($definition);
: $this->getIterator()->get($provideDefinition);
}
}

Expand Down
126 changes: 126 additions & 0 deletions src/Resolver/Url.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/

declare(strict_types=1);

namespace Magento\Upward\Resolver;

use Magento\Upward\Definition;
use Zend\Uri\UriFactory;

class Url extends AbstractResolver
{
public const FAKE_BASE_HOST = 'upward-fake.localhost';
public const FAKE_BASE_URL = 'https://' . self::FAKE_BASE_HOST;
public const NON_RELATIVE_PARTS = ['protocol', 'port', 'username', 'password'];

/**
* {@inheritdoc}
*/
public function getIndicator(): string
{
return 'baseUrl';
}

/**
* {@inheritdoc}
*/
public function isValid(Definition $definition): bool
{
if ($definition->has('query')) {
$query = $this->getIterator()->get('query', $definition);
if (!\is_array($query)) {
return false;
}
}

if ($definition->has('password') && !$definition->has('username')) {
return false;
}

if ($this->getIterator()->get('baseUrl', $definition) === false && !$definition->has('hostname')) {
foreach (self::NON_RELATIVE_PARTS as $nonRelativePart) {
if ($definition->has($nonRelativePart)) {
return false;
}
}
}

return parent::isValid($definition);
}

/**
* {@inheritdoc}
*/
public function resolve($definition)
{
if (!$definition instanceof Definition) {
throw new \InvalidArgumentException('$definition must be an instance of ' . Definition::class);
}

$baseUrl = $this->getIterator()->get('baseUrl', $definition)
? $this->getIterator()->get('baseUrl', $definition)
: self::FAKE_BASE_URL;
$uri = UriFactory::factory($baseUrl);

if ($definition->has('hostname')) {
$uri->setHost($this->getIterator()->get('hostname', $definition));
}

if ($definition->has('protocol')) {
$uri->setScheme(str_replace(':', '', $this->getIterator()->get('protocol', $definition)));
}

if ($definition->has('pathname')) {
$pathname = $this->getIterator()->get('pathname', $definition);
$currentPathname = $uri->getPath();
if ($pathname[0] !== '/') {
if ($currentPathname === null) {
$pathname = "/${pathname}";
} elseif ($currentPathname[\strlen($currentPathname) - 1] === '/') {
$pathname = $currentPathname . $pathname;
} else {
$trimmedCurrentPathname = substr($currentPathname, 0, strrpos($currentPathname, '/') + 1);
$pathname = $trimmedCurrentPathname . $pathname;
}
}

$uri->setPath($pathname);
}

if ($definition->has('search')) {
parse_str($this->getIterator()->get('search', $definition), $searchArray);
$uri->setQuery(array_merge($uri->getQueryAsArray(), $searchArray));
}

if ($definition->has('query')) {
$mergedQuery = array_merge($uri->getQueryAsArray(), $this->getIterator()->get('query', $definition));
$uri->setQuery($mergedQuery);
}

if ($definition->has('port')) {
$uri->setPort($this->getIterator()->get('port', $definition));
}

if ($definition->has('username')) {
$userInfo = $this->getIterator()->get('username', $definition);
if ($definition->has('password')) {
$userInfo .= ':' . $this->getIterator()->get('password', $definition);
}
$uri->setUserInfo($userInfo);
}

if ($definition->has('hash')) {
$uri->setFragment(str_replace('#', '', $this->getIterator()->get('hash', $definition)));
}

$returnUrl = $uri->toString();

return $uri->getHost() === self::FAKE_BASE_HOST
? str_replace(self::FAKE_BASE_URL, '', $returnUrl)
: $returnUrl;
}
}
16 changes: 13 additions & 3 deletions src/ResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class ResolverFactory
public const RESOLVER_TYPE_PROXY = 'proxy';
public const RESOLVER_TYPE_SERVICE = 'service';
public const RESOLVER_TYPE_TEMPLATE = 'template';
public const RESOLVER_TYPE_URL = 'url';

/**
* @var array map of resolver key to their class implementation
Expand All @@ -29,6 +30,7 @@ class ResolverFactory
self::RESOLVER_TYPE_PROXY => Resolver\Proxy::class,
self::RESOLVER_TYPE_SERVICE => Resolver\Service::class,
self::RESOLVER_TYPE_TEMPLATE => Resolver\Template::class,
self::RESOLVER_TYPE_URL => Resolver\Url::class,
];

/**
Expand Down Expand Up @@ -66,7 +68,7 @@ public static function get($definition): ?Resolver\ResolverInterface
* Return a resolver from instance cache by its type,
* otherwise instantiate one based on its type and cache it.
*
* @throws RuntimeException if there is no cached instance or configured class for $resolverType
* @throws \RuntimeException if there is no cached instance or configured class for $resolverType
*/
private static function build(string $resolverType): Resolver\ResolverInterface
{
Expand All @@ -84,7 +86,7 @@ private static function build(string $resolverType): Resolver\ResolverInterface
/**
* Get a Resolver for a Definition.
*
* @throws RuntimeException if $definition is not valid for the Resolver
* @throws \RuntimeException if $definition is not valid for the Resolver
*/
private static function getForDefinition(Definition $definition): Resolver\ResolverInterface
{
Expand Down Expand Up @@ -112,7 +114,7 @@ private static function getForScalar(string $lookup): ?Resolver\ResolverInterfac
/**
* Return a resolver for a Definition based on if that Definition has the resolver's indicator.
*
* @throw RuntimeException if no resolver can be inferred.
* @throws \RuntimeException if no resolver can be inferred
*/
private static function inferResolver(Definition $definition): Resolver\ResolverInterface
{
Expand All @@ -122,6 +124,14 @@ private static function inferResolver(Definition $definition): Resolver\Resolver
if ($definition->has($resolver->getIndicator())) {
return $resolver;
}

if ($resolver instanceof Resolver\AbstractResolver) {
foreach ($resolver->getDeprecatedIndicators() as $deprecatedIndicator) {
if ($definition->has($deprecatedIndicator)) {
return $resolver;
}
}
}
}

throw new \RuntimeException('No resolver found for definition: ' . json_encode($definition));
Expand Down
39 changes: 29 additions & 10 deletions test/Resolver/ServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,31 @@ protected function setUp(): void
$this->resolver->setIterator($this->definitionIteratorMock);
}

/**
* Data provider to verify backwards compatibility with previous indicator.
*
* @return string[]
*/
public function indicatorDataProvider()
{
return [['url'], ['endpoint']];
}

public function testIndicator(): void
{
verify($this->resolver->getIndicator())->is()->sameAs('url');
verify($this->resolver->getIndicator())->is()->sameAs('endpoint');
}

public function testIsValid(): void
/**
* @dataProvider indicatorDataProvider
*/
public function testIsValid(string $indicator): void
{
$validDefinition = new Definition(['url' => '/graphql', 'query' => 'gql']);
$validWithMethodDefinition = new Definition(['url' => '/graphql', 'query' => 'gql', 'method' => 'GET']);
$validDefinition = new Definition([$indicator => '/graphql', 'query' => 'gql']);
$validWithMethodDefinition = new Definition([$indicator => '/graphql', 'query' => 'gql', 'method' => 'GET']);
$invalidNoURL = new Definition(['query' => 'gql']);
$invalidNoQuery = new Definition(['url' => '/graphql']);
$invalidUnsupportedMethod = new Definition(['url' => '/graphql', 'query' => 'gql', 'method' => 'PUT']);
$invalidNoQuery = new Definition([$indicator => '/graphql']);
$invalidUnsupportedMethod = new Definition([$indicator => '/graphql', 'query' => 'gql', 'method' => 'PUT']);

$this->definitionIteratorMock->shouldReceive('get')
->twice()
Expand All @@ -69,9 +82,12 @@ public function testIsValid(): void
verify($this->resolver->isValid($invalidUnsupportedMethod))->is()->false();
}

public function testResolve(): void
/**
* @dataProvider indicatorDataProvider
*/
public function testResolve(string $indicator): void
{
$definition = new Definition(['url' => '/graphql', 'query' => 'gql']);
$definition = new Definition([$indicator => '/graphql', 'query' => 'gql']);
$expectedRequestBody = json_encode(['query' => 'gql', 'variables' => []]);
$expectedResponseArray = ['data' => ['key' => 'value']];

Expand Down Expand Up @@ -102,10 +118,13 @@ public function testResolveThrowsException(): void
$this->resolver->resolve('Not a Definition');
}

public function testResolveWithConfiguration(): void
/**
* @dataProvider indicatorDataProvider
*/
public function testResolveWithConfiguration(string $indicator): void
{
$definition = new Definition([
'url' => '/graphql',
$indicator => '/graphql',
'query' => 'gql',
'variables' => [
'var1' => 'var1Value',
Expand Down
Loading

0 comments on commit b009fbc

Please sign in to comment.