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],