Skip to content

Commit

Permalink
Merge pull request #19 from courtney-miles/enhance/13/EnsureConsisten…
Browse files Browse the repository at this point in the history
…tRecordValues

#13 Ensure CSV rows have consistent number of fields.
  • Loading branch information
courtney-miles committed Sep 3, 2018
2 parents 2e30671 + 3982ba6 commit bda34ad
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 15 deletions.
27 changes: 24 additions & 3 deletions src/Extract/CsvFileExtractor/CsvFileExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace MilesAsylum\Slurp\Extract\CsvFileExtractor;

use CallbackFilterIterator;
use League\Csv\Reader;
use MilesAsylum\Slurp\Extract\ExtractorInterface;

Expand All @@ -19,18 +20,20 @@ class CsvFileExtractor implements ExtractorInterface

private $headers = [];

private $headerOffset;

public function __construct(Reader $csvReader)
{
$this->csvReader = $csvReader;
}

/**
* Loads the first row in the CSV file as the headers.
* @throws \League\Csv\Exception
*/
public function loadHeadersFromFile() : void
{
$this->csvReader->setHeaderOffset(0);
$this->headers = $this->csvReader->fetchOne();
$this->headerOffset = 0;
}

public function setHeaders(array $headers) : void
Expand All @@ -40,6 +43,24 @@ public function setHeaders(array $headers) : void

public function getIterator() : \Iterator
{
return $this->csvReader->getRecords($this->headers);
return $this->prepareRecords($this->csvReader->getRecords(), $this->headers);
}

protected function prepareRecords(\Iterator $records, array $headers): \Iterator
{
if ($this->headerOffset !== null) {
$records = new CallbackFilterIterator($records, function (array $record, int $offset): bool {
return $offset !== $this->headerOffset;
});
}

$valueCount = !empty($headers) ? count($headers) : count($this->csvReader->fetchOne());
$records = new VerifyValueCountIterator($records, $valueCount);

if (!empty($headers)) {
$records = new MapIterator($records, $headers);
}

return $records;
}
}
31 changes: 31 additions & 0 deletions src/Extract/CsvFileExtractor/MapIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/**
* Author: Courtney Miles
* Date: 27/08/18
* Time: 6:32 PM
*/

namespace MilesAsylum\Slurp\Extract\CsvFileExtractor;

use Traversable;

class MapIterator extends \IteratorIterator
{
/**
* @var array
*/
private $headers;

public function __construct(Traversable $iterator, array $headers)
{
parent::__construct($iterator);
$this->headers = $headers;
}

public function current()
{
$record = parent::current();

return array_combine($this->headers, $record);
}
}
45 changes: 45 additions & 0 deletions src/Extract/CsvFileExtractor/VerifyValueCountIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* Author: Courtney Miles
* Date: 27/08/18
* Time: 6:05 PM
*/

namespace MilesAsylum\Slurp\Extract\CsvFileExtractor;

use MilesAsylum\Slurp\Extract\Exception\ValueCountMismatchException;
use Traversable;

class VerifyValueCountIterator extends \IteratorIterator
{
private $expectedValueCount;

public function __construct(Traversable $iterator, int $expectedValueCount)
{
parent::__construct($iterator);
$this->expectedValueCount = $expectedValueCount;
}

/**
* {@inheritdoc}
* @throws ValueCountMismatchException
*/
public function current()
{
$record = parent::current();

if (count($record) !== $this->expectedValueCount) {
$recordId = $this->key();
throw new ValueCountMismatchException(
sprintf(
'Record %s contained %s values where we expected %s.',
$recordId,
count($record),
$this->expectedValueCount
)
);
}

return $record;
}
}
12 changes: 12 additions & 0 deletions src/Extract/Exception/ExceptionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
/**
* Author: Courtney Miles
* Date: 27/08/18
* Time: 6:10 PM
*/

namespace MilesAsylum\Slurp\Extract\Exception;

interface ExceptionInterface
{
}
13 changes: 13 additions & 0 deletions src/Extract/Exception/ValueCountMismatchException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
/**
* Author: Courtney Miles
* Date: 27/08/18
* Time: 6:10 PM
*/

namespace MilesAsylum\Slurp\Extract\Exception;

class ValueCountMismatchException extends \Exception implements ExceptionInterface
{

}
82 changes: 70 additions & 12 deletions tests/Slurp/Extract/CsvFileExtractor/CsvFileExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@

use League\Csv\Reader;
use MilesAsylum\Slurp\Extract\CsvFileExtractor\CsvFileExtractor;
use MilesAsylum\Slurp\Extract\Exception\ValueCountMismatchException;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;


/**
* @covers \MilesAsylum\Slurp\Extract\CsvFileExtractor\CsvFileExtractor
*/
class CsvFileExtractorTest extends TestCase
{
use MockeryPHPUnitIntegration;
Expand All @@ -35,25 +40,78 @@ public function setUp()
$this->csvExtractor = new CsvFileExtractor($this->mockReader);
}

public function testLoadHeaders()
public function testNoHeaders()
{
$this->mockReader->shouldReceive('setHeaderOffset')
->with(0)
->once();
$csvRows = [[123, 234]];
$this->setUpMockReader($this->mockReader, $csvRows);

$this->csvExtractor->loadHeadersFromFile();
foreach ($this->csvExtractor as $rowId => $row) {
$this->assertArrayHasKey($rowId, $csvRows);
$this->assertSame($csvRows[$rowId], $row);
}
}

public function testUseSuppliedHeaders()
public function testUseHeadersFromFile()
{
$headers = ['col 1', 'col 2'];
$mockIterator = \Mockery::mock(\Iterator::class);
$this->mockReader->shouldReceive('getRecords')
->with($headers)
->andReturn($mockIterator);
$csvRows = [
['col_1', 'col_2'],
[123, 234]
];
$this->setUpMockReader($this->mockReader, $csvRows);
$this->csvExtractor->loadHeadersFromFile();

foreach ($this->csvExtractor as $rowId => $row) {
$this->assertArrayHasKey($rowId, $csvRows);
$this->assertSame(array_combine($csvRows[0], $csvRows[$rowId]), $row);
}
}

public function testSetHeaders()
{
$headers = ['col_1', 'col_2'];
$csvRows = [[123, 234]];
$this->setUpMockReader($this->mockReader, $csvRows);
$this->csvExtractor->setHeaders($headers);

$this->assertSame($mockIterator, $this->csvExtractor->getIterator());
foreach ($this->csvExtractor as $rowId => $row) {
$this->assertArrayHasKey($rowId, $csvRows);
$this->assertSame(array_combine($headers, $csvRows[$rowId]), $row);
}
}

public function testExceptionOnInconsistentNumberOfRowValuesWithoutHeaders()
{
$this->expectException(ValueCountMismatchException::class);

$csvRows = [[123, 234],[345,456,567]];
$this->setUpMockReader($this->mockReader, $csvRows);

foreach ($this->csvExtractor as $rowId => $row) {
// Do nothing.
}
}

public function testExceptionOnInconsistentNumberOfRowValuesWithHeaders()
{
$this->expectException(ValueCountMismatchException::class);

$csvRows = [[123, 234, 345]];
$this->setUpMockReader($this->mockReader, $csvRows);
$this->csvExtractor->setHeaders(['col_1', 'col_2']);

foreach ($this->csvExtractor as $rowId => $row) {
// Do nothing.
}
}

public function setUpMockReader(MockInterface $mockReader, array $rows)
{
$mockReader->shouldReceive('fetchOne')
->withNoArgs()
->andReturn($rows[0]);

$mockReader->shouldReceive('getRecords')
->withNoArgs()
->andReturn(new \ArrayIterator($rows));
}
}
29 changes: 29 additions & 0 deletions tests/Slurp/Extract/CsvFileExtractor/MapIteratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/**
* Author: Courtney Miles
* Date: 27/08/18
* Time: 6:47 PM
*/

namespace MilesAsylum\Slurp\Tests\Slurp\Extract\CsvFileExtractor;

use MilesAsylum\Slurp\Extract\CsvFileExtractor\MapIterator;
use PHPUnit\Framework\TestCase;

class MapIteratorTest extends TestCase
{
public function testMapHeaders()
{
$mi = new MapIterator(
new \ArrayIterator([[123, 234]]),
['val_1', 'val_2']
);

$mi->rewind();

$this->assertSame(
['val_1' => 123, 'val_2' => 234],
$mi->current()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php
/**
* Author: Courtney Miles
* Date: 27/08/18
* Time: 6:53 PM
*/

namespace MilesAsylum\Slurp\Tests\Slurp\Extract\CsvFileExtractor;

use MilesAsylum\Slurp\Extract\CsvFileExtractor\VerifyValueCountIterator;
use MilesAsylum\Slurp\Extract\Exception\ValueCountMismatchException;
use PHPUnit\Framework\TestCase;

class VerifyValueCountIteratorTest extends TestCase
{
/**
* @dataProvider getValueCountMismatchTestData
*/
public function testValueCountMismatch($values, $expectedCount)
{
$this->expectException(ValueCountMismatchException::class);
$this->expectExceptionMessage(
sprintf(
'Record 0 contained %s values where we expected %.',
count($values),
$expectedCount
)
);

$iterator = new VerifyValueCountIterator(
new \ArrayIterator(
[$values]
),
$expectedCount
);

$iterator->rewind();
$iterator->current();
}

public function getValueCountMismatchTestData()
{
return [
[[123,234], 1],
[[123,234], 3],
];
}
}

0 comments on commit bda34ad

Please sign in to comment.