Skip to content

Commit 2ae2ffb

Browse files
committed
Prevented single record resources being imported with multi-record import methods.
1 parent d39c321 commit 2ae2ffb

File tree

6 files changed

+124
-50
lines changed

6 files changed

+124
-50
lines changed

src/IncompatibleResourceException.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@
66
use ScriptFUSION\Porter\Provider\Resource\SingleRecordResource;
77

88
/**
9-
* The exception that is throw when a resource is incompatible with an importOne operation because it is not marked
10-
* with the single record interface.
9+
* The exception that is throw when a resource is incompatible with an import operation.
1110
*/
1211
final class IncompatibleResourceException extends \LogicException
1312
{
14-
public function __construct()
13+
public function __construct(string $message)
1514
{
16-
parent::__construct('Cannot import one: resource does not implement ' . SingleRecordResource::class . '.');
15+
parent::__construct($message);
16+
}
17+
18+
public static function createMustImplementInterface(): self
19+
{
20+
return new self('Cannot import one: resource does not implement ' . SingleRecordResource::class . '.');
21+
}
22+
23+
public static function createMustNotImplementInterface(): self
24+
{
25+
return new self('This is a single record resource. Try calling importOne() instead.');
26+
}
27+
28+
public static function createMustNotImplementInterfaceAsync(): self
29+
{
30+
return new self('This is a single record resource. Try calling importOneAsync() instead.');
1731
}
1832
}

src/Porter.php

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
namespace ScriptFUSION\Porter;
55

6-
use Amp\Iterator;
76
use Amp\Promise;
87
use Psr\Container\ContainerInterface;
98
use ScriptFUSION\Porter\Collection\AsyncPorterRecords;
@@ -60,20 +59,17 @@ public function __construct(ContainerInterface $providers)
6059
*
6160
* @return PorterRecords|CountablePorterRecords Collection of records. If the total size of the collection is known,
6261
* the collection may implement Countable, otherwise PorterRecords is returned.
62+
*
63+
* @throws IncompatibleResourceException Resource emits a single record and must be imported with
64+
* importOne() instead.
6365
*/
6466
public function import(ImportSpecification $specification): PorterRecords
6567
{
66-
$specification = clone $specification;
67-
68-
$records = $this->fetch($specification);
69-
70-
if (!$records instanceof ProviderRecords) {
71-
$records = $this->createProviderRecords($records, $specification->getResource());
68+
if ($specification->getResource() instanceof SingleRecordResource) {
69+
throw IncompatibleResourceException::createMustNotImplementInterface();
7270
}
7371

74-
$records = $this->transformRecords($records, $specification->getTransformers(), $specification->getContext());
75-
76-
return $this->createPorterRecords($records, $specification);
72+
return $this->fetch($specification);
7773
}
7874

7975
/**
@@ -83,15 +79,16 @@ public function import(ImportSpecification $specification): PorterRecords
8379
*
8480
* @return array|null Record.
8581
*
82+
* @throws IncompatibleResourceException Resource does not implement required interface.
8683
* @throws ImportException More than one record was imported.
8784
*/
8885
public function importOne(ImportSpecification $specification): ?array
8986
{
9087
if (!$specification->getResource() instanceof SingleRecordResource) {
91-
throw new IncompatibleResourceException;
88+
throw IncompatibleResourceException::createMustImplementInterface();
9289
}
9390

94-
$results = $this->import($specification);
91+
$results = $this->fetch($specification);
9592

9693
if (!$results->valid()) {
9794
return null;
@@ -106,8 +103,9 @@ public function importOne(ImportSpecification $specification): ?array
106103
return $one;
107104
}
108105

109-
private function fetch(ImportSpecification $specification): \Iterator
106+
private function fetch(ImportSpecification $specification): PorterRecords
110107
{
108+
$specification = clone $specification;
111109
$resource = $specification->getResource();
112110
$provider = $this->getProvider($specification->getProviderName() ?? $resource->getProviderClassName());
113111

@@ -122,9 +120,15 @@ private function fetch(ImportSpecification $specification): \Iterator
122120
));
123121
}
124122

