diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9395936..bce78ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,10 @@ The project follows the [Open Knowledge International coding standards](https:// All PHP Code should conform to [PHP-FIG](http://www.php-fig.org/psr/) accepted PSRs. +Flow Framework has a nice guide regarding coding standards: +* [Printable summary of most important coding guidelines on one page **(.pdf)**](http://flowframework.readthedocs.io/en/stable/_downloads/Flow_Coding_Guidelines_on_one_page.pdf) +* [The full guide **(.html)**](http://flowframework.readthedocs.io/en/stable/TheDefinitiveGuide/PartV/CodingGuideLines/PHP.html) + ## Getting Started diff --git a/README.md b/README.md index c237089..13c36aa 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ $ composer require frictionlessdata/datapackage ```php use frictionlessdata\datapackage; -$datapackage = new Datapackage("tests/fixtures/multi_data_datapackage.json"); +// iterate over the data +$datapackage = new datapackage\Datapackage("tests/fixtures/multi_data_datapackage.json"); foreach ($datapackage as $resource) { print("-- ".$resource->name()." --"); $i = 0; @@ -34,6 +35,14 @@ foreach ($datapackage as $resource) { } } } + +// validate the descriptor +$validationErrors = datapackage\Datapackage::validate("tests/fixtures/simple_invalid_datapackage.json"); +if (count($validationErrors) == 0) { + print("descriptor is valid"); +} else { + print(datapackage\DatapackageValidationError::getErrorMessages($validationErrors)); +} ``` diff --git a/composer.json b/composer.json index 9deae4b..c5a5a2c 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,9 @@ "description": "A utility library for working with Data Packages", "license": "MIT", "require": { - "php": ">=5.4" + "php": ">=5.4", + "justinrainbow/json-schema": "^5.2", + "frictionlessdata/tableschema": "^0.1.2" }, "require-dev": { "phpunit/phpunit": "^4.8.35", @@ -15,6 +17,6 @@ } }, "scripts": { - "test": "phpunit --debug tests/ --coverage-clover coverage-clover.xml" + "test": "phpunit --debug --coverage-clover coverage-clover.xml --bootstrap tests/autoload.php tests/" } } diff --git a/src/DataStream.php b/src/DataStream.php index 0e59ac0..419756f 100644 --- a/src/DataStream.php +++ b/src/DataStream.php @@ -1,34 +1,30 @@ -_fopenResource = fopen($dataSource, "r"); + $this->fopenResource = fopen($dataSource, "r"); } catch (\Exception $e) { - throw new DataStreamOpenException("Failed to open source ".json_encode($dataSource)); + throw new Exceptions\DataStreamOpenException("Failed to open source ".json_encode($dataSource).": ".json_encode($e->getMessage())); } } public function __destruct() { - fclose($this->_fopenResource); + fclose($this->fopenResource); } public function rewind() { - if ($this->_currentLineNumber != 0) { + if ($this->currentLineNumber != 0) { throw new \Exception("DataStream does not support rewind, sorry"); } } public function current() { - $line = fgets($this->_fopenResource); + $line = fgets($this->fopenResource); if ($line === false) { return ""; } else { @@ -37,17 +33,18 @@ public function current() { } public function key() { - return $this->_currentLineNumber; + return $this->currentLineNumber; } public function next() { - $this->_currentLineNumber++; + $this->currentLineNumber++; } public function valid() { - return (!feof($this->_fopenResource)); + return (!feof($this->fopenResource)); } -} - -class DataStreamOpenException extends \Exception {}; + protected $currentLineNumber = 0; + protected $fopenResource; + protected $dataSource; +} diff --git a/src/Datapackage.php b/src/Datapackage.php index d8b714a..abf64bb 100644 --- a/src/Datapackage.php +++ b/src/Datapackage.php @@ -1,5 +1,5 @@ -_descriptor = $source; - $this->_basePath = $basePath; + $this->descriptor = $source; + $this->basePath = $basePath; } elseif (is_string($source)) { - if (Utils::is_json_string($source)) { + if (Utils::isJsonString($source)) { try { - $this->_descriptor = json_decode($source); + $this->descriptor = json_decode($source); } catch (\Exception $e) { - throw new DatapackageInvalidSourceException("Failed to load source: ".json_encode($source).": ".$e->getMessage()); + throw new Exceptions\DatapackageInvalidSourceException( + "Failed to load source: ".json_encode($source).": ".$e->getMessage() + ); } - $this->_basePath = $basePath; - } elseif ($this->_isHttpSource($source)) { + $this->basePath = $basePath; + } elseif ($this->isHttpSource($source)) { try { - $this->_descriptor = json_decode(file_get_contents($this->_normalizeHttpSource($source))); + $this->descriptor = json_decode(file_get_contents($this->normalizeHttpSource($source))); } catch (\Exception $e) { - throw new DatapackageInvalidSourceException("Failed to load source: ".json_encode($source).": ".$e->getMessage()); + throw new Exceptions\DatapackageInvalidSourceException( + "Failed to load source: ".json_encode($source).": ".$e->getMessage() + ); } // http sources don't allow relative paths, hence basePath should remain null - $this->_basePath = null; + $this->basePath = null; } else { if (empty($basePath)) { - $this->_basePath = dirname($source); + $this->basePath = dirname($source); } else { - $this->_basePath = $basePath; - $absPath = $this->_basePath.DIRECTORY_SEPARATOR.$source; + $this->basePath = $basePath; + $absPath = $this->basePath.DIRECTORY_SEPARATOR.$source; if (file_exists($absPath)) { $source = $absPath; } } try { - $this->_descriptor = json_decode(file_get_contents($source)); + $this->descriptor = json_decode(file_get_contents($source)); } catch (\Exception $e) { - throw new DatapackageInvalidSourceException("Failed to load source: ".json_encode($source).": ".$e->getMessage()); + throw new Exceptions\DatapackageInvalidSourceException( + "Failed to load source: ".json_encode($source).": ".$e->getMessage() + ); } } } else { - throw new DatapackageInvalidSourceException("Invalid source: ".json_encode($source)); + throw new Exceptions\DatapackageInvalidSourceException( + "Invalid source: ".json_encode($source) + ); } } - protected function _normalizeHttpSource($source) + public static function validate($source, $basePath=null) { - return $source; + try { + $datapackage = new self($source, $basePath); + return DatapackageValidator::validate($datapackage->descriptor()); + } catch (\Exception $e) { + return [new DatapackageValidationError(DatapackageValidationError::LOAD_FAILED, $e->getMessage())]; + } + } - protected function _isHttpSource($source) + /** + * get the descriptor as a native PHP object + * + * @return object + */ + public function descriptor() { - return Utils::is_http_source($source); + return $this->descriptor; } - protected function _initResource($resourceDescriptor) + // standard iterator functions - to iterate over the resources + public function rewind() {$this->currentResourcePosition = 0;} + public function current() { return $this->initResource($this->descriptor()->resources[$this->currentResourcePosition]); } + public function key() { return $this->currentResourcePosition; } + public function next() { $this->currentResourcePosition++; } + public function valid() { return isset($this->descriptor()->resources[$this->currentResourcePosition]); } + + protected $descriptor; + protected $currentResourcePosition = 0; + protected $basePath; + + /** + * allows extending classes to add custom sources + * used by unit tests to add a mock http source + * + * @param string $source + * @return string + */ + protected function normalizeHttpSource($source) { - return new Resource($resourceDescriptor, $this->_basePath); + return $source; } - public function descriptor() + /** + * allows extending classes to add custom sources + * used by unit tests to add a mock http source + * + * @param string $source + * @return bool + */ + protected function isHttpSource($source) { - return $this->_descriptor; + return Utils::isHttpSource($source); } - // standard iterator functions - to iterate over the resources - public function rewind() { $this->_currentResourcePosition = 0; } - public function current() { return $this->_initResource($this->descriptor()->resources[$this->_currentResourcePosition]); } - public function key() { return $this->_currentResourcePosition; } - public function next() { $this->_currentResourcePosition++; } - public function valid() { return isset($this->descriptor()->resources[$this->_currentResourcePosition]); } + /** + * called by the resources iterator for each iteration + * + * @param object $resourceDescriptor + * @return \frictionlessdata\datapackage\Resource + */ + protected function initResource($resourceDescriptor) + { + return new Resource($resourceDescriptor, $this->basePath); + } } - - -class DatapackageInvalidSourceException extends \Exception {}; diff --git a/src/DatapackageValidationError.php b/src/DatapackageValidationError.php new file mode 100644 index 0000000..cb090d3 --- /dev/null +++ b/src/DatapackageValidationError.php @@ -0,0 +1,8 @@ +get_validation_errors(); + } + + protected function _validateSchema() + { + // Validate + $validator = new \JsonSchema\Validator(); + $validator->validate( + $this->descriptor, (object)[ + '$ref' => 'file://' . realpath(dirname(__FILE__)).'/schemas/data-package.json' + ] + ); + if (!$validator->isValid()) { + foreach ($validator->getErrors() as $error) { + $this->_addError( + DatapackageValidationError::SCHEMA_VIOLATION, + sprintf("[%s] %s", $error['property'], $error['message']) + ); + } + } + } +} diff --git a/src/Exceptions/DataStreamOpenException.php b/src/Exceptions/DataStreamOpenException.php new file mode 100644 index 0000000..b6894cc --- /dev/null +++ b/src/Exceptions/DataStreamOpenException.php @@ -0,0 +1,6 @@ +_basePath = $basePath; - $this->_descriptor = $descriptor; + $this->basePath = $basePath; + $this->descriptor = $descriptor; } - protected function _isHttpSource($dataSource) + public function descriptor() { - return Utils::is_http_source($dataSource); + return $this->descriptor; } - protected function _normalizeDataSource($dataSource) + public function name() { - if (!empty($this->_basePath) && !Utils::is_http_source($dataSource)) { - // TODO: support JSON pointers - $absPath = $this->_basePath.DIRECTORY_SEPARATOR.$dataSource; - if (file_exists($absPath)) { - $dataSource = $absPath; - } - } - return $dataSource; + return $this->descriptor()->name; } - protected function _getDataStream($dataSource) + // standard iterator functions - to iterate over the data sources + public function rewind() { $this->_currentDataPosition = 0; } + public function current() { return $this->getDataStream($this->descriptor()->data[$this->currentDataPosition]); } + public function key() { return $this->currentDataPosition; } + public function next() { $this->currentDataPosition++; } + public function valid() { return isset($this->descriptor()->data[$this->currentDataPosition]); } + + protected $descriptor; + protected $basePath; + protected $currentDataPosition = 0; + + /** + * allows extending classes to add custom sources + * used by unit tests to add a mock http source + * + * @param string $dataSource + * @return bool + */ + protected function isHttpSource($dataSource) { - return new DataStream($this->_normalizeDataSource($dataSource)); + return Utils::isHttpSource($dataSource); } - public function descriptor() + /** + * allows extending classes to add custom sources + * used by unit tests to add a mock http source + * + * @param string $dataSource + * @return string + */ + protected function normalizeDataSource($dataSource) { - return $this->_descriptor; + if (!empty($this->basePath) && !$this->isHttpSource($dataSource)) { + // TODO: support JSON pointers + $absPath = $this->basePath.DIRECTORY_SEPARATOR.$dataSource; + if (file_exists($absPath)) { + $dataSource = $absPath; + } + } + return $dataSource; } - public function name() + protected function getDataStream($dataSource) { - return $this->descriptor()->name; + return new DataStream($this->normalizeDataSource($dataSource)); } - - // standard iterator functions - to iterate over the data sources - public function rewind() { $this->_currentDataPosition = 0; } - public function current() { return $this->_getDataStream($this->descriptor()->data[$this->_currentDataPosition]); } - public function key() { return $this->_currentDataPosition; } - public function next() { $this->_currentDataPosition++; } - public function valid() { return isset($this->descriptor()->data[$this->_currentDataPosition]); } } diff --git a/src/Utils.php b/src/Utils.php index bc65bf2..d78fb04 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -1,10 +1,10 @@ -fixturesPath = dirname(__FILE__)."/fixtures"; } - /** - * @param object $expectedDescriptor - * @param Datapackage $datapackage - */ - public function assertDatapackageDescriptor($expectedDescriptor, $datapackage) - { - $this->assertEquals($expectedDescriptor, $datapackage->descriptor()); - } - - /** - * @param array $expectedData - * @param Datapackage $datapackage - */ - public function assertDatapackageData($expectedData, $datapackage) - { - $allResourcesData = []; - foreach ($datapackage as $resource) { - $resourceData = []; - foreach ($resource as $dataStream) { - $data = []; - foreach ($dataStream as $line) { - $data[] = $line; - } - $resourceData[] = $data; - } - $allResourcesData[$resource->name()] = $resourceData; - } - $this->assertEquals($expectedData, $allResourcesData); - } - - /** - * @param string $source - * @param object $expectedDescriptor - * @param array $expectedData - */ - public function assertDatapackage($expectedDescriptor, $expectedData, $datapackage) - { - $this->assertDatapackageDescriptor($expectedDescriptor, $datapackage); - $this->assertDatapackageData($expectedData, $datapackage); - } - - public function assertDatapackageException($expectedExceptionClass, $datapackageCallback) - { - try { - $datapackageCallback(); - } catch (\Exception $e) { - $this->assertEquals($expectedExceptionClass, get_class($e), $e->getMessage()); - } - } - public function testNativePHPArrayShouldFail() { $descriptorArray = $this->simpleDescriptorArray; $this->assertDatapackageException( - "frictionlessdata\\datapackage\\DatapackageInvalidSourceException", + "frictionlessdata\\datapackage\\Exceptions\\DatapackageInvalidSourceException", function() use ($descriptorArray) { new Datapackage($descriptorArray); } ); } @@ -93,7 +43,7 @@ public function testNativePHPObjectWithoutBasePathShouldFail() { $descriptor = $this->simpleDescriptor; $this->assertDatapackageException( - "frictionlessdata\\datapackage\\DataStreamOpenException", + "frictionlessdata\\datapackage\\Exceptions\\DataStreamOpenException", function() use ($descriptor) { new Datapackage($descriptor); } ); } @@ -110,7 +60,7 @@ public function testJsonStringWithoutBasePathShouldFail() { $source = json_encode($this->simpleDescriptor); $this->assertDatapackageException( - "frictionlessdata\\datapackage\\DataStreamOpenException", + "frictionlessdata\\datapackage\\Exceptions\\DataStreamOpenException", function() use ($source) { new Datapackage($source); } ); } @@ -127,7 +77,7 @@ public function testJsonStringWithBasePath() public function testNonExistantFileShouldFail() { $this->assertDatapackageException( - "frictionlessdata\\datapackage\\DatapackageInvalidSourceException", + "frictionlessdata\\datapackage\\Exceptions\\DatapackageInvalidSourceException", function() { new Datapackage("-invalid-"); } ); } @@ -157,7 +107,7 @@ public function testHttpSource() (object)["name" => "resource-name", "data" => [] ] ] ], ["resource-name" => []], - new MockDatapackage("mock-http://simple_valid_datapackage_no_data.json") + new Mocks\MockDatapackage("mock-http://simple_valid_datapackage_no_data.json") ); } @@ -203,27 +153,68 @@ public function testMultiDataDatapackage() ], $out); } -} - - -class MockDatapackage extends Datapackage { + public function testDatapackageValidation() + { + $this->assertEquals([], Datapackage::validate("tests/fixtures/multi_data_datapackage.json")); + } - protected function _isHttpSource($dataSource) + public function testDatapackageValidationFailed() { - return ( - strpos($dataSource, "mock-http://") === 0 - || parent::_isHttpSource($dataSource) + $this->assertEquals( + "[resources] The property resources is required", + DatapackageValidationError::getErrorMessages( + Datapackage::validate("tests/fixtures/simple_invalid_datapackage.json") + ) ); } - protected function _normalizeHttpSource($dataSource) + /** + * @param object $expectedDescriptor + * @param Datapackage $datapackage + */ + protected function assertDatapackageDescriptor($expectedDescriptor, $datapackage) + { + $this->assertEquals($expectedDescriptor, $datapackage->descriptor()); + } + + /** + * @param array $expectedData + * @param Datapackage $datapackage + */ + protected function assertDatapackageData($expectedData, $datapackage) { - $dataSource = parent::_normalizeHttpSource($dataSource); - if (strpos($dataSource, "mock-http://") === 0) { - $dataSource = str_replace("mock-http://", "", $dataSource); - $dataSource = dirname(__FILE__).DIRECTORY_SEPARATOR."fixtures".DIRECTORY_SEPARATOR.$dataSource; + $allResourcesData = []; + foreach ($datapackage as $resource) { + $resourceData = []; + foreach ($resource as $dataStream) { + $data = []; + foreach ($dataStream as $line) { + $data[] = $line; + } + $resourceData[] = $data; + } + $allResourcesData[$resource->name()] = $resourceData; } - return $dataSource; + $this->assertEquals($expectedData, $allResourcesData); } -} \ No newline at end of file + /** + * @param string $source + * @param object $expectedDescriptor + * @param array $expectedData + */ + protected function assertDatapackage($expectedDescriptor, $expectedData, $datapackage) + { + $this->assertDatapackageDescriptor($expectedDescriptor, $datapackage); + $this->assertDatapackageData($expectedData, $datapackage); + } + + protected function assertDatapackageException($expectedExceptionClass, $datapackageCallback) + { + try { + $datapackageCallback(); + } catch (\Exception $e) { + $this->assertEquals($expectedExceptionClass, get_class($e), $e->getMessage()); + } + } +} diff --git a/tests/Mocks/MockDatapackage.php b/tests/Mocks/MockDatapackage.php new file mode 100644 index 0000000..b5b1740 --- /dev/null +++ b/tests/Mocks/MockDatapackage.php @@ -0,0 +1,25 @@ +assertResourceData( + [["foo"],["foo"]], + new Mocks\MockResource( + (object)[ + "name" => "resource-name", + "data" => [ + "mock-http://foo.txt", // basePath will not be added to http source + "foo.txt" // basePath will be added here + ] + ], + dirname(__FILE__).DIRECTORY_SEPARATOR."fixtures" + ) + ); + } + protected function assertResourceData($expectedData, $resource) { $actualData = []; @@ -19,37 +34,4 @@ protected function assertResourceData($expectedData, $resource) } $this->assertEquals($expectedData, $actualData); } - - public function testHttpDataSourceShouldNotGetBasePath() - { - $this->assertResourceData([["foo"],["foo"]], new MockResource((object)[ - "name" => "resource-name", - "data" => [ - "mock-http://foo.txt", // basePath will not be added to http source - "foo.txt" // basePath will be added here - ] - ], dirname(__FILE__).DIRECTORY_SEPARATOR."fixtures")); - } } - - -class MockResource extends Resource -{ - protected function _isHttpSource($dataSource) - { - return ( - strpos($dataSource, "mock-http://") === 0 - || parent::_isHttpSource($dataSource) - ); - } - - protected function _normalizeDataSource($dataSource) - { - $dataSource = parent::_normalizeDataSource($dataSource); - if (strpos($dataSource, "mock-http://") === 0) { - $dataSource = str_replace("mock-http://", "", $dataSource); - $dataSource = dirname(__FILE__).DIRECTORY_SEPARATOR."fixtures".DIRECTORY_SEPARATOR.$dataSource; - } - return $dataSource; - } -} \ No newline at end of file diff --git a/tests/autoload.php b/tests/autoload.php new file mode 100644 index 0000000..8b3ad11 --- /dev/null +++ b/tests/autoload.php @@ -0,0 +1,6 @@ +addPsr4("frictionlessdata\\datapackage\\tests\\", __DIR__, true); +$classLoader->register(); diff --git a/tests/fixtures/simple_invalid_datapackage.json b/tests/fixtures/simple_invalid_datapackage.json new file mode 100644 index 0000000..80ab287 --- /dev/null +++ b/tests/fixtures/simple_invalid_datapackage.json @@ -0,0 +1,3 @@ +{ + "name": "datapackage-name" +} \ No newline at end of file