Skip to content

Commit b009fbc

Browse files
authored
Merge pull request #64 from magento-research/feature/url-resolver
URL Resolver
2 parents 6a705e6 + 0537842 commit b009fbc

File tree

7 files changed

+360
-20
lines changed

7 files changed

+360
-20
lines changed

src/Resolver/AbstractResolver.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ abstract class AbstractResolver implements ResolverInterface
1818
*/
1919
protected $iterator;
2020

21+
/**
22+
* Return list of previous indicators.
23+
*
24+
* Given that the UPWARD specification is a living document, it's possible that indicators may change, but
25+
* to maintain backward compatibility we should still support those past indicators.
26+
*
27+
* @return string[]
28+
*/
29+
public function getDeprecatedIndicators()
30+
{
31+
return [];
32+
}
33+
2134
/**
2235
* {@inheritdoc}
2336
*/

src/Resolver/Service.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,22 @@
1313

1414
class Service extends AbstractResolver
1515
{
16+
public const DEPRECATED_URL_INDICATOR = 'url';
17+
18+
/**
19+
* {@inheritdoc}
20+
*/
21+
public function getDeprecatedIndicators()
22+
{
23+
return [self::DEPRECATED_URL_INDICATOR];
24+
}
25+
1626
/**
1727
* {@inheritdoc}
1828
*/
1929
public function getIndicator(): string
2030
{
21-
return 'url';
31+
return 'endpoint';
2232
}
2333

2434
/**
@@ -37,7 +47,7 @@ public function isValid(Definition $definition): bool
3747
}
3848
}
3949

40-
return parent::isValid($definition);
50+
return $definition->has($this->getIndicator()) xor $definition->has(self::DEPRECATED_URL_INDICATOR);
4151
}
4252

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

52-
$url = $this->getIterator()->get('url', $definition);
62+
$urlParameter = $definition->has($this->getIndicator())
63+
? $this->getIndicator()
64+
: self::DEPRECATED_URL_INDICATOR;
65+
66+
$url = $this->getIterator()->get($urlParameter, $definition);
5367
$query = $this->getIterator()->get('query', $definition);
5468
$method = $definition->has('method') ? $this->getIterator()->get('method', $definition) : 'POST';
5569
$variables = $definition->has('variables') ? $this->getIterator()->get('variables', $definition) : [];

src/Resolver/Template.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ public function resolve($definition)
5353
$renderData = [];
5454

5555
if ($definition->has('provide')) {
56-
foreach ($definition->get('provide') as $index => $definition) {
57-
$key = \is_int($index) ? $definition : $index;
58-
$renderData[$key] = $definition instanceof Definition
56+
foreach ($definition->get('provide') as $index => $provideDefinition) {
57+
$key = \is_int($index) ? $provideDefinition : $index;
58+
$renderData[$key] = $provideDefinition instanceof Definition
5959
? $this->getIterator()->get('provide.' . $index)
60-
: $this->getIterator()->get($definition);
60+
: $this->getIterator()->get($provideDefinition);
6161
}
6262
}
6363

src/Resolver/Url.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Upward\Resolver;
10+
11+
use Magento\Upward\Definition;
12+
use Zend\Uri\UriFactory;
13+
14+
class Url extends AbstractResolver
15+
{
16+
public const FAKE_BASE_HOST = 'upward-fake.localhost';
17+
public const FAKE_BASE_URL = 'https://' . self::FAKE_BASE_HOST;
18+
public const NON_RELATIVE_PARTS = ['protocol', 'port', 'username', 'password'];
19+
20+
/**
21+
* {@inheritdoc}
22+
*/
23+
public function getIndicator(): string
24+
{
25+
return 'baseUrl';
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function isValid(Definition $definition): bool
32+
{
33+
if ($definition->has('query')) {
34+
$query = $this->getIterator()->get('query', $definition);
35+
if (!\is_array($query)) {
36+
return false;
37+
}
38+
}
39+
40+
if ($definition->has('password') && !$definition->has('username')) {
41+
return false;
42+
}
43+
44+
if ($this->getIterator()->get('baseUrl', $definition) === false && !$definition->has('hostname')) {
45+
foreach (self::NON_RELATIVE_PARTS as $nonRelativePart) {
46+
if ($definition->has($nonRelativePart)) {
47+
return false;
48+
}
49+
}
50+
}
51+
52+
return parent::isValid($definition);
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public function resolve($definition)
59+
{
60+
if (!$definition instanceof Definition) {
61+
throw new \InvalidArgumentException('$definition must be an instance of ' . Definition::class);
62+
}
63+
64+
$baseUrl = $this->getIterator()->get('baseUrl', $definition)
65+
? $this->getIterator()->get('baseUrl', $definition)
66+
: self::FAKE_BASE_URL;
67+
$uri = UriFactory::factory($baseUrl);
68+
69+
if ($definition->has('hostname')) {
70+
$uri->setHost($this->getIterator()->get('hostname', $definition));
71+
}
72+
73+
if ($definition->has('protocol')) {
74+
$uri->setScheme(str_replace(':', '', $this->getIterator()->get('protocol', $definition)));
75+
}
76+
77+
if ($definition->has('pathname')) {
78+
$pathname = $this->getIterator()->get('pathname', $definition);
79+
$currentPathname = $uri->getPath();
80+
if ($pathname[0] !== '/') {
81+
if ($currentPathname === null) {
82+
$pathname = "/${pathname}";
83+
} elseif ($currentPathname[\strlen($currentPathname) - 1] === '/') {
84+
$pathname = $currentPathname . $pathname;
85+
} else {
86+
$trimmedCurrentPathname = substr($currentPathname, 0, strrpos($currentPathname, '/') + 1);
87+
$pathname = $trimmedCurrentPathname . $pathname;
88+
}
89+
}
90+
91+
$uri->setPath($pathname);
92+
}
93+
94+
if ($definition->has('search')) {
95+
parse_str($this->getIterator()->get('search', $definition), $searchArray);
96+
$uri->setQuery(array_merge($uri->getQueryAsArray(), $searchArray));
97+
}
98+
99+
if ($definition->has('query')) {
100+
$mergedQuery = array_merge($uri->getQueryAsArray(), $this->getIterator()->get('query', $definition));
101+
$uri->setQuery($mergedQuery);
102+
}
103+
104+
if ($definition->has('port')) {
105+
$uri->setPort($this->getIterator()->get('port', $definition));
106+
}
107+
108+
if ($definition->has('username')) {
109+
$userInfo = $this->getIterator()->get('username', $definition);
110+
if ($definition->has('password')) {
111+
$userInfo .= ':' . $this->getIterator()->get('password', $definition);
112+
}
113+
$uri->setUserInfo($userInfo);
114+
}
115+
116+
if ($definition->has('hash')) {
117+
$uri->setFragment(str_replace('#', '', $this->getIterator()->get('hash', $definition)));
118+
}
119+
120+
$returnUrl = $uri->toString();
121+
122+
return $uri->getHost() === self::FAKE_BASE_HOST
123+
? str_replace(self::FAKE_BASE_URL, '', $returnUrl)
124+
: $returnUrl;
125+
}
126+
}

src/ResolverFactory.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ResolverFactory
1717
public const RESOLVER_TYPE_PROXY = 'proxy';
1818
public const RESOLVER_TYPE_SERVICE = 'service';
1919
public const RESOLVER_TYPE_TEMPLATE = 'template';
20+
public const RESOLVER_TYPE_URL = 'url';
2021

2122
/**
2223
* @var array map of resolver key to their class implementation
@@ -29,6 +30,7 @@ class ResolverFactory
2930
self::RESOLVER_TYPE_PROXY => Resolver\Proxy::class,
3031
self::RESOLVER_TYPE_SERVICE => Resolver\Service::class,
3132
self::RESOLVER_TYPE_TEMPLATE => Resolver\Template::class,
33+
self::RESOLVER_TYPE_URL => Resolver\Url::class,
3234
];
3335

3436
/**
@@ -66,7 +68,7 @@ public static function get($definition): ?Resolver\ResolverInterface
6668
* Return a resolver from instance cache by its type,
6769
* otherwise instantiate one based on its type and cache it.
6870
*
69-
* @throws RuntimeException if there is no cached instance or configured class for $resolverType
71+
* @throws \RuntimeException if there is no cached instance or configured class for $resolverType
7072
*/
7173
private static function build(string $resolverType): Resolver\ResolverInterface
7274
{
@@ -84,7 +86,7 @@ private static function build(string $resolverType): Resolver\ResolverInterface
8486
/**
8587
* Get a Resolver for a Definition.
8688
*
87-
* @throws RuntimeException if $definition is not valid for the Resolver
89+
* @throws \RuntimeException if $definition is not valid for the Resolver
8890
*/
8991
private static function getForDefinition(Definition $definition): Resolver\ResolverInterface
9092
{
@@ -112,7 +114,7 @@ private static function getForScalar(string $lookup): ?Resolver\ResolverInterfac
112114
/**
113115
* Return a resolver for a Definition based on if that Definition has the resolver's indicator.
114116
*
115-
* @throw RuntimeException if no resolver can be inferred.
117+
* @throws \RuntimeException if no resolver can be inferred
116118
*/
117119
private static function inferResolver(Definition $definition): Resolver\ResolverInterface
118120
{
@@ -122,6 +124,14 @@ private static function inferResolver(Definition $definition): Resolver\Resolver
122124
if ($definition->has($resolver->getIndicator())) {
123125
return $resolver;
124126
}
127+
128+
if ($resolver instanceof Resolver\AbstractResolver) {
129+
foreach ($resolver->getDeprecatedIndicators() as $deprecatedIndicator) {
130+
if ($definition->has($deprecatedIndicator)) {
131+
return $resolver;
132+
}
133+
}
134+
}
125135
}
126136

127137
throw new \RuntimeException('No resolver found for definition: ' . json_encode($definition));

test/Resolver/ServiceTest.php

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,31 @@ protected function setUp(): void
4242
$this->resolver->setIterator($this->definitionIteratorMock);
4343
}
4444

45+
/**
46+
* Data provider to verify backwards compatibility with previous indicator.
47+
*
48+
* @return string[]
49+
*/
50+
public function indicatorDataProvider()
51+
{
52+
return [['url'], ['endpoint']];
53+
}
54+
4555
public function testIndicator(): void
4656
{
47-
verify($this->resolver->getIndicator())->is()->sameAs('url');
57+
verify($this->resolver->getIndicator())->is()->sameAs('endpoint');
4858
}
4959

50-
public function testIsValid(): void
60+
/**
61+
* @dataProvider indicatorDataProvider
62+
*/
63+
public function testIsValid(string $indicator): void
5164
{
52-
$validDefinition = new Definition(['url' => '/graphql', 'query' => 'gql']);
53-
$validWithMethodDefinition = new Definition(['url' => '/graphql', 'query' => 'gql', 'method' => 'GET']);
65+
$validDefinition = new Definition([$indicator => '/graphql', 'query' => 'gql']);
66+
$validWithMethodDefinition = new Definition([$indicator => '/graphql', 'query' => 'gql', 'method' => 'GET']);
5467
$invalidNoURL = new Definition(['query' => 'gql']);
55-
$invalidNoQuery = new Definition(['url' => '/graphql']);
56-
$invalidUnsupportedMethod = new Definition(['url' => '/graphql', 'query' => 'gql', 'method' => 'PUT']);
68+
$invalidNoQuery = new Definition([$indicator => '/graphql']);
69+
$invalidUnsupportedMethod = new Definition([$indicator => '/graphql', 'query' => 'gql', 'method' => 'PUT']);
5770

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

72-
public function testResolve(): void
85+
/**
86+
* @dataProvider indicatorDataProvider
87+
*/
88+
public function testResolve(string $indicator): void
7389
{
74-
$definition = new Definition(['url' => '/graphql', 'query' => 'gql']);
90+
$definition = new Definition([$indicator => '/graphql', 'query' => 'gql']);
7591
$expectedRequestBody = json_encode(['query' => 'gql', 'variables' => []]);
7692
$expectedResponseArray = ['data' => ['key' => 'value']];
7793

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

105-
public function testResolveWithConfiguration(): void
121+
/**
122+
* @dataProvider indicatorDataProvider
123+
*/
124+
public function testResolveWithConfiguration(string $indicator): void
106125
{
107126
$definition = new Definition([
108-
'url' => '/graphql',
127+
$indicator => '/graphql',
109128
'query' => 'gql',
110129
'variables' => [
111130
'var1' => 'var1Value',

0 commit comments

Comments
 (0)