diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbc8dbf..61bf6ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,8 @@ $ composer test ## Phpunit - for unit tests +[![Travis](https://travis-ci.org/frictionlessdata/datapackage-php.svg?branch=master)](https://travis-ci.org/frictionlessdata/datapackage-php) + Phpunit is used for unit tests, you can find the tests under tests directory Running Phpunit directly: `vendor/bin/phpunit --bootstrap tests/autoload.php tests/` @@ -35,8 +37,22 @@ when running `composer test` phpunit generates coverage report in coverage-clove ## Scrutinizer-ci - for code analysis -[Scrutinizer-ci](https://scrutinizer-ci.com/) integrates with GitHub and runs on commits. +[![Scrutinizer-ci](https://scrutinizer-ci.com/g/OriHoch/datapackage-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/OriHoch/datapackage-php/) + +Scrutinizer-ci integrates with GitHub and runs on commits. It does static code analysis and ensure confirmation to the coding stnadards. At the moment, the integration with frictionlessdata repo is not working, you can setup a Scrutinizer-ci account for your fork and run against that. + + +## Publishing a release and updating Packagist + +[![Packagist](https://img.shields.io/packagist/dm/frictionlessdata/datapackage.svg)](https://packagist.org/packages/frictionlessdata/datapackage) +[![SemVer](https://img.shields.io/badge/versions-SemVer-brightgreen.svg)](http://semver.org/) + +* Publish a release (using SemVer) on GitHub +* go to https://packagist.org/packages/frictionlessdata/datapackage +* Login with GitHub which has permissions +* click "Update" +* all releases from GitHub appear as releases on Packagist diff --git a/README.md b/README.md index 13c36aa..fbab84c 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,10 @@ $ composer require frictionlessdata/datapackage ```php use frictionlessdata\datapackage; +// get a datapackage object +$datapackage = datapackage\Factory::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; @@ -36,12 +38,12 @@ foreach ($datapackage as $resource) { } } -// validate the descriptor -$validationErrors = datapackage\Datapackage::validate("tests/fixtures/simple_invalid_datapackage.json"); +// validate a datapackage descriptor +$validationErrors = datapackage\Factory::validate("tests/fixtures/simple_invalid_datapackage.json"); if (count($validationErrors) == 0) { print("descriptor is valid"); } else { - print(datapackage\DatapackageValidationError::getErrorMessages($validationErrors)); + print(datapackage\Validators\DatapackageValidationError::getErrorMessages($validationErrors)); } ``` diff --git a/composer.json b/composer.json index c5a5a2c..c9935d7 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "require": { "php": ">=5.4", "justinrainbow/json-schema": "^5.2", - "frictionlessdata/tableschema": "^0.1.2" + "frictionlessdata/tableschema": "^0.1.3" }, "require-dev": { "phpunit/phpunit": "^4.8.35", diff --git a/src/DataStream.php b/src/DataStream.php deleted file mode 100644 index 419756f..0000000 --- a/src/DataStream.php +++ /dev/null @@ -1,50 +0,0 @@ -fopenResource = fopen($dataSource, "r"); - } catch (\Exception $e) { - throw new Exceptions\DataStreamOpenException("Failed to open source ".json_encode($dataSource).": ".json_encode($e->getMessage())); - } - } - - public function __destruct() - { - fclose($this->fopenResource); - } - - public function rewind() { - if ($this->currentLineNumber != 0) { - throw new \Exception("DataStream does not support rewind, sorry"); - } - } - - public function current() { - $line = fgets($this->fopenResource); - if ($line === false) { - return ""; - } else { - return $line; - } - } - - public function key() { - return $this->currentLineNumber; - } - - public function next() { - $this->currentLineNumber++; - } - - public function valid() { - return (!feof($this->fopenResource)); - } - - protected $currentLineNumber = 0; - protected $fopenResource; - protected $dataSource; -} diff --git a/src/DataStreams/BaseDataStream.php b/src/DataStreams/BaseDataStream.php new file mode 100644 index 0000000..276f7a5 --- /dev/null +++ b/src/DataStreams/BaseDataStream.php @@ -0,0 +1,24 @@ +fopenResource = fopen($dataSource, "r"); + } catch (\Exception $e) { + throw new DataStreamOpenException("Failed to open data source ".json_encode($dataSource).": ".json_encode($e->getMessage())); + } + } + + public function __destruct() + { + fclose($this->fopenResource); + } + + public function rewind() { + if ($this->currentLineNumber == 0) { + // starting iterations + $this->currentLineNumber = 1; + } else { + throw new \Exception("DataStream does not support rewinding a stream, sorry"); + } + } + + public function current() { + return fgets($this->fopenResource); + } + + public function key() { + return $this->currentLineNumber; + } + + public function next() { + $this->currentLineNumber++; + } + + public function valid() { + return (!feof($this->fopenResource)); + } + + protected $currentLineNumber = 0; + protected $fopenResource; +} diff --git a/src/DataStreams/TabularDataStream.php b/src/DataStreams/TabularDataStream.php new file mode 100644 index 0000000..674f620 --- /dev/null +++ b/src/DataStreams/TabularDataStream.php @@ -0,0 +1,58 @@ +table = new Table($dataSource, $schema); + } catch (\Exception $e) { + throw new DataStreamOpenException("Failed to open tabular data source ".json_encode($dataSource).": ".json_encode($e->getMessage())); + } + } + } + + protected $table; + + public function rewind() { + $this->table->rewind(); + } + + /** + * @return array + * @throws DataStreamValidationException + */ + public function current() { + try { + return $this->table->current(); + } catch (TableRowValidationException $e) { + throw new DataStreamValidationException($e->getMessage()); + } + } + + public function key() { + return $this->table->key(); + } + + public function next() { + $this->table->next(); + } + + public function valid() { + return $this->table->valid(); + } +} diff --git a/src/Datapackage.php b/src/Datapackage.php deleted file mode 100644 index abf64bb..0000000 --- a/src/Datapackage.php +++ /dev/null @@ -1,130 +0,0 @@ -descriptor = $source; - $this->basePath = $basePath; - } elseif (is_string($source)) { - if (Utils::isJsonString($source)) { - try { - $this->descriptor = json_decode($source); - } catch (\Exception $e) { - throw new Exceptions\DatapackageInvalidSourceException( - "Failed to load source: ".json_encode($source).": ".$e->getMessage() - ); - } - $this->basePath = $basePath; - } elseif ($this->isHttpSource($source)) { - try { - $this->descriptor = json_decode(file_get_contents($this->normalizeHttpSource($source))); - } catch (\Exception $e) { - 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; - } else { - if (empty($basePath)) { - $this->basePath = dirname($source); - } else { - $this->basePath = $basePath; - $absPath = $this->basePath.DIRECTORY_SEPARATOR.$source; - if (file_exists($absPath)) { - $source = $absPath; - } - } - try { - $this->descriptor = json_decode(file_get_contents($source)); - } catch (\Exception $e) { - throw new Exceptions\DatapackageInvalidSourceException( - "Failed to load source: ".json_encode($source).": ".$e->getMessage() - ); - } - - } - } else { - throw new Exceptions\DatapackageInvalidSourceException( - "Invalid source: ".json_encode($source) - ); - } - } - - public static function validate($source, $basePath=null) - { - try { - $datapackage = new self($source, $basePath); - return DatapackageValidator::validate($datapackage->descriptor()); - } catch (\Exception $e) { - return [new DatapackageValidationError(DatapackageValidationError::LOAD_FAILED, $e->getMessage())]; - } - - } - - /** - * get the descriptor as a native PHP object - * - * @return object - */ - public function descriptor() - { - return $this->descriptor; - } - - // 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 $source; - } - - /** - * 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 Utils::isHttpSource($source); - } - - /** - * 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); - } -} diff --git a/src/DatapackageValidationError.php b/src/DatapackageValidationError.php deleted file mode 100644 index cb090d3..0000000 --- a/src/DatapackageValidationError.php +++ /dev/null @@ -1,8 +0,0 @@ -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/Datapackages/BaseDatapackage.php b/src/Datapackages/BaseDatapackage.php new file mode 100644 index 0000000..0a448c0 --- /dev/null +++ b/src/Datapackages/BaseDatapackage.php @@ -0,0 +1,56 @@ +descriptor = $descriptor; + $this->basePath = $basePath; + $validationErrors = DatapackageValidator::validate($this->descriptor()); + if (count($validationErrors) > 0) { + throw new DatapackageValidationFailedException($validationErrors); + } + } + + /** + * returns the descriptor as-is, without adding default values or normalizing + * @return object + */ + public function descriptor() + { + return $this->descriptor; + } + + // 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; + + /** + * called by the resources iterator for each iteration + * + * @param object $descriptor + * @return \frictionlessdata\datapackage\Resources\BaseResource + */ + protected function initResource($descriptor) + { + return Factory::resource($descriptor, $this->basePath); + } +} diff --git a/src/Datapackages/DefaultDatapackage.php b/src/Datapackages/DefaultDatapackage.php new file mode 100644 index 0000000..668ba60 --- /dev/null +++ b/src/Datapackages/DefaultDatapackage.php @@ -0,0 +1,7 @@ +validationErrors = $validationErrors; + parent::__construct("DefaultDatapackage validation failed: ".DatapackageValidationError::getErrorMessages($validationErrors)); + } +} diff --git a/src/Exceptions/ResourceValidationFailedException.php b/src/Exceptions/ResourceValidationFailedException.php new file mode 100644 index 0000000..6c4239c --- /dev/null +++ b/src/Exceptions/ResourceValidationFailedException.php @@ -0,0 +1,15 @@ +validationErrors = $validationErrors; + parent::__construct("resource validation failed: ".ResourceValidationError::getErrorMessages($validationErrors)); + } +} diff --git a/src/Factory.php b/src/Factory.php new file mode 100644 index 0000000..a733cd0 --- /dev/null +++ b/src/Factory.php @@ -0,0 +1,217 @@ +descriptor; + $basePath = $source->basePath; + $datapackageClass = static::getDatapackageClass($descriptor); + $datapackage = new $datapackageClass($descriptor, $basePath); + return $datapackage; + } + + /** + * create a resource object + * @param object $descriptor + * @param null|string $basePath + * @return Resources\BaseResource + * @throws Exceptions\ResourceValidationFailedException + */ + public static function resource($descriptor, $basePath=null) + { + $resourceClass = static::getResourceClass($descriptor); + $resource = new $resourceClass($descriptor, $basePath); + return $resource; + } + + /** + * validates a given datapackage descriptor + * will load all resources, and sample 10 lines of data from each data source + * @param mixed $source datapackage source - same as in datapackage function + * @param null|string $basePath same as in datapackage function + * @return Validators\DatapackageValidationError[] + */ + public static function validate($source, $basePath=null) + { + $curResource = 1; + $curData = null; + $curLine = null; + try { + $datapackage = static::datapackage($source, $basePath); + foreach ($datapackage as $resource) { + $curData = 1; + foreach ($resource as $dataStream) { + $curLine = 1; + foreach ($dataStream as $line) { + if ($curLine == self::VALIDATE_PEEK_LINES) break; + $curLine++; + } + $curData++; + } + $curResource++; + } + // no validation errors + return []; + } catch (Exceptions\DatapackageInvalidSourceException $e) { + // failed to load the datapackage descriptor + // return a list containing a single LOAD_FAILED validation error + return [ + new Validators\DatapackageValidationError( + Validators\DatapackageValidationError::LOAD_FAILED, $e->getMessage() + ) + ]; + } catch (Exceptions\DatapackageValidationFailedException $e) { + // datapackage descriptor failed validation - return the validation errors + return $e->validationErrors; + } catch (Exceptions\ResourceValidationFailedException $e) { + // resource descriptor failed validation - return the validation errors + return [ + new Validators\DatapackageValidationError( + Validators\DatapackageValidationError::RESOURCE_FAILED_VALIDATION, + [ + "resource" => $curResource, + "validationErrors" => $e->validationErrors + ] + ) + ]; + } catch (Exceptions\DataStreamOpenException $e) { + // failed to open data stream + return [ + new Validators\DatapackageValidationError( + Validators\DatapackageValidationError::DATA_STREAM_FAILURE, + [ + "resource" => $curResource, + "dataStream" => $curData, + "line" => 0, + "error" => $e->getMessage() + ] + ) + ]; + } catch (Exceptions\DataStreamValidationException $e) { + // failed to validate the data stream + return [ + new Validators\DatapackageValidationError( + Validators\DatapackageValidationError::DATA_STREAM_FAILURE, + [ + "resource" => $curResource, + "dataStream" => $curData, + "line" => $curLine, + "error" => $e->getMessage() + ] + ) + ]; + } + } + + protected static function getDatapackageClass($descriptor) + { + return Repository::getDatapackageClass($descriptor); + } + + protected static function getResourceClass($descriptor) + { + return Repository::getResourceClass($descriptor); + } + + /** + * allows extending classes to add custom sources + * used by unit tests to add a mock http source + */ + protected static function normalizeHttpSource($source) + { + return $source; + } + + /** + * allows extending classes to add custom sources + * used by unit tests to add a mock http source + */ + protected static function isHttpSource($source) + { + return Utils::isHttpSource($source); + } + + /** + * loads the datapackage descriptor from different sources + * returns an object containing: + * - the datapackage descriptor as native php object + * - normalized basePath + * @param $source + * @param $basePath + * @return object + * @throws Exceptions\DatapackageInvalidSourceException + */ + protected static function loadSource($source, $basePath) + { + if (is_object($source)) { + $descriptor = $source; + } elseif (is_string($source)) { + if (Utils::isJsonString($source)) { + try { + $descriptor = json_decode($source); + } catch (\Exception $e) { + throw new Exceptions\DatapackageInvalidSourceException( + "Failed to load source: ".json_encode($source).": ".$e->getMessage() + ); + } + } elseif (static::isHttpSource($source)) { + try { + $descriptor = json_decode(file_get_contents(static::normalizeHttpSource($source))); + } catch (\Exception $e) { + 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 + $basePath = null; + } else { + if (empty($basePath)) { + $basePath = dirname($source); + } else { + $absPath = $basePath.DIRECTORY_SEPARATOR.$source; + if (file_exists($absPath)) { + $source = $absPath; + } + } + try { + $descriptor = json_decode(file_get_contents($source)); + } catch (\Exception $e) { + throw new Exceptions\DatapackageInvalidSourceException( + "Failed to load source: ".json_encode($source).": ".$e->getMessage() + ); + } + + } + } else { + throw new Exceptions\DatapackageInvalidSourceException( + "Invalid source: ".json_encode($source) + ); + } + return (object)["descriptor" => $descriptor, "basePath" => $basePath]; + } +} \ No newline at end of file diff --git a/src/Repository.php b/src/Repository.php new file mode 100644 index 0000000..6b76b56 --- /dev/null +++ b/src/Repository.php @@ -0,0 +1,39 @@ +profile) && $descriptor->profile != "default") { + return $descriptor->profile; + } else { + return "data-resource"; + } + } + + public static function getDatapackageClass($descriptor) + { + return "frictionlessdata\\datapackage\\Datapackages\\DefaultDatapackage"; + } + + public static function getDatapackageValidationProfile($descriptor) + { + if (isset($descriptor->profile) && $descriptor->profile != "default") { + return $descriptor->profile; + } else { + return "data-package"; + } + } +} \ No newline at end of file diff --git a/src/Resource.php b/src/Resources/BaseResource.php similarity index 57% rename from src/Resource.php rename to src/Resources/BaseResource.php index b734788..ec9d078 100644 --- a/src/Resource.php +++ b/src/Resources/BaseResource.php @@ -1,26 +1,47 @@ basePath = $basePath; $this->descriptor = $descriptor; + $validationErrors = ResourceValidator::validate($this->descriptor()); + if (count($validationErrors) > 0) { + throw new ResourceValidationFailedException($validationErrors); + } } + /** + * @return object + */ public function descriptor() { return $this->descriptor; } + /** + * @return string + */ public function name() { return $this->descriptor()->name; } // standard iterator functions - to iterate over the data sources - public function rewind() { $this->_currentDataPosition = 0; } + 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++; } @@ -30,18 +51,6 @@ public function valid() { return isset($this->descriptor()->data[$this->currentD 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 Utils::isHttpSource($dataSource); - } - /** * allows extending classes to add custom sources * used by unit tests to add a mock http source @@ -51,7 +60,7 @@ protected function isHttpSource($dataSource) */ protected function normalizeDataSource($dataSource) { - if (!empty($this->basePath) && !$this->isHttpSource($dataSource)) { + if (!empty($this->basePath) && !Utils::isHttpSource($dataSource)) { // TODO: support JSON pointers $absPath = $this->basePath.DIRECTORY_SEPARATOR.$dataSource; if (file_exists($absPath)) { @@ -61,8 +70,9 @@ protected function normalizeDataSource($dataSource) return $dataSource; } - protected function getDataStream($dataSource) - { - return new DataStream($this->normalizeDataSource($dataSource)); - } + /** + * @param string $dataSource + * @return BaseDataStream + */ + abstract protected function getDataStream($dataSource); } diff --git a/src/Resources/DefaultResource.php b/src/Resources/DefaultResource.php new file mode 100644 index 0000000..e6aea7b --- /dev/null +++ b/src/Resources/DefaultResource.php @@ -0,0 +1,16 @@ +normalizeDataSource($dataSource)); + } +} diff --git a/src/Resources/TabularResource.php b/src/Resources/TabularResource.php new file mode 100644 index 0000000..2b0a2a1 --- /dev/null +++ b/src/Resources/TabularResource.php @@ -0,0 +1,28 @@ +descriptor()->schema; + } + + /** + * @param string $dataSource + * @return TabularDataStream + */ + protected function getDataStream($dataSource) + { + return new TabularDataStream($this->normalizeDataSource($dataSource), $this->schema()); + } + +} \ No newline at end of file diff --git a/src/Validators/BaseValidationError.php b/src/Validators/BaseValidationError.php new file mode 100644 index 0000000..51bc54b --- /dev/null +++ b/src/Validators/BaseValidationError.php @@ -0,0 +1,10 @@ +getValidationErrors(); + } + + /** + * should be implemented properly by extending classes + * should return the profile used for validation + * if using the default getValidationSchemaUrl function - this value should correspond to a file in schemas/ directory + * @return string + */ + protected function getValidationProfile() + { + return $this->descriptor->profile; + } + + /** + * get the url which the schema for validation can be fetched from + * @return string + */ + protected function getValidationSchemaUrl() + { + // TODO: support loading from url + return 'file://' . realpath(dirname(__FILE__))."/schemas/".$this->getValidationProfile().".json"; + } + + /** + * Allows to specify different error classes for different validators + * @return string + */ + protected function getSchemaValidationErrorClass() + { + return "frictionlessdata\\tableschema\\SchemaValidationError"; + } + + /** + * allows extending classes to modify the descriptor before passing to the validator + * @return object + */ + protected function getDescriptorForValidation() + { + return $this->descriptor; + } + + /** + * conver the validation error message received from JsonSchema to human readable string + * @param array $error + * @return string + */ + protected function getValidationErrorMessage($error) + { + return sprintf("[%s] %s", $error['property'], $error['message']); + } + + /** + * does the validation, adds errors to the validator object using _addError method + */ + protected function validateSchema() + { + $validator = new \JsonSchema\Validator(); + $descriptor = $this->getDescriptorForValidation(); + $validator->validate( + $descriptor, + (object)[ + "\$ref" => $this->getValidationSchemaUrl() + ] + ); + if (!$validator->isValid()) { + foreach ($validator->getErrors() as $error) { + $this->addError( + SchemaValidationError::SCHEMA_VIOLATION, + $this->getValidationErrorMessage($error) + ); + } + } + } + + /** + * Add an error to the validator object - errors are aggregated and returned by validate function + * @param integer $code + * @param null|mixed $extraDetails + */ + protected function addError($code, $extraDetails=null) + { + $errorClass = $this->getSchemaValidationErrorClass(); + $this->errors[] = new $errorClass($code, $extraDetails); + } +} diff --git a/src/Validators/DatapackageValidationError.php b/src/Validators/DatapackageValidationError.php new file mode 100644 index 0000000..882b278 --- /dev/null +++ b/src/Validators/DatapackageValidationError.php @@ -0,0 +1,26 @@ +code) { + case static::RESOURCE_FAILED_VALIDATION: + return "resource {$this->extraDetails['resource']} failed validation: " + .ResourceValidationError::getErrorMessages($this->extraDetails["validationErrors"]); + case static::DATA_STREAM_FAILURE: + return "resource {$this->extraDetails['resource']}, " + ."data stream {$this->extraDetails['dataStream']}" + //.($this->extraDetails['line']?", line number {$this->extraDetails['line']}":"") + .": ".$this->extraDetails['error']; + default: + return parent::getMessage(); + } + } +} diff --git a/src/Validators/DatapackageValidator.php b/src/Validators/DatapackageValidator.php new file mode 100644 index 0000000..bb9f929 --- /dev/null +++ b/src/Validators/DatapackageValidator.php @@ -0,0 +1,20 @@ +descriptor); + } +} diff --git a/src/Validators/ResourceValidationError.php b/src/Validators/ResourceValidationError.php new file mode 100644 index 0000000..2d54c7d --- /dev/null +++ b/src/Validators/ResourceValidationError.php @@ -0,0 +1,8 @@ +descriptor); + if ($profile == "tabular-data-resource") { + $profile = "tabular-data-package"; + } elseif ($profile == "data-resource") { + $profile = "data-package"; + } + return $profile; + } + + protected function getDescriptorForValidation() + { + $descriptor = (object)[ + "name" => "dummy-datapackage-name", + "resources" => [ + $this->descriptor + ] + ]; + if ($this->getValidationProfile() == "tabular-data-package") { + // profile is required for tabular data package + $descriptor->profile = "tabular-data-package"; + } + return $descriptor; + } + + protected function getValidationErrorMessage($error) + { + $property = $error['property']; + // silly hack to only show properties within the resource of the fake datapackage + $property = str_replace("resources[0].", "", $property); + return sprintf("[%s] %s", $property, $error['message']); + } +} diff --git a/src/schemas/data-package.json b/src/Validators/schemas/data-package.json similarity index 100% rename from src/schemas/data-package.json rename to src/Validators/schemas/data-package.json diff --git a/src/Validators/schemas/tabular-data-package.json b/src/Validators/schemas/tabular-data-package.json new file mode 100644 index 0000000..e26f6fa --- /dev/null +++ b/src/Validators/schemas/tabular-data-package.json @@ -0,0 +1,2141 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Tabular Data Package", + "description": "Tabular Data Package is a simple specification for data access and delivery of tabular data.", + "type": "object", + "required": [ + "resources", + "profile" + ], + "properties": { + "profile": { + "propertyOrder": 10, + "title": "Profile", + "description": "The profile of this descriptor.", + "context": "Every Package and Resource descriptor has a profile. The default profile, if none is declared, is `default`. The namespace for the profile is the type of descriptor, so, `default` for a Package descriptor is not the same as `default` for a Resource descriptor.", + "type": "string", + "default": "default", + "examples": [ + "{\n \"profile\": \"tabular-data-package\"\n}\n", + "{\n \"profile\": \"http://example.com/my-profiles-json-schema.json\"\n}\n" + ] + }, + "name": { + "propertyOrder": 20, + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "id": { + "propertyOrder": 30, + "title": "ID", + "description": "A property reserved for globally unique identifiers. Examples of identifiers that are unique include UUIDs and DOIs.", + "context": "A common usage pattern for Data Packages is as a packaging format within the bounds of a system or platform. In these cases, a unique identifier for a package is desired for common data handling workflows, such as updating an existing package. While at the level of the specification, global uniqueness cannot be validated, consumers using the `id` property `MUST` ensure identifiers are globally unique.", + "type": "string", + "examples": [ + "{\n \"id\": \"b03ec84-77fd-4270-813b-0c698943f7ce\"\n}\n", + "{\n \"id\": \"http://dx.doi.org/10.1594/PANGAEA.726855\"\n}\n" + ] + }, + "title": { + "propertyOrder": 40, + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "propertyOrder": 50, + "format": "textarea", + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "homepage": { + "propertyOrder": 60, + "title": "Home Page", + "description": "The home on the web that is related to this data package.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "uri": { + "title": "URI", + "description": "A URI (with some restrictions), being a fully qualified HTTP address, a relative POSIX path, or a JSON Pointer.", + "type": "string", + "format": "uri", + "examples": [ + "{\n \"uri\": \"file.csv\"\n}\n", + "{\n \"uri\": \"#/data/my-data\"\n}\n", + "{\n \"uri\": \"http://example.com/file.csv\"\n}\n" + ], + "context": "Implementations need to negotiate the type of URI provided, and dereference the data accordingly. There are restrictions imposed on URIs that are POSIX paths: see [the notes on descriptors](#descriptor) for more information." + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + } + }, + "examples": [ + "{\n \"homepage\": {\n \"name\": \"My Web Page\",\n \"uri\": \"http://example.com/\"\n }\n}\n" + ] + }, + "created": { + "propertyOrder": 70, + "title": "Created", + "description": "The datetime on which this descriptor was created.", + "context": "The datetime must conform to the string formats for datetime as described in [RFC3339](https://tools.ietf.org/html/rfc3339#section-5.6)", + "type": "string", + "format": "date-time", + "examples": [ + "{\n \"created\": \"1985-04-12T23:20:50.52Z\"\n}\n" + ] + }, + "contributors": { + "propertyOrder": 80, + "title": "Contributors", + "description": "The contributors to this descriptor.", + "type": "array", + "minItems": 1, + "items": { + "title": "Contributor", + "description": "A contributor to this descriptor.", + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "uri": { + "title": "URI", + "description": "A URI (with some restrictions), being a fully qualified HTTP address, a relative POSIX path, or a JSON Pointer.", + "type": "string", + "format": "uri", + "examples": [ + "{\n \"uri\": \"file.csv\"\n}\n", + "{\n \"uri\": \"#/data/my-data\"\n}\n", + "{\n \"uri\": \"http://example.com/file.csv\"\n}\n" + ], + "context": "Implementations need to negotiate the type of URI provided, and dereference the data accordingly. There are restrictions imposed on URIs that are POSIX paths: see [the notes on descriptors](#descriptor) for more information." + }, + "email": { + "title": "Email", + "description": "An email address.", + "type": "string", + "format": "email", + "examples": [ + "{\n \"email\": \"example@example.com\"\n}\n" + ] + }, + "role": { + "type": "string", + "enum": [ + "author", + "maintainer" + ] + } + }, + "required": [ + "name" + ], + "context": "Use of this property does not imply that the person was the original creator of, or a contributor to, the data in the descriptor, but refers to the composition of the descriptor itself." + }, + "examples": [ + "{\n \"contributors\": [\n {\n \"name\": \"Joe Bloggs\"\n }\n ]\n}\n", + "{\n \"contributors\": [\n {\n \"name\": \"Joe Bloggs\",\n \"email\": \"joe@example.com\",\n \"role\": \"author\"\n }\n ]\n}\n" + ] + }, + "keywords": { + "propertyOrder": 90, + "title": "Keywords", + "description": "A list of keywords that describe this package.", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + }, + "examples": [ + "{\n \"keywords\": [\n \"data\",\n \"fiscal\",\n \"transparency\"\n ]\n}\n" + ] + }, + "licenses": { + "propertyOrder": 100, + "title": "Licenses", + "description": "The license(s) under which this package is published.", + "type": "array", + "minItems": 1, + "items": { + "title": "License", + "description": "A license for this descriptor.", + "type": "object", + "required": [ + "uri" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "uri": { + "title": "URI", + "description": "A URI (with some restrictions), being a fully qualified HTTP address, a relative POSIX path, or a JSON Pointer.", + "type": "string", + "format": "uri", + "examples": [ + "{\n \"uri\": \"file.csv\"\n}\n", + "{\n \"uri\": \"#/data/my-data\"\n}\n", + "{\n \"uri\": \"http://example.com/file.csv\"\n}\n" + ], + "context": "Implementations need to negotiate the type of URI provided, and dereference the data accordingly. There are restrictions imposed on URIs that are POSIX paths: see [the notes on descriptors](#descriptor) for more information." + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + } + }, + "context": "Use of this property does not imply that the person was the original creator of, or a contributor to, the data in the descriptor, but refers to the composition of the descriptor itself." + }, + "context": "This property is not legally binding and does not guarantee that the package is licensed under the terms defined herein.", + "examples": [ + "{\n \"licenses\": [\n {\n \"name\": \"ODC-PDDL-1.0\",\n \"uri\": \"http://opendatacommons.org/licenses/pddl/\"\n }\n ]\n}\n" + ] + }, + "resources": { + "propertyOrder": 110, + "title": "Tabular Data Resources", + "description": "An `array` of Tabular Data Resource objects, each compliant with the [Tabular Data Resource](/tabular-data-resource/) specification.", + "type": "array", + "minItems": 1, + "items": { + "title": "Tabular Data Resource", + "description": "A Tabular Data Resource.", + "type": "object", + "required": [ + "name", + "data", + "schema", + "profile" + ], + "properties": { + "profile": { + "propertyOrder": 10, + "title": "Profile", + "description": "The profile of this descriptor.", + "context": "Every Package and Resource descriptor has a profile. The default profile, if none is declared, is `default`. The namespace for the profile is the type of descriptor, so, `default` for a Package descriptor is not the same as `default` for a Resource descriptor.", + "type": "string", + "default": "default", + "examples": [ + "{\n \"profile\": \"tabular-data-package\"\n}\n", + "{\n \"profile\": \"http://example.com/my-profiles-json-schema.json\"\n}\n" + ] + }, + "name": { + "propertyOrder": 20, + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "data": { + "propertyOrder": 30, + "title": "Data", + "description": "A reference to the data for this resource. `data` `MUST` be an array of valid URIs.", + "type": "array", + "minItems": 1, + "items": { + "examples": [ + "[ \"file.csv\" ]\n", + "[ \"#/data/my-data\" ]\n", + "[ \"http://example.com/file.csv\" ]\n" + ], + "title": "URI", + "description": "A URI (with some restrictions), being a fully qualified HTTP address, a relative POSIX path, or a JSON Pointer.", + "type": "string", + "format": "uri", + "context": "Implementations need to negotiate the type of URI provided, and dereference the data accordingly. There are restrictions imposed on URIs that are POSIX paths: see [the notes on descriptors](#descriptor) for more information." + }, + "context": "The dereferenced value of each referenced data source in the `data` `array` `MUST` be commensurate with a native, dereferenced representation of the data the resource describes. For example, in a *Tabular* Data Resource, this means that the dereferenced value of `data` `MUST` be an array.", + "examples": [ + "{\n \"data\": [\n \"file.csv\",\n \"file2.csv\"\n ]\n}\n", + "{\n \"data\": [\n \"http://example.com/file.csv\",\n \"http://example.com/file2.csv\"\n ]\n}\n", + "{\n \"data\": [\n \"#/data/my-data\",\n \"#/data/my-data2\"\n ]\n}\n" + ] + }, + "schema": { + "propertyOrder": 40, + "title": "Table Schema", + "description": "A Table Schema for this resource, compliant with the [Table Schema](/tableschema/) specification.", + "type": "object", + "required": [ + "fields" + ], + "properties": { + "fields": { + "type": "array", + "minItems": 1, + "items": { + "title": "Table Schema Field", + "type": "object", + "anyOf": [ + { + "type": "object", + "title": "String Field", + "description": "The field contains strings, that is, sequences of characters.", + "required": [ + "name" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `string`.", + "enum": [ + "string" + ] + }, + "format": { + "description": "The format keyword options for `string` are `default`, `email`, `uri`, `binary`, and `uuid`.", + "context": "The following `format` options are supported:\n * **default**: any valid string.\n * **email**: A valid email address.\n * **uri**: A valid URI.\n * **binary**: A base64 encoded string representing binary data.\n * **uuid**: A string that is a uuid.", + "enum": [ + "default", + "email", + "uri", + "binary", + "uuid" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `string` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "pattern": { + "type": "string", + "description": "A regular expression pattern to test each value of the property against, where a truthy response indicates validity.", + "context": "Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs)." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minLength": { + "type": "integer", + "description": "An integer that specifies the minimum length of a value." + }, + "maxLength": { + "type": "integer", + "description": "An integer that specifies the maximum length of a value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"name\",\n \"type\": \"string\"\n}\n", + "{\n \"name\": \"name\",\n \"type\": \"string\",\n \"format\": \"email\"\n}\n", + "{\n \"name\": \"name\",\n \"type\": \"string\",\n \"constraints\": {\n \"minLength\": 3,\n \"maxLength\": 35\n }\n}\n" + ] + }, + { + "type": "object", + "title": "Number Field", + "description": "The field contains numbers of any kind including decimals.", + "context": "The lexical formatting follows that of decimal in [XMLSchema](https://www.w3.org/TR/xmlschema-2/#decimal): a non-empty finite-length sequence of decimal digits separated by a period as a decimal indicator. An optional leading sign is allowed. If the sign is omitted, '+' is assumed. Leading and trailing zeroes are optional. If the fractional part is zero, the period and following zero(es) can be omitted. For example: '-1.23', '12678967.543233', '+100000.00', '210'.\n\nThe following special string values are permitted (case does not need to be respected):\n - NaN: not a number\n - INF: positive infinity\n - -INF: negative infinity\n\nA number `MAY` also have a trailing:\n - exponent: this `MUST` consist of an E followed by an optional + or - sign followed by one or more decimal digits (0-9)\n - percentage: the percentage sign: `%`. In conversion percentages should be divided by 100.\n\nIf both exponent and percentages are present the percentage `MUST` follow the exponent e.g. '53E10%' (equals 5.3).", + "required": [ + "name" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `number`.", + "enum": [ + "number" + ] + }, + "format": { + "description": "There are no format keyword options for `number`: only `default` is allowed.", + "enum": [ + "default" + ], + "default": "default" + }, + "decimalChar": { + "type": "string", + "description": "A string whose value is used to represent a decimal point within the number. The default value is `.`." + }, + "groupChar": { + "type": "string", + "description": "A string whose value is used to group digits within the number. The default value is `null`. A common value is `,` e.g. '100,000'." + }, + "currency": { + "type": "string", + "description": "A number that may include additional currency symbols." + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `number` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "pattern": { + "type": "string", + "description": "A regular expression pattern to test each value of the property against, where a truthy response indicates validity.", + "context": "Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs)." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minimum": { + "type": "string", + "description": "A minimum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `minLength`, which checks the number of items in the value. A `minimum` value constraint checks whether a field value is greater than or equal to the specified value." + }, + "maximum": { + "type": "string", + "description": "A maximum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `maxLength`, which checks the number of items in the value. A `maximum` value constraint checks whether a field value is smaller than or equal to the specified value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"field-name\",\n \"type\": \"number\"\n}\n", + "{\n \"name\": \"field-name\",\n \"type\": \"number\",\n \"constraints\": {\n \"enum\": [ \"1.00\", \"1.50\", \"2.00\" ]\n }\n}\n" + ] + }, + { + "type": "object", + "title": "Integer Field", + "description": "The field contains integers - that is whole numbers.", + "context": "Integer values are indicated in the standard way for any valid integer.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `integer`.", + "enum": [ + "integer" + ] + }, + "format": { + "description": "There are no format keyword options for `integer`: only `default` is allowed.", + "enum": [ + "default" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `integer` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "pattern": { + "type": "string", + "description": "A regular expression pattern to test each value of the property against, where a truthy response indicates validity.", + "context": "Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs)." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minimum": { + "type": "string", + "description": "A minimum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `minLength`, which checks the number of items in the value. A `minimum` value constraint checks whether a field value is greater than or equal to the specified value." + }, + "maximum": { + "type": "string", + "description": "A maximum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `maxLength`, which checks the number of items in the value. A `maximum` value constraint checks whether a field value is smaller than or equal to the specified value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"age\",\n \"type\": \"integer\",\n \"constraints\": {\n \"unique\": true,\n \"minimum\": 100,\n \"maximum\": 9999\n }\n}\n" + ] + }, + { + "type": "object", + "title": "Date Field", + "description": "The field contains temporal date values.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `date`.", + "enum": [ + "date" + ] + }, + "format": { + "description": "The format keyword options for `date` are `default`, `any`, and `{PATTERN}`.", + "context": "The following `format` options are supported:\n * **default**: An ISO8601 format string of YYYY-MM-DD.\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).", + "enum": [ + "default", + "any" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `date` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minimum": { + "type": "string", + "description": "A minimum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `minLength`, which checks the number of items in the value. A `minimum` value constraint checks whether a field value is greater than or equal to the specified value." + }, + "maximum": { + "type": "string", + "description": "A maximum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `maxLength`, which checks the number of items in the value. A `maximum` value constraint checks whether a field value is smaller than or equal to the specified value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"date_of_birth\",\n \"type\": \"date\"\n}\n", + "{\n \"name\": \"date_of_birth\",\n \"type\": \"date\",\n \"constraints\": {\n \"minimum\": \"01-01-1900\"\n }\n}\n", + "{\n \"name\": \"date_of_birth\",\n \"type\": \"date\",\n \"format\": \"MM-DD-YYYY\"\n}\n" + ] + }, + { + "type": "object", + "title": "Time Field", + "description": "The field contains temporal time values.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `time`.", + "enum": [ + "time" + ] + }, + "format": { + "description": "The format keyword options for `time` are `default`, `any`, and `{PATTERN}`.", + "context": "The following `format` options are supported:\n * **default**: An ISO8601 format string for time.\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).", + "enum": [ + "default", + "any" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `time` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minimum": { + "type": "string", + "description": "A minimum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `minLength`, which checks the number of items in the value. A `minimum` value constraint checks whether a field value is greater than or equal to the specified value." + }, + "maximum": { + "type": "string", + "description": "A maximum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `maxLength`, which checks the number of items in the value. A `maximum` value constraint checks whether a field value is smaller than or equal to the specified value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"appointment_start\",\n \"type\": \"time\"\n}\n", + "{\n \"name\": \"appointment_start\",\n \"type\": \"time\",\n \"format\": \"any\"\n}\n" + ] + }, + { + "type": "object", + "title": "Date Time Field", + "description": "The field contains temporal datetime values.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `datetime`.", + "enum": [ + "datetime" + ] + }, + "format": { + "description": "The format keyword options for `datetime` are `default`, `any`, and `{PATTERN}`.", + "context": "The following `format` options are supported:\n * **default**: An ISO8601 format string for datetime.\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).", + "enum": [ + "default", + "any" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `datetime` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minimum": { + "type": "string", + "description": "A minimum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `minLength`, which checks the number of items in the value. A `minimum` value constraint checks whether a field value is greater than or equal to the specified value." + }, + "maximum": { + "type": "string", + "description": "A maximum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `maxLength`, which checks the number of items in the value. A `maximum` value constraint checks whether a field value is smaller than or equal to the specified value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"timestamp\",\n \"type\": \"datetime\"\n}\n", + "{\n \"name\": \"timestamp\",\n \"type\": \"datetime\",\n \"format\": \"default\"\n}\n" + ] + }, + { + "type": "object", + "title": "Year Field", + "description": "A calendar year, being an integer with 4 digits. Equivalent to [gYear in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYear)", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `year`.", + "enum": [ + "year" + ] + }, + "format": { + "description": "There are no format keyword options for `year`: only `default` is allowed.", + "enum": [ + "default" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `year` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minimum": { + "type": "string", + "description": "A minimum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `minLength`, which checks the number of items in the value. A `minimum` value constraint checks whether a field value is greater than or equal to the specified value." + }, + "maximum": { + "type": "string", + "description": "A maximum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `maxLength`, which checks the number of items in the value. A `maximum` value constraint checks whether a field value is smaller than or equal to the specified value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"year\",\n \"type\": \"year\"\n}\n", + "{\n \"name\": \"year\",\n \"type\": \"year\",\n \"constraints\": {\n \"minimum\": 1970,\n \"maximum\": 2003\n }\n}\n" + ] + }, + { + "type": "object", + "title": "Year Month Field", + "description": "A calendar year month, being an integer with 1 or 2 digits. Equivalent to [gYearMonth in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYearMonth)", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `yearmonth`.", + "enum": [ + "yearmonth" + ] + }, + "format": { + "description": "There are no format keyword options for `yearmonth`: only `default` is allowed.", + "enum": [ + "default" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `yearmonth` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "pattern": { + "type": "string", + "description": "A regular expression pattern to test each value of the property against, where a truthy response indicates validity.", + "context": "Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs)." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minimum": { + "type": "string", + "description": "A minimum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `minLength`, which checks the number of items in the value. A `minimum` value constraint checks whether a field value is greater than or equal to the specified value." + }, + "maximum": { + "type": "string", + "description": "A maximum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `maxLength`, which checks the number of items in the value. A `maximum` value constraint checks whether a field value is smaller than or equal to the specified value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"month\",\n \"type\": \"yearmonth\"\n}\n", + "{\n \"name\": \"month\",\n \"type\": \"yearmonth\",\n \"constraints\": {\n \"minimum\": 1,\n \"maximum\": 6\n }\n}\n" + ] + }, + { + "type": "object", + "title": "Boolean Field", + "description": "The field contains boolean (true/false) data.", + "context": "Boolean values can be indicated with the following strings (case-insensitive, so, for example, 'Y' and 'y' are both acceptable):\n* **true**: 'yes', 'y', 'true', 't', '1'\n* **false**: 'no', 'n', 'false', 'f', '0'", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `boolean`.", + "enum": [ + "boolean" + ] + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `boolean` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"registered\",\n \"type\": \"boolean\"\n}\n" + ] + }, + { + "type": "object", + "title": "Object Field", + "description": "The field contains data which can be parsed as a valid JSON object.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `object`.", + "enum": [ + "object" + ] + }, + "format": { + "description": "There are no format keyword options for `object`: only `default` is allowed.", + "enum": [ + "default" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints apply for `object` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minLength": { + "type": "integer", + "description": "An integer that specifies the minimum length of a value." + }, + "maxLength": { + "type": "integer", + "description": "An integer that specifies the maximum length of a value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"extra\"\n \"type\": \"object\"\n}\n" + ] + }, + { + "type": "object", + "title": "GeoPoint Field", + "description": "The field contains data describing a geographic point.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `geopoint`.", + "enum": [ + "geopoint" + ] + }, + "format": { + "description": "The format keyword options for `geopoint` are `default`,`array`, and `object`.", + "context": "The following `format` options are supported:\n * **default**: A string of the pattern 'lon, lat', where `lon` is the longitude and `lat` is the latitude.\n * **array**: An array of exactly two items, where each item is either a number, or a string parsable as a number, and the first item is `lon` and the second item is `lat`.\n * **object**: A JSON object with exactly two keys, `lat` and `lon`", + "notes": [ + "Implementations `MUST` strip all white space in the default format of `lon, lat`." + ], + "enum": [ + "default", + "array", + "object" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `geopoint` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"post_office\",\n \"type\": \"geopoint\"\n}\n", + "{\n \"name\": \"post_office\",\n \"type\": \"geopoint\",\n \"format\": \"array\"\n}\n" + ] + }, + { + "type": "object", + "title": "GeoJSON Field", + "description": "The field contains a JSON object according to GeoJSON or TopoJSON", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `geojson`.", + "enum": [ + "geojson" + ] + }, + "format": { + "description": "The format keyword options for `geojson` are `default` and `topojson`.", + "context": "The following `format` options are supported:\n * **default**: A geojson object as per the [GeoJSON spec](http://geojson.org/).\n * **topojson**: A topojson object as per the [TopoJSON spec](https://github.com/topojson/topojson-specification/blob/master/README.md)", + "enum": [ + "default", + "topojson" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `geojson` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minLength": { + "type": "integer", + "description": "An integer that specifies the minimum length of a value." + }, + "maxLength": { + "type": "integer", + "description": "An integer that specifies the maximum length of a value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"city_limits\",\n \"type\": \"geojson\"\n}\n", + "{\n \"name\": \"city_limits\",\n \"type\": \"geojson\",\n \"format\": \"topojson\"\n}\n" + ] + }, + { + "type": "object", + "title": "Array Field", + "description": "The field contains data which can be parsed as a valid JSON array.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `array`.", + "enum": [ + "array" + ] + }, + "format": { + "description": "There are no format keyword options for `array`: only `default` is allowed.", + "enum": [ + "default" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints apply for `array` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minLength": { + "type": "integer", + "description": "An integer that specifies the minimum length of a value." + }, + "maxLength": { + "type": "integer", + "description": "An integer that specifies the maximum length of a value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"options\"\n \"type\": \"array\"\n}\n" + ] + }, + { + "type": "object", + "title": "Duration Field", + "description": "The field contains a duration of time.", + "context": "The lexical representation for duration is the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) extended format `PnYnMnDTnHnMnS`, where `nY` represents the number of years, `nM` the number of months, `nD` the number of days, 'T' is the date/time separator, `nH` the number of hours, `nM` the number of minutes and `nS` the number of seconds. The number of seconds can include decimal digits to arbitrary precision. Date and time elements including their designator may be omitted if their value is zero, and lower order elements may also be omitted for reduced precision. Here we follow the definition of [XML Schema duration datatype](http://www.w3.org/TR/xmlschema-2/#duration) directly and that definition is implicitly inlined here.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `duration`.", + "enum": [ + "duration" + ] + }, + "format": { + "description": "There are no format keyword options for `duration`: only `default` is allowed.", + "enum": [ + "default" + ], + "default": "default" + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints are supported for `duration` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + }, + "minimum": { + "type": "string", + "description": "A minimum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `minLength`, which checks the number of items in the value. A `minimum` value constraint checks whether a field value is greater than or equal to the specified value." + }, + "maximum": { + "type": "string", + "description": "A maximum value for the property, context specific for the property type, and conforming with the format for the property.", + "context": "This is different to `maxLength`, which checks the number of items in the value. A `maximum` value constraint checks whether a field value is smaller than or equal to the specified value." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"period\"\n \"type\": \"duration\"\n}\n" + ] + }, + { + "type": "object", + "title": "Any Field", + "description": "Any value is accepted, including values that are not captured by the type/format/constraint requirements of the specification.", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "type": { + "description": "The type keyword, which `MUST` be a value of `any`.", + "enum": [ + "any" + ] + }, + "constraints": { + "title": "Constraints", + "description": "The following constraints apply to `any` fields.", + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Indicates whether a property must have a value for each instance.", + "context": "An empty string is considered to be a missing value." + }, + "unique": { + "type": "boolean", + "description": "When `true`, each value for the property `MUST` be unique." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "An array of values, where the property value `MUST` match any. Each enum item `MUST` comply with the type and format of the property." + } + } + }, + "rdfType": { + "type": "string", + "description": "The RDF type for this field." + } + }, + "examples": [ + "{\n \"name\": \"notes\",\n \"type\": \"any\"\n" + ] + } + ] + }, + "description": "An `array` of Table Schema Field objects.", + "examples": [ + "{\n \"fields\": [\n {\n \"name\": \"my-field-name\"\n }\n ]\n}\n", + "{\n \"fields\": [\n {\n \"name\": \"my-field-name\",\n \"type\": \"number\"\n },\n {\n \"name\": \"my-field-name-2\",\n \"type\": \"string\",\n \"format\": \"email\"\n }\n ]\n}\n" + ] + }, + "primaryKey": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + }, + "description": "A primary key is an array of field names, whose values `MUST` uniquely identify each row in the table.", + "context": "Each string in the `primaryKey` array `MUST` be unique, and `MUST` match a field name in the associated table. It is acceptable to have an array with a single value, indicating that the value of a single field is the primary key.", + "examples": [ + "{\n \"primaryKey\": [\n \"name\"\n ]\n}\n", + "{\n \"primaryKey\": [\n \"first_name\",\n \"last_name\"\n ]\n}\n" + ] + }, + "foreignKeys": { + "type": "array", + "minItems": 1, + "items": { + "title": "Table Schema Foreign Key", + "description": "Table Schema Foreign Key", + "type": "object", + "required": [ + "fields", + "reference" + ], + "properties": { + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "Fields that make up the primary key." + }, + "reference": { + "type": "object", + "required": [ + "resource", + "fields" + ], + "properties": { + "resource": { + "type": "string", + "format": "uri" + }, + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + } + } + } + }, + "examples": [ + "{\n \"foreignKeys\": [\n {\n \"fields\": \"state\",\n \"reference\": {\n \"resource\": \"the-resource\",\n \"fields\": \"state_id\"\n }\n }\n ]\n}\n", + "{\n \"foreignKeys\": [\n {\n \"fields\": \"state\",\n \"reference\": {\n \"resource\": \"\",\n \"fields\": \"id\"\n }\n }\n ]\n}\n" + ] + }, + "missingValues": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + }, + "default": [ + "" + ], + "description": "Values that when encountered in the source, should be considered as `null`, 'not present', or 'blank' values.", + "context": "Many datasets arrive with missing data values, either because a value was not collected or it never existed.\nMissing values may be indicated simply by the value being empty in other cases a special value may have been used e.g. `-`, `NaN`, `0`, `-9999` etc.\nThe `missingValues` property provides a way to indicate that these values should be interpreted as equivalent to null.\n\n`missingValues` are strings rather than being the data type of the particular field. This allows for comparison prior to casting and for fields to have missing value which are not of their type, for example a `number` field to have missing values indicated by `-`.\n\nThe default value of `missingValue` for a non-string type field is the empty string `''`. For string type fields there is no default for `missingValue` (for string fields the empty string `''` is a valid value and need not indicate null).", + "examples": [ + "{\n \"missingValues\": [\n \"-\",\n \"NaN\",\n \"\"\n ]\n}\n" + ] + } + }, + "examples": [ + "{\n \"schema\": {\n \"fields\": [\n {\n \"name\": \"first_name\",\n \"type\": \"string\"\n \"constraints\": {\n \"required\": true\n }\n },\n {\n \"name\": \"age\",\n \"type\": \"integer\"\n },\n ],\n \"primaryKey\": [\n \"name\"\n ]\n }\n}\n" + ] + }, + "title": { + "propertyOrder": 50, + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + }, + "description": { + "propertyOrder": 60, + "format": "textarea", + "title": "Description", + "description": "A text description. Markdown is encouraged.", + "type": "string", + "examples": [ + "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" + ] + }, + "homepage": { + "propertyOrder": 70, + "title": "Home Page", + "description": "The home on the web that is related to this data package.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "uri": { + "title": "URI", + "description": "A URI (with some restrictions), being a fully qualified HTTP address, a relative POSIX path, or a JSON Pointer.", + "type": "string", + "format": "uri", + "examples": [ + "{\n \"uri\": \"file.csv\"\n}\n", + "{\n \"uri\": \"#/data/my-data\"\n}\n", + "{\n \"uri\": \"http://example.com/file.csv\"\n}\n" + ], + "context": "Implementations need to negotiate the type of URI provided, and dereference the data accordingly. There are restrictions imposed on URIs that are POSIX paths: see [the notes on descriptors](#descriptor) for more information." + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + } + }, + "examples": [ + "{\n \"homepage\": {\n \"name\": \"My Web Page\",\n \"uri\": \"http://example.com/\"\n }\n}\n" + ] + }, + "sources": { + "propertyOrder": 140, + "options": { + "hidden": true + }, + "title": "Sources", + "description": "The raw sources for this resource.", + "type": "array", + "minItems": 1, + "items": { + "title": "Source", + "description": "A source file.", + "type": "object", + "required": [ + "uri" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "uri": { + "title": "URI", + "description": "A URI (with some restrictions), being a fully qualified HTTP address, a relative POSIX path, or a JSON Pointer.", + "type": "string", + "format": "uri", + "examples": [ + "{\n \"uri\": \"file.csv\"\n}\n", + "{\n \"uri\": \"#/data/my-data\"\n}\n", + "{\n \"uri\": \"http://example.com/file.csv\"\n}\n" + ], + "context": "Implementations need to negotiate the type of URI provided, and dereference the data accordingly. There are restrictions imposed on URIs that are POSIX paths: see [the notes on descriptors](#descriptor) for more information." + }, + "email": { + "title": "Email", + "description": "An email address.", + "type": "string", + "format": "email", + "examples": [ + "{\n \"email\": \"example@example.com\"\n}\n" + ] + } + } + }, + "examples": [ + "{\n \"sources\": [\n {\n \"name\": \"World Bank and OECD\",\n \"uri\": \"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD\"\n }\n ]\n}\n" + ] + }, + "licenses": { + "description": "The license(s) under which the resource is published.", + "propertyOrder": 150, + "options": { + "hidden": true + }, + "title": "Licenses", + "type": "array", + "minItems": 1, + "items": { + "title": "License", + "description": "A license for this descriptor.", + "type": "object", + "required": [ + "uri" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "uri": { + "title": "URI", + "description": "A URI (with some restrictions), being a fully qualified HTTP address, a relative POSIX path, or a JSON Pointer.", + "type": "string", + "format": "uri", + "examples": [ + "{\n \"uri\": \"file.csv\"\n}\n", + "{\n \"uri\": \"#/data/my-data\"\n}\n", + "{\n \"uri\": \"http://example.com/file.csv\"\n}\n" + ], + "context": "Implementations need to negotiate the type of URI provided, and dereference the data accordingly. There are restrictions imposed on URIs that are POSIX paths: see [the notes on descriptors](#descriptor) for more information." + }, + "title": { + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "{\n \"title\": \"My Package Title\"\n}\n" + ] + } + }, + "context": "Use of this property does not imply that the person was the original creator of, or a contributor to, the data in the descriptor, but refers to the composition of the descriptor itself." + }, + "context": "This property is not legally binding and does not guarantee that the package is licensed under the terms defined herein.", + "examples": [ + "{\n \"licenses\": [\n {\n \"name\": \"ODC-PDDL-1.0\",\n \"uri\": \"http://opendatacommons.org/licenses/pddl/\"\n }\n ]\n}\n" + ] + }, + "dialect": { + "propertyOrder": 50, + "title": "CSV Dialect", + "description": "The CSV dialect descriptor.", + "type": "object", + "required": [ + "delimiter", + "doubleQuote" + ], + "properties": { + "delimiter": { + "title": "Delimiter", + "description": "A character sequence to use as the field separator.", + "type": "string", + "default": ",", + "examples": [ + "{\n \"delimiter\": \",\"\n}\n", + "{\n \"delimiter\": \";\"\n}\n" + ] + }, + "doubleQuote": { + "title": "Double Quote", + "description": "Specifies the handling of quotes inside fields.", + "context": "If Double Quote is set to true, two consecutive quotes must be interpreted as one.", + "type": "boolean", + "default": true, + "examples": [ + "{\n \"doubleQuote\": true\n}\n" + ] + }, + "lineTerminator": { + "title": "Line Terminator", + "description": "Specifies the character sequence that must be used to terminate rows.", + "type": "string", + "default": "\r\n", + "examples": [ + "{\n \"lineTerminator\": \"\\r\\n\"\n}\n", + "{\n \"lineTerminator\": \"\\n\"\n}\n" + ] + }, + "nullSequence": { + "title": "Null Sequence", + "description": "Specifies the null sequence, for example, `\\N`.", + "type": "string", + "examples": [ + "{\n \"nullSequence\": \"\\N\"\n}\n" + ] + }, + "quoteChar": { + "title": "Quote Character", + "description": "Specifies a one-character string to use as the quoting character.", + "type": "string", + "default": "\"", + "examples": [ + "{\n \"quoteChar\": \"\"\n}\n", + "{\n \"quoteChar\": \"''\"\n}\n" + ] + }, + "escapeChar": { + "title": "Escape Character", + "description": "Specifies a one-character string to use as the escape character.", + "type": "string", + "default": "\\", + "examples": [ + "{\n \"escapeChar\": \"\\\\\"\n}\n" + ] + }, + "skipInitialSpace": { + "title": "Skip Initial Space", + "description": "Specifies the interpretation of whitespace immediately following a delimiter. If false, whitespace immediately after a delimiter should be treated as part of the subsequent field.", + "type": "boolean", + "default": true, + "examples": [ + "{\n \"skipInitialSpace\": true\n}\n" + ] + }, + "header": { + "title": "Header", + "description": "Specifies if the file includes a header row, always as the first row in the file.", + "type": "boolean", + "default": true, + "examples": [ + "{\n \"header\": true\n}\n" + ] + }, + "caseSensitiveHeader": { + "title": "Case Sensitive Header", + "description": "Specifies if the case of headers is meaningful.", + "context": "Use of case in source CSV files is not always an intentional decision. For example, should \"CAT\" and \"Cat\" be considered to have the same meaning.", + "type": "boolean", + "default": false, + "examples": [ + "{\n \"caseSensitiveHeader\": true\n}\n" + ] + } + }, + "examples": [ + "{\n \"dialect\": {\n \"delimiter\": \";\"\n }\n}\n", + "{\n \"dialect\": {\n \"delimiter\": \"\\t\",\n \"quoteChar\": \"''\"\n }\n}\n" + ] + }, + "format": { + "propertyOrder": 80, + "title": "Format", + "description": "The file format of this resource.", + "context": "`csv`, `xls`, `json` are examples of common formats.", + "type": "string", + "examples": [ + "{\n \"format\": \"xls\"\n}\n" + ] + }, + "mediatype": { + "propertyOrder": 90, + "title": "Media Type", + "description": "The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml).", + "type": "string", + "pattern": "^(.+)/(.+)$", + "examples": [ + "{\n \"mediatype\": \"text/csv\"\n}\n" + ] + }, + "encoding": { + "propertyOrder": 100, + "title": "Encoding", + "description": "The file encoding of this resource.", + "type": "string", + "default": "utf-8", + "examples": [ + "{\n \"encoding\": \"utf-8\"\n}\n" + ] + }, + "bytes": { + "propertyOrder": 110, + "options": { + "hidden": true + }, + "title": "Bytes", + "description": "The size of this resource in bytes.", + "type": "integer", + "examples": [ + "{\n \"bytes\": 2082\n}\n" + ] + }, + "hash": { + "propertyOrder": 120, + "options": { + "hidden": true + }, + "title": "Hash", + "type": "string", + "description": "The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format.", + "pattern": "^([^:]+:[a-fA-F0-9]+|[a-fA-F0-9]{32}|)$", + "examples": [ + "{\n \"hash\": \"d25c9c77f588f5dc32059d2da1136c02\"\n}\n", + "{\n \"hash\": \"SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0\"\n}\n" + ] + } + } + }, + "examples": [ + "{\n \"resources\": [\n {\n \"name\": \"my-data\",\n \"data\": [\n \"data.csv\"\n ],\n \"schema\": \"tableschema.json\",\n \"mediatype\": \"text/csv\"\n }\n ]\n}\n" + ] + }, + "sources": { + "propertyOrder": 200, + "options": { + "hidden": true + }, + "title": "Sources", + "description": "The raw sources for this resource.", + "type": "array", + "minItems": 1, + "items": { + "title": "Source", + "description": "A source file.", + "type": "object", + "required": [ + "uri" + ], + "properties": { + "name": { + "title": "Name", + "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated.", + "examples": [ + "{\n \"name\": \"my-nice-name\"\n}\n" + ] + }, + "uri": { + "title": "URI", + "description": "A URI (with some restrictions), being a fully qualified HTTP address, a relative POSIX path, or a JSON Pointer.", + "type": "string", + "format": "uri", + "examples": [ + "{\n \"uri\": \"file.csv\"\n}\n", + "{\n \"uri\": \"#/data/my-data\"\n}\n", + "{\n \"uri\": \"http://example.com/file.csv\"\n}\n" + ], + "context": "Implementations need to negotiate the type of URI provided, and dereference the data accordingly. There are restrictions imposed on URIs that are POSIX paths: see [the notes on descriptors](#descriptor) for more information." + }, + "email": { + "title": "Email", + "description": "An email address.", + "type": "string", + "format": "email", + "examples": [ + "{\n \"email\": \"example@example.com\"\n}\n" + ] + } + } + }, + "examples": [ + "{\n \"sources\": [\n {\n \"name\": \"World Bank and OECD\",\n \"uri\": \"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD\"\n }\n ]\n}\n" + ] + } + } +} \ No newline at end of file diff --git a/tests/DatapackageTest.php b/tests/DatapackageTest.php index 910275d..263e628 100644 --- a/tests/DatapackageTest.php +++ b/tests/DatapackageTest.php @@ -1,9 +1,11 @@ simpleDescriptorArray; $this->assertDatapackageException( "frictionlessdata\\datapackage\\Exceptions\\DatapackageInvalidSourceException", - function() use ($descriptorArray) { new Datapackage($descriptorArray); } + function() use ($descriptorArray) { Factory::datapackage($descriptorArray); } ); } @@ -44,7 +46,7 @@ public function testNativePHPObjectWithoutBasePathShouldFail() $descriptor = $this->simpleDescriptor; $this->assertDatapackageException( "frictionlessdata\\datapackage\\Exceptions\\DataStreamOpenException", - function() use ($descriptor) { new Datapackage($descriptor); } + function() use ($descriptor) { Factory::datapackage($descriptor); } ); } @@ -52,7 +54,7 @@ public function testNativePHPObjectWithBasePath() { $this->assertDatapackage( $this->simpleDescriptor, $this->simpleDescriptorExpectedData, - new Datapackage($this->simpleDescriptor, $this->fixturesPath) + Factory::datapackage($this->simpleDescriptor, $this->fixturesPath) ); } @@ -61,7 +63,7 @@ public function testJsonStringWithoutBasePathShouldFail() $source = json_encode($this->simpleDescriptor); $this->assertDatapackageException( "frictionlessdata\\datapackage\\Exceptions\\DataStreamOpenException", - function() use ($source) { new Datapackage($source); } + function() use ($source) { Factory::datapackage($source); } ); } @@ -70,7 +72,7 @@ public function testJsonStringWithBasePath() $source = json_encode($this->simpleDescriptor); $this->assertDatapackage( $this->simpleDescriptor, $this->simpleDescriptorExpectedData, - new Datapackage($source, $this->fixturesPath) + Factory::datapackage($source, $this->fixturesPath) ); } @@ -78,7 +80,7 @@ public function testNonExistantFileShouldFail() { $this->assertDatapackageException( "frictionlessdata\\datapackage\\Exceptions\\DatapackageInvalidSourceException", - function() { new Datapackage("-invalid-"); } + function() { Factory::datapackage("-invalid-"); } ); } @@ -86,7 +88,7 @@ public function testJsonFileRelativeToBasePath() { $this->assertDatapackage( $this->simpleDescriptor, $this->simpleDescriptorExpectedData, - new Datapackage("simple_valid_datapackage.json", $this->fixturesPath) + Factory::datapackage("simple_valid_datapackage.json", $this->fixturesPath) ); } @@ -94,7 +96,7 @@ public function testJsonFileRelativeToCurrentDirectory() { $this->assertDatapackage( $this->simpleDescriptor, $this->simpleDescriptorExpectedData, - new Datapackage("tests/fixtures/simple_valid_datapackage.json") + Factory::datapackage("tests/fixtures/simple_valid_datapackage.json") ); } @@ -104,17 +106,20 @@ public function testHttpSource() (object)[ "name" => "datapackage-name", "resources" => [ - (object)["name" => "resource-name", "data" => [] ] + (object)[ + "name" => "resource-name", + "data" => ["mock-http://foo.txt", "mock-http://foo.txt"] + ] ] - ], ["resource-name" => []], - new Mocks\MockDatapackage("mock-http://simple_valid_datapackage_no_data.json") + ], ["resource-name" => [["foo"], ["foo"]]], + Mocks\MockFactory::datapackage("mock-http://simple_valid_datapackage_mock_http_data.json") ); } public function testMultiDataDatapackage() { $out = []; - $datapackage = new Datapackage("tests/fixtures/multi_data_datapackage.json"); + $datapackage = Factory::datapackage("tests/fixtures/multi_data_datapackage.json"); foreach ($datapackage as $resource) { $out[] = "-- ".$resource->name()." --"; $i = 0; @@ -155,22 +160,64 @@ public function testMultiDataDatapackage() public function testDatapackageValidation() { - $this->assertEquals([], Datapackage::validate("tests/fixtures/multi_data_datapackage.json")); + $this->assertEquals([], Factory::validate("tests/fixtures/multi_data_datapackage.json")); } public function testDatapackageValidationFailed() { - $this->assertEquals( + $this->assertDatapackageValidation( "[resources] The property resources is required", - DatapackageValidationError::getErrorMessages( - Datapackage::validate("tests/fixtures/simple_invalid_datapackage.json") - ) + "tests/fixtures/simple_invalid_datapackage.json" + ); + } + + public function testDatapackageValidationFailedShouldPreventConstruct() + { + try { + Factory::datapackage((object)["name" => "foobar"]); + $caughtException = null; + } catch (Exceptions\DatapackageValidationFailedException $e) { + $caughtException = $e; + } + $this->assertEquals("DefaultDatapackage validation failed: [resources] The property resources is required", $caughtException->getMessage()); + } + + public function testTabularResourceDescriptorValidation() + { + $this->assertDatapackageValidation( + "resource 1 failed validation: [schema.fields] The property fields is required", + "tests/fixtures/invalid_tabular_resource.json" + ); + } + + public function testDefaultResourceInvalidData() + { + $this->assertDatapackageValidation( + 'resource 1, data stream 2: Failed to open data source "--invalid--": "'.$this->getFopenErrorMessage("--invalid--").'"', + "tests/fixtures/default_resource_invalid_data.json" + ); + } + + public function testTabularResourceInvalidData() + { + $this->assertDatapackageValidation( + 'resource 1, data stream 2: row 2.email(bad.email): invalid value for email format', + "tests/fixtures/tabular_resource_invalid_data.json" + ); + } + + protected function assertDatapackageValidation($expectedMessages, $source, $basePath=null) + { + $validationErrors = Factory::validate($source, $basePath); + $this->assertEquals( + $expectedMessages, + DatapackageValidationError::getErrorMessages($validationErrors) ); } /** * @param object $expectedDescriptor - * @param Datapackage $datapackage + * @param DefaultDatapackage $datapackage */ protected function assertDatapackageDescriptor($expectedDescriptor, $datapackage) { @@ -179,7 +226,7 @@ protected function assertDatapackageDescriptor($expectedDescriptor, $datapackage /** * @param array $expectedData - * @param Datapackage $datapackage + * @param DefaultDatapackage $datapackage */ protected function assertDatapackageData($expectedData, $datapackage) { @@ -217,4 +264,14 @@ protected function assertDatapackageException($expectedExceptionClass, $datapack $this->assertEquals($expectedExceptionClass, get_class($e), $e->getMessage()); } } + + protected function getFopenErrorMessage($in) + { + try { + fopen($in, "r"); + } catch (\Exception $e) { + return $e->getMessage(); + } + throw new \Exception(); + } } diff --git a/tests/Mocks/MockDatapackage.php b/tests/Mocks/MockDatapackage.php deleted file mode 100644 index b5b1740..0000000 --- a/tests/Mocks/MockDatapackage.php +++ /dev/null @@ -1,25 +0,0 @@ -basePath); + } +} \ No newline at end of file diff --git a/tests/Mocks/MockDefaultResource.php b/tests/Mocks/MockDefaultResource.php new file mode 100644 index 0000000..a03adb9 --- /dev/null +++ b/tests/Mocks/MockDefaultResource.php @@ -0,0 +1,24 @@ +assertDatapackageClassProfile( + "frictionlessdata\\datapackage\\Datapackages\\DefaultDatapackage", + "data-package", + (object)[] + ); + } + + public function testDatapackageWithDefaultProfile() + { + $this->assertDatapackageClassProfile( + "frictionlessdata\\datapackage\\Datapackages\\DefaultDatapackage", + "data-package", + (object)["profile" => "default"] + ); + } + + public function testDatapackageWithDataPackageProfile() + { + $this->assertDatapackageClassProfile( + "frictionlessdata\\datapackage\\Datapackages\\DefaultDatapackage", + "data-package", + (object)["profile" => "data-package"] + ); + } + + public function testDatapackageWithTabularDataPackageProfile() + { + $this->assertDatapackageClassProfile( + "frictionlessdata\\datapackage\\Datapackages\\DefaultDatapackage", + "tabular-data-package", + (object)["profile" => "tabular-data-package"] + ); + } + + public function testResourceWithoutProfile() + { + $this->assertResourceClassProfile( + "frictionlessdata\\datapackage\\Resources\\DefaultResource", + "data-resource", + (object)[] + ); + } + + public function testResourceWithDefaultProfile() + { + $this->assertResourceClassProfile( + "frictionlessdata\\datapackage\\Resources\\DefaultResource", + "data-resource", + (object)["profile" => "default"] + ); + } + + public function testResourceWithDataResourceProfile() + { + $this->assertResourceClassProfile( + "frictionlessdata\\datapackage\\Resources\\DefaultResource", + "data-resource", + (object)["profile" => "data-resource"] + ); + } + + public function testResourceWithTabularDataResourceProfile() + { + $this->assertResourceClassProfile( + "frictionlessdata\\datapackage\\Resources\\TabularResource", + "tabular-data-resource", + (object)["profile" => "tabular-data-resource"] + ); + } + + public function testValidate() + { + + } + + protected function assertDatapackageClassProfile($expectedClass, $expectedProfile, $descriptor) + { + $this->assertEquals($expectedClass, Repository::getDatapackageClass($descriptor)); + $this->assertEquals($expectedProfile, Repository::getDatapackageValidationProfile($descriptor)); + } + + protected function assertResourceClassProfile($expectedClass, $expectedProfile, $descriptor) + { + $this->assertEquals($expectedClass, Repository::getResourceClass($descriptor)); + $this->assertEquals($expectedProfile, Repository::getResourceValidationProfile($descriptor)); + } + +} diff --git a/tests/ResourceTest.php b/tests/ResourceTest.php index 767e9db..fec12d1 100644 --- a/tests/ResourceTest.php +++ b/tests/ResourceTest.php @@ -2,6 +2,7 @@ namespace frictionlessdata\datapackage\tests; use PHPUnit\Framework\TestCase; +use frictionlessdata\datapackage\Resource; class ResourceTest extends TestCase { @@ -9,7 +10,7 @@ public function testHttpDataSourceShouldNotGetBasePath() { $this->assertResourceData( [["foo"],["foo"]], - new Mocks\MockResource( + Mocks\MockFactory::resource( (object)[ "name" => "resource-name", "data" => [ diff --git a/tests/fixtures/default_resource_invalid_data.json b/tests/fixtures/default_resource_invalid_data.json new file mode 100644 index 0000000..89a23b6 --- /dev/null +++ b/tests/fixtures/default_resource_invalid_data.json @@ -0,0 +1,9 @@ +{ + "name": "default-resource-invalid-data", + "resources": [ + { + "name": "default-resource-invalid-data", + "data": ["foo.txt", "--invalid--"] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/invalid_tabular_data.csv b/tests/fixtures/invalid_tabular_data.csv new file mode 100644 index 0000000..bdad169 --- /dev/null +++ b/tests/fixtures/invalid_tabular_data.csv @@ -0,0 +1,3 @@ +id,email +1,"good@email.and.nice" +2,"bad.email" \ No newline at end of file diff --git a/tests/fixtures/invalid_tabular_resource.json b/tests/fixtures/invalid_tabular_resource.json new file mode 100644 index 0000000..a61384d --- /dev/null +++ b/tests/fixtures/invalid_tabular_resource.json @@ -0,0 +1,13 @@ +{ + "name": "invalid-tabular-resource", + "resources": [ + { + "profile": "tabular-data-resource", + "name": "invalid-tabular-resource", + "data": ["simple_tabular_data2.csv"], + "schema": { + "foo": "bar" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/simple_tabular_data.csv b/tests/fixtures/simple_tabular_data.csv new file mode 100644 index 0000000..092f2a9 --- /dev/null +++ b/tests/fixtures/simple_tabular_data.csv @@ -0,0 +1,4 @@ +id,name +1,"one" +2,"two" +3,"three" \ No newline at end of file diff --git a/tests/fixtures/simple_valid_datapackage_mock_http_data.json b/tests/fixtures/simple_valid_datapackage_mock_http_data.json new file mode 100644 index 0000000..e3893cf --- /dev/null +++ b/tests/fixtures/simple_valid_datapackage_mock_http_data.json @@ -0,0 +1,6 @@ +{ + "name": "datapackage-name", + "resources": [ + { "name": "resource-name", "data": ["mock-http://foo.txt", "mock-http://foo.txt"] } + ] +} \ No newline at end of file diff --git a/tests/fixtures/simple_valid_datapackage_no_data.json b/tests/fixtures/simple_valid_datapackage_no_data.json deleted file mode 100644 index 2c71d24..0000000 --- a/tests/fixtures/simple_valid_datapackage_no_data.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "datapackage-name", - "resources": [ - { "name": "resource-name", "data": [] } - ] -} \ No newline at end of file diff --git a/tests/fixtures/tabular_resource_invalid_data.json b/tests/fixtures/tabular_resource_invalid_data.json new file mode 100644 index 0000000..6196649 --- /dev/null +++ b/tests/fixtures/tabular_resource_invalid_data.json @@ -0,0 +1,16 @@ +{ + "name": "tabular-resource-invalid-data", + "resources": [ + { + "profile": "tabular-data-resource", + "name": "tabular-resource-invalid-data", + "data": ["valid_emails_tabular_data.csv", "invalid_tabular_data.csv"], + "schema": { + "fields": [ + {"name": "id", "type": "integer"}, + {"name": "email", "type": "string", "format": "email"} + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/valid_emails_tabular_data.csv b/tests/fixtures/valid_emails_tabular_data.csv new file mode 100644 index 0000000..5455ee0 --- /dev/null +++ b/tests/fixtures/valid_emails_tabular_data.csv @@ -0,0 +1,3 @@ +id,email +1,"good@email.and.nice" +2,"another.good@email.com" \ No newline at end of file