125-
$connector = $provider->getConnector();
123+
$records = $resource->fetch(ImportConnectorFactory::create($provider->getConnector(), $specification));
124+
125+
if (!$records instanceof ProviderRecords) {
126+
$records = $this->createProviderRecords($records, $specification->getResource());
127+
}
128+
129+
$records = $this->transformRecords($records, $specification->getTransformers(), $specification->getContext());
126130

127-
return $resource->fetch(ImportConnectorFactory::create($connector, $specification));
131+
return $this->createPorterRecords($records, $specification);
128132
}
129133

130134
/**
@@ -135,24 +139,17 @@ private function fetch(ImportSpecification $specification): \Iterator
135139
*
136140
* @return AsyncPorterRecords|CountableAsyncPorterRecords Collection of records. If the total size of the
137141
* collection is known, the collection may implement Countable, otherwise AsyncPorterRecords is returned.
142+
*
143+
* @throws IncompatibleResourceException Resource emits a single record and must be imported with
144+
* importOneAsync() instead.
138145
*/
139146
public function importAsync(AsyncImportSpecification $specification): AsyncRecordCollection
140147
{
141-
$specification = clone $specification;
142-
143-
$records = $this->fetchAsync($specification);
144-
145-
if (!$records instanceof AsyncProviderRecords) {
146-
$records = new AsyncProviderRecords($records, $specification->getAsyncResource());
148+
if ($specification->getAsyncResource() instanceof SingleRecordResource) {
149+
throw IncompatibleResourceException::createMustNotImplementInterfaceAsync();
147150
}
148151

149-
$records = $this->transformRecordsAsync(
150-
$records,
151-
$specification->getTransformers(),
152-
$specification->getContext()
153-
);
154-
155-
return $this->createAsyncPorterRecords($records, $specification);
152+
return $this->fetchAsync($specification);
156153
}
157154

158155
/**
@@ -162,16 +159,17 @@ public function importAsync(AsyncImportSpecification $specification): AsyncRecor
162159
*
163160
* @return Promise<array|null> Record.
164161
*
162+
* @throws IncompatibleResourceException Resource does not implement required interface.
165163
* @throws ImportException More than one record was imported.
166164
*/
167165
public function importOneAsync(AsyncImportSpecification $specification): Promise
168166
{
169167
return call(function () use ($specification) {
170168
if (!$specification->getAsyncResource() instanceof SingleRecordResource) {
171-
throw new IncompatibleResourceException;
169+
throw IncompatibleResourceException::createMustImplementInterface();
172170
}
173171

174-
$results = $this->importAsync($specification);
172+
$results = $this->fetchAsync($specification);
175173

176174
yield $results->advance();
177175

@@ -185,8 +183,9 @@ public function importOneAsync(AsyncImportSpecification $specification): Promise
185183
});
186184
}
187185

188-
private function fetchAsync(AsyncImportSpecification $specification): Iterator
186+
private function fetchAsync(AsyncImportSpecification $specification): AsyncRecordCollection
189187
{
188+
$specification = clone $specification;
190189
$resource = $specification->getAsyncResource();
191190
$provider = $this->getProvider($specification->getProviderName() ?? $resource->getProviderClassName());
192191

@@ -201,9 +200,21 @@ private function fetchAsync(AsyncImportSpecification $specification): Iterator
201200
));
202201
}
203202

204-
$connector = $provider->getAsyncConnector();
203+
$records = $resource->fetchAsync(
204+
ImportConnectorFactory::create($provider->getAsyncConnector(), $specification)
205+
);
205206

206-
return $resource->fetchAsync(ImportConnectorFactory::create($connector, $specification));
207+
if (!$records instanceof AsyncProviderRecords) {
208+
$records = new AsyncProviderRecords($records, $specification->getAsyncResource());
209+
}
210+
211+
$records = $this->transformRecordsAsync(
212+
$records,
213+
$specification->getTransformers(),
214+
$specification->getContext()
215+
);
216+
217+
return $this->createAsyncPorterRecords($records, $specification);
207218
}
208219

209220
/**

test/Integration/PorterAsyncTest.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ protected function setUp(): void
3333
parent::setUp();
3434

3535
$this->specification = new AsyncImportSpecification($this->resource);
36+
$this->singleSpecification = new AsyncImportSpecification($this->singleResource);
3637
}
3738

3839
/**
@@ -46,12 +47,23 @@ public function testImportAsync(): \Generator
4647
self::assertSame(['foo'], $records->getCurrent());
4748
}
4849

50+
/**
51+
* Tests that when importing a single record resource, an exception is thrown.
52+
*/
53+
public function testImportSingle(): void
54+
{
55+
$this->expectException(IncompatibleResourceException::class);
56+
$this->expectExceptionMessage('importOneAsync()');
57+
58+
$this->porter->importAsync($this->singleSpecification);
59+
}
60+
4961
/**
5062
* Tests that the full async import path, via connector, resource and provider, fetches one record correctly.
5163
*/
5264
public function testImportOneAsync(): \Generator
5365
{
54-
self::assertSame(['foo'], yield $this->porter->importOneAsync($this->specification));
66+
self::assertSame(['foo'], yield $this->porter->importOneAsync($this->singleSpecification));
5567
}
5668

5769
/**
@@ -94,10 +106,10 @@ public function testImportCountableAsyncRecords(): \Generator
94106
*/
95107
public function testImportOneOfManyAsync(): \Generator
96108
{
97-
$this->resource->shouldReceive('fetchAsync')->andReturn(Iterator\fromIterable([['foo'], ['bar']]));
109+
$this->singleResource->shouldReceive('fetchAsync')->andReturn(Iterator\fromIterable([['foo'], ['bar']]));
98110

99111
$this->expectException(ImportException::class);
100-
yield $this->porter->importOneAsync($this->specification);
112+
yield $this->porter->importOneAsync($this->singleSpecification);
101113
}
102114

103115
/**

test/Integration/PorterSyncTest.php

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,38 +184,49 @@ public function testImportForeignResource(): void
184184
$this->porter->import($this->specification);
185185
}
186186

187+
/**
188+
* Tests that when importing a single record resource, an exception is thrown.
189+
*/
190+
public function testImportSingle(): void
191+
{
192+
$this->expectException(IncompatibleResourceException::class);
193+
$this->expectExceptionMessage('importOne()');
194+
195+
$this->porter->import($this->singleSpecification);
196+
}
197+
187198
#endregion
188199

189200
#region Import one
190201

191202
public function testImportOne(): void
192203
{
193-
$result = $this->porter->importOne($this->specification);
204+
$result = $this->porter->importOne($this->singleSpecification);
194205

195206
self::assertSame(['foo'], $result);
196207
}
197208

198209
public function testImportOneOfNone(): void
199210
{
200-
$this->resource->shouldReceive('fetch')->andReturn(new \EmptyIterator);
211+
$this->singleResource->shouldReceive('fetch')->andReturn(new \EmptyIterator);
201212

202-
$result = $this->porter->importOne($this->specification);
213+
$result = $this->porter->importOne($this->singleSpecification);
203214

204215
self::assertNull($result);
205216
}
206217

207218
public function testImportOneOfMany(): void
208219
{
209-
$this->resource->shouldReceive('fetch')->andReturn(new \ArrayIterator([['foo'], ['bar']]));
220+
$this->singleResource->shouldReceive('fetch')->andReturn(new \ArrayIterator([['foo'], ['bar']]));
210221

211222
$this->expectException(ImportException::class);
212-
$this->porter->importOne($this->specification);
223+
$this->porter->importOne($this->singleSpecification);
213224
}
214225

215226
/**
216227
* Tests that when importing one from a resource not marked with SingleRecordResource, an exception is thrown.
217228
*/
218-
public function testImportOneNonSingleAsync(): \Generator
229+
public function testImportOneNonSingle(): \Generator
219230
{
220231
$this->expectException(IncompatibleResourceException::class);
221232
$this->expectExceptionMessage(SingleRecordResource::class);
@@ -318,7 +329,7 @@ public function testCustomProviderFetchExceptionHandler(): void
318329
->shouldReceive('fetch')
319330
->andReturnUsing(static function (ImportConnector $connector) use ($connectorException): \Generator {
320331
$connector->setRecoverableExceptionHandler(new StatelessRecoverableExceptionHandler(
321-
function (\Exception $exception) use ($connectorException) {
332+
static function (\Exception $exception) use ($connectorException) {
322333
self::assertSame($connectorException, $exception);
323334

324335
throw new \RuntimeException('This exception is thrown by the provider handler.');
@@ -330,7 +341,7 @@ function (\Exception $exception) use ($connectorException) {
330341
;
331342

332343
$this->expectException(\RuntimeException::class);
333-
$this->porter->importOne($this->specification);
344+
$this->porter->importOne($this->singleSpecification);
334345
}
335346

336347
#endregion

test/Integration/PorterTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ abstract class PorterTest extends AsyncTestCase
3737
*/
3838
protected $resource;
3939

40+
/**
41+
* @var ProviderResource|AsyncResource|MockInterface
42+
*/
43+
protected $singleResource;
44+
4045
/**
4146
* @var Connector|AsyncConnector|MockInterface
4247
*/
@@ -47,6 +52,11 @@ abstract class PorterTest extends AsyncTestCase
4752
*/
4853
protected $specification;
4954

55+
/**
56+
* @var ImportSpecification|AsyncImportSpecification
57+
*/
58+
protected $singleSpecification;
59+
5060
/**
5161
* @var ContainerInterface|MockInterface
5262
*/
@@ -62,6 +72,8 @@ protected function setUp(): void
6272
$this->connector = $this->provider->getConnector();
6373
$this->resource = MockFactory::mockResource($this->provider);
6474
$this->specification = new ImportSpecification($this->resource);
75+
$this->singleResource = MockFactory::mockSingleRecordResource($this->provider);
76+
$this->singleSpecification = new ImportSpecification($this->singleResource);
6577
}
6678

6779
/**

test/MockFactory.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,15 @@ public static function mockProvider()
5353
/**
5454
* @return ProviderResource|AsyncResource|MockInterface
5555
*/
56-
public static function mockResource(Provider $provider, \Iterator $return = null)
56+
public static function mockResource(Provider $provider, \Iterator $return = null, bool $single = false)
5757
{
58-
$resource = \Mockery::mock(ProviderResource::class, AsyncResource::class, SingleRecordResource::class)
58+
/** @var ProviderResource|AsyncResource|MockInterface $resource */
59+
$resource = \Mockery::mock(
60+
...array_merge(
61+
[ProviderResource::class, AsyncResource::class],
62+
$single ? [SingleRecordResource::class] : []
63+
)
64+
)
5965
->shouldReceive('getProviderClassName')
6066
->andReturn(\get_class($provider))
6167
->shouldReceive('fetch')
@@ -80,6 +86,14 @@ public static function mockResource(Provider $provider, \Iterator $return = null
8086
return $resource;
8187
}
8288

89+
/**
90+
* @return ProviderResource|AsyncResource|MockInterface
91+
*/
92+
public static function mockSingleRecordResource(Provider $provider)
93+
{
94+
return self::mockResource($provider, null, true);
95+
}
96+
8397
/**
8498
* @return Promise|MockInterface
8599
*/

0 commit comments

Comments
 (0)