From 4c5c4c01224327e4de30f87d0755fa62baa20efa Mon Sep 17 00:00:00 2001 From: Ori Hoch <ori@uumpa.com> Date: Thu, 13 Jul 2017 15:43:41 +0300 Subject: [PATCH] minor fixes for compliancy --- README.md | 127 ++++++++++++++++++++++------------- src/Fields/BaseField.php | 15 +++++ src/Fields/FieldsFactory.php | 2 +- src/Fields/GeojsonField.php | 4 ++ src/Fields/GeopointField.php | 40 +++++------ tests/FieldTest.php | 17 +++++ tests/FieldTypesTest.php | 22 +++--- 7 files changed, 149 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 2d703fe..ddc65a1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,58 @@ A utility library for working with [Table Schema](https://specs.frictionlessdata $ composer require frictionlessdata/tableschema ``` +### Table + +Table class allows to iterate over data conforming to a table schema + +Instantiate a Table object based on a data source and a table schema. + +```php +use frictionlessdata\tableschema\Table; + +$table = new Table("tests/fixtures/data.csv", ["fields" => [ + ["name" => "first_name"], + ["name" => "last_name"], + ["name" => "order"] +]]); +``` + +Schema can be any parameter valid for the Schema object (See below), so you can use a url or filename which contains the schema + +```php +$table = new Table("tests/fixtures/data.csv", "tests/fixtures/data.json"); +``` + +iterate over the data, all the values are cast and validated according to the schema + +```php +foreach ($table as $row) { + print($row["order"]." ".$row["first_name"]." ".$row["last_name"]."\n"); +}; +``` + +validate function will validate the schema and get some sample of the data itself to validate it as well + +```php +Table::validate(new CsvDataSource("http://invalid.data.source/"), $schema); +``` + +You can instantiate a table object without schema, in this case the schema will be inferred automatically based on the data + +```php +$table = new Table("tests/fixtures/data.csv"); +$table->schema()->fields(); // ["first_name" => StringField, "last_name" => StringField, "order" => IntegerField] +``` + +Additional methods and functionality + +```php +$table->headers() // ["first_name", "last_name", "order"] +$table->save("output.csv") // iterate over all the rows and save the to a csv file +$table->schema() // get the Schema object +$table->read() // returns all the data as an array +``` + ### Schema Schema class provides helpful methods for working with a table schema and related data. @@ -26,7 +78,7 @@ Schema class provides helpful methods for working with a table schema and relate Schema objects can be constructed using any of the following: -* php array +* php array (or object) ```php $schema = new Schema([ 'fields' => [ @@ -68,14 +120,7 @@ $schema->missingValues(); // [""] $schema->primaryKey(); // ["id"] $schema->foreignKeys(); // [] $schema->fields(); // ["id" => IntegerField, "name" => StringField] -$field = $schema->field("id"); -$field("id")->format(); // "default" -$field("id")->name(); // "id" -$field("id")->type(); // "integer" -$field("id")->constraints(); // (object)["required"=>true, "minimum"=>1, "maximum"=>500] -$field("id")->enum(); // [] -$field("id")->required(); // true -$field("id")->unique(); // false +$field = $schema->field("id"); // Field object (See Field reference below) ``` validate function accepts the same arguemnts as the Schema constructor but returns a list of errors instead of raising exceptions @@ -123,7 +168,7 @@ $schema->fields([ ]); ``` -appropriate field object is created according to the given descriptor +appropriate Field object is created according to the given descriptor (see below for Field class reference) ```php $schema->field("id"); // IntegerField object @@ -145,62 +190,52 @@ $schema->primaryKey(["id"]); after every change - schema is validated and will raise Exception in case of validation errors -finally, save the schema to a json file +Finally, you can get the full validated descriptor ```php -$schema->save("my-schema.json"); +$schema->fullDescriptor(); ``` -### Table - -Table class allows to iterate over data conforming to a table schema - -Instantiate a Table object based on a data source and a table schema. +And, save it to a json file ```php -use frictionlessdata\tableschema\Table; - -$table = new Table("tests/fixtures/data.csv", ["fields" => [ - ["name" => "first_name"], - ["name" => "last_name"], - ["name" => "order"] -]]); +$schema->save("my-schema.json"); ``` -Schema can be any parameter valid for the Schema object, so you can use a url or filename which contains the schema - -```php -$table = new Table("tests/fixtures/data.csv", "tests/fixtures/data.json"); -``` +### Field -iterate over the data, all the values are cast and validated according to the schema +Field class represents a single table schema field descriptor -```php -foreach ($table as $row) { - print($row["order"]." ".$row["first_name"]." ".$row["last_name"]."\n"); -}; -``` +Create a field from a descriptor -validate function will validate the schema and get some sample of the data itself to validate it as well - ```php -Table::validate(new CsvDataSource("http://invalid.data.source/"), $schema); +use frictionlessdata\tableschema\Fields\FieldsFactory; +$field = FieldsFactory::field([ + "name" => "id", "type" => "integer", + "constraints" => ["required" => true, "minimum" => 5] +]); ``` -You can instantiate a table object without schema, in this case the schema will be inferred automatically based on the data +Cast and validate values using the field ```php -$table = new Table("tests/fixtures/data.csv"); -$table->schema()->fields(); // ["first_name" => StringField, "last_name" => StringField, "order" => IntegerField] +$field->castValue("3"); // exception: value is below minimum +$field->castValue("7"); // 7 ``` -Additional methods and functionality +Additional method to access field data ```php -$table->headers() // ["first_name", "last_name", "order"] -$table->save("output.csv") // iterate over all the rows and save the to a csv file -$table->schema() // get the Schema object -$table->read() // returns all the data as an array +$field("id")->format(); // "default" +$field("id")->name(); // "id" +$field("id")->type(); // "integer" +$field("id")->constraints(); // (object)["required"=>true, "minimum"=>1, "maximum"=>500] +$field("id")->enum(); // [] +$field("id")->required(); // true +$field("id")->unique(); // false +$field("id")->title(); // "Id" (or null if not provided in descriptor) +$field("id")->description(); // "The ID" (or null if not provided in descriptor) +$field("id")->rdfType(); // "http://schema.org/Thing" (or null if not provided in descriptor) ``` ## Important Notes diff --git a/src/Fields/BaseField.php b/src/Fields/BaseField.php index 286dc87..6a77317 100644 --- a/src/Fields/BaseField.php +++ b/src/Fields/BaseField.php @@ -31,6 +31,21 @@ public function name() return $this->descriptor()->name; } + public function title() + { + return isset($this->descriptor()->title) ? $this->descriptor()->title : null; + } + + public function description() + { + return isset($this->descriptor()->description) ? $this->descriptor()->description : null; + } + + public function rdfType() + { + return isset($this->descriptor()->rdfType) ? $this->descriptor()->rdfType : null; + } + public function format() { return isset($this->descriptor()->format) ? $this->descriptor()->format : 'default'; diff --git a/src/Fields/FieldsFactory.php b/src/Fields/FieldsFactory.php index 4ef863e..9142a98 100644 --- a/src/Fields/FieldsFactory.php +++ b/src/Fields/FieldsFactory.php @@ -38,7 +38,7 @@ class FieldsFactory /** * get a new field object in the correct type according to the descriptor. * - * @param object $descriptor + * @param object|array $descriptor * * @return BaseField * diff --git a/src/Fields/GeojsonField.php b/src/Fields/GeojsonField.php index 8365123..d1de10a 100644 --- a/src/Fields/GeojsonField.php +++ b/src/Fields/GeojsonField.php @@ -12,7 +12,11 @@ protected function validateCastValue($val) } catch (\Exception $e) { throw $this->getValidationException($e->getMessage(), $val); } + if (!$val) { + throw $this->getValidationException('failed to decode json', $val); + } } + $val = json_decode(json_encode($val)); if (!is_object($val)) { throw $this->getValidationException('must be an object', $val); } diff --git a/src/Fields/GeopointField.php b/src/Fields/GeopointField.php index d9d8812..6c24363 100644 --- a/src/Fields/GeopointField.php +++ b/src/Fields/GeopointField.php @@ -18,21 +18,25 @@ protected function validateCastValue($val) if (!is_string($val)) { throw $this->getValidationException('value must be a string', $val); } else { - return $this->getNativeGeopoint(explode(',', $val)); + $val = explode(',', $val); + if (count($val) != 2) { + throw $this->getValidationException('value must be a string with 2 comma-separated elements', $val); + } else { + return $this->getNativeGeopoint($val); + } } case 'array': - if (!is_array($val)) { - throw $this->getValidationException('value must be an array', $val); + if (!is_array($val) || array_keys($val) != [0, 1]) { + throw $this->getValidationException('value must be an array with 2 elements', $val); } else { return $this->getNativeGeopoint($val); } case 'object': - if (!is_object($val)) { - throw $this->getValidationException('value must be an object', $val); - } elseif (!isset($val->lat) || !isset($val->lon)) { + $val = json_decode(json_encode($val), true); + if (!is_array($val) || !array_key_exists('lat', $val) || !array_key_exists('lon', $val)) { throw $this->getValidationException('object must contain lon and lat attributes', $val); } else { - return $this->getNativeGeopoint([$val->lon, $val->lat]); + return $this->getNativeGeopoint([$val['lon'], $val['lat']]); } default: throw $this->getValidationException('invalid format', $val); @@ -46,20 +50,16 @@ public static function type() protected function getNativeGeopoint($arr) { - if (count($arr) != 2) { - throw $this->getValidationException('lon,lat array must contain only lon,lat', json_encode($arr)); + list($lon, $lat) = $arr; + $lon = (int) $lon; + $lat = (int) $lat; + if ( + $lon > 180 || $lon < -180 + || $lat > 90 or $lat < -90 + ) { + throw $this->getValidationException('invalid lon,lat values', json_encode($arr)); } else { - list($lon, $lat) = $arr; - $lon = (int) $lon; - $lat = (int) $lat; - if ( - $lon > 180 || $lon < -180 - || $lat > 90 or $lat < -90 - ) { - throw $this->getValidationException('invalid lon,lat values', json_encode($arr)); - } else { - return [$lon, $lat]; - } + return [$lon, $lat]; } } } diff --git a/tests/FieldTest.php b/tests/FieldTest.php index 4bb22a4..8a65b20 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -86,6 +86,23 @@ public function testCastValue() $this->assertEquals(1, FieldsFactory::field($this->DESCRIPTOR_MAX)->castValue('1')); } + public function testAdditionalMethods() + { + $field = FieldsFactory::field(['name' => 'name', 'type' => 'string']); + $this->assertEquals(null, $field->title()); + $this->assertEquals(null, $field->description()); + $this->assertEquals(null, $field->rdfType()); + $field = FieldsFactory::field([ + 'name' => 'name', 'type' => 'string', + 'title' => 'Title', + 'description' => 'Description', + 'rdfType' => 'http://schema.org/Thing', + ]); + $this->assertEquals('Title', $field->title()); + $this->assertEquals('Description', $field->description()); + $this->assertEquals('http://schema.org/Thing', $field->rdfType()); + } + public function testCastValueConstraintError() { try { diff --git a/tests/FieldTypesTest.php b/tests/FieldTypesTest.php index 877be04..58ce8da 100644 --- a/tests/FieldTypesTest.php +++ b/tests/FieldTypesTest.php @@ -227,17 +227,17 @@ public function testGeojson() [ 'default', ['properties' => ['Ã' => 'Ã'], 'type' => 'Feature', 'geometry' => null], - ['properties' => ['Ã' => 'Ã'], 'type' => 'Feature', 'geometry' => null], + (object) ['properties' => (object) ['Ã' => 'Ã'], 'type' => 'Feature', 'geometry' => null], ], [ 'default', '{"geometry": null, "type": "Feature", "properties": {"\\u00c3": "\\u00c3"}}', - ['properties' => ['Ã' => 'Ã'], 'type' => 'Feature', 'geometry' => null], + (object) ['properties' => (object) ['Ã' => 'Ã'], 'type' => 'Feature', 'geometry' => null], ], [ 'default', ['coordinates' => [0, 0, 0], 'type' => 'Point'], - ['coordinates' => [0, 0, 0], 'type' => 'Point'], + (object) ['coordinates' => [0, 0, 0], 'type' => 'Point'], ], ['default', 'string', self::ERROR], ['default', 1, self::ERROR], @@ -248,12 +248,12 @@ public function testGeojson() [ 'topojson', ['type' => 'LineString', 'arcs' => [42]], - ['type' => 'LineString', 'arcs' => [42]], + (object) ['type' => 'LineString', 'arcs' => [42]], ], [ 'topojson', '{"type": "LineString", "arcs": [42]}', - ['type' => 'LineString', 'arcs' => [42]], + (object) ['type' => 'LineString', 'arcs' => [42]], ], ['topojson', 'string', self::ERROR], ['topojson', 1, self::ERROR], @@ -264,7 +264,7 @@ public function testGeojson() // enum [[ 'format' => 'default', 'constraints' => ['enum' => ['{"geometry": null, "type": "Feature", "properties": {"\\u00c3": "\\u00c3"}}']], - ], '{"geometry": null, "type": "Feature", "properties": {"\\u00c3": "\\u00c3"}}', ['properties' => ['Ã' => 'Ã'], 'type' => 'Feature', 'geometry' => null]], + ], '{"geometry": null, "type": "Feature", "properties": {"\\u00c3": "\\u00c3"}}', (object) ['properties' => (object) ['Ã' => 'Ã'], 'type' => 'Feature', 'geometry' => null]], [[ 'format' => 'default', 'constraints' => ['enum' => ['{"geometry": null, "type": "Feature", "properties": {"\\u00c3": "\\u00c3"}}']], ], '{"geometry": null, "type": "Feature", "properties": {"\\u00c3": "\\u00c4"}}', self::ERROR], @@ -387,10 +387,10 @@ public function testNumber() public function testObject() { $this->assertFieldTestData('object', [ - ['default', [], []], - ['default', '{}', []], - ['default', ['key' => 'value'], ['key' => 'value']], - ['default', '{"key": "value"}', ['key' => 'value']], + ['default', (object) [], (object) []], + ['default', '{}', (object) []], + ['default', (object) ['key' => 'value'], (object) ['key' => 'value']], + ['default', '{"key": "value"}', (object) ['key' => 'value']], ['default', '["key", "value"]', self::ERROR], ['default', 'string', self::ERROR], ['default', 1, self::ERROR], @@ -399,7 +399,7 @@ public function testObject() // required [['format' => 'default', 'constraints' => ['required' => true]], null, self::ERROR], // enum - [['format' => 'default', 'constraints' => ['enum' => ['{"foo":"bar"}']]], '{"foo":"bar"}', ['foo' => 'bar']], + [['format' => 'default', 'constraints' => ['enum' => ['{"foo":"bar"}']]], '{"foo":"bar"}', (object) ['foo' => 'bar']], [['format' => 'default', 'constraints' => ['enum' => ['{"foo":"bar"}']]], '{"foox":"bar"}', self::ERROR], // minLength / maxLength [['format' => 'default', 'constraints' => ['minLength' => 1]], '{}', self::ERROR],