From b144f7df38392c66ed01cc3affaec3730a510501 Mon Sep 17 00:00:00 2001 From: Martin Fous Date: Mon, 27 Mar 2017 00:08:35 +0200 Subject: [PATCH] Improve GeoJSON validation - New config variable to allow custom fields in GeoJSON (Issue #769) - Validation if coordinates contain at least two values - Support for Feature and FeatureCollection structures --- eve/__init__.py | 2 ++ eve/default_settings.py | 4 +++ eve/io/mongo/geo.py | 31 +++++++++++++++++-- eve/io/mongo/validation.py | 25 ++++++++++++++- eve/tests/io/mongo.py | 62 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index 767f3cfeb..a78ae6f31 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -53,6 +53,8 @@ CACHE_CONTROL = 'max-age=10,must-revalidate' # TODO confirm this value CACHE_EXPIRES = 10 +ALLOW_CUSTOM_FIELDS_IN_GEOJSON = False + RESOURCE_METHODS = ['GET'] ITEM_METHODS = ['GET'] ITEM_LOOKUP = True diff --git a/eve/default_settings.py b/eve/default_settings.py index 6da59d429..5f633e264 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -234,6 +234,10 @@ # don't allow unknown key/value pairs for POST/PATCH payloads. ALLOW_UNKNOWN = False +# GeoJSON specs allows any number of key/value pairs +# http://geojson.org/geojson-spec.html#geojson-objects +ALLOW_CUSTOM_FIELDS_IN_GEOJSON = False + # don't ignore unknown schema rules (raise SchemaError) TRANSPARENT_SCHEMA_RULES = False diff --git a/eve/io/mongo/geo.py b/eve/io/mongo/geo.py index af4d40429..a383b3fb9 100644 --- a/eve/io/mongo/geo.py +++ b/eve/io/mongo/geo.py @@ -9,6 +9,7 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +from eve.utils import config class GeoJSON(dict): @@ -18,11 +19,13 @@ def __init__(self, json): except KeyError: raise TypeError("Not compliant to GeoJSON") self.update(json) - if len(self.keys()) != 2: + if not config.ALLOW_CUSTOM_FIELDS_IN_GEOJSON and \ + len(self.keys()) != 2: raise TypeError("Not compliant to GeoJSON") def _correct_position(self, position): return isinstance(position, list) and \ + len(position) > 1 and \ all(isinstance(pos, int) or isinstance(pos, float) for pos in position) @@ -102,7 +105,31 @@ def __init__(self, json): raise TypeError +class Feature(GeoJSON): + def __init__(self, json): + super(Feature, self).__init__(json) + try: + geometry = self["geometry"] + factory = factories[geometry["type"]] + factory(geometry) + + except (KeyError, TypeError, AttributeError): + raise TypeError("Feature not compliant to GeoJSON") + + +class FeatureCollection(GeoJSON): + def __init__(self, json): + super(FeatureCollection, self).__init__(json) + try: + if not isinstance(self["features"], list): + raise TypeError + for feature in self["features"]: + Feature(feature) + except (KeyError, TypeError, AttributeError): + raise TypeError("FeatureCollection not compliant to GeoJSON") + + factories = dict([(_type.__name__, _type) for _type in [GeometryCollection, Point, MultiPoint, LineString, - MultiLineString, Polygon, MultiPolygon]]) + MultiLineString, Polygon, MultiPolygon]]) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index bb35f06d1..469ef808a 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -21,7 +21,8 @@ from eve.auth import auth_field_and_value from eve.io.mongo.geo import Point, MultiPoint, LineString, Polygon, \ - MultiLineString, MultiPolygon, GeometryCollection + MultiLineString, MultiPolygon, GeometryCollection, Feature, \ + FeatureCollection from eve.utils import config, str_type from eve.versioning import get_data_version_relation_document @@ -443,6 +444,28 @@ def _validate_type_geometrycollection(self, field, value): except TypeError: self._error(field, "GeometryCollection not correct" % value) + def _validate_type_feature(self, field, value): + """ Enables validation for `feature`data type + + :param field: field name. + :param value: field nvalue + """ + try: + Feature(value) + except TypeError: + self._error(field, "Feature not correct" % value) + + def _validate_type_featurecollection(self, field, value): + """ Enables validation for `featurecollection`data type + + :param field: field name. + :param value: field nvalue + """ + try: + FeatureCollection(value) + except TypeError: + self._error(field, "FeatureCollection not correct" % value) + def _error(self, field, _error): """ Change the default behaviour so that, if VALIDATION_ERROR_AS_LIST is enabled, single validation errors are returned as a list. See #536. diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index c5e17cead..378322f1f 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -180,6 +180,14 @@ def test_point_fail(self): self.assertTrue('location' in v.errors) self.assertTrue('Point' in v.errors['location']) + def test_point_coordinates_fail(self): + schema = {'location': {'type': 'point'}} + doc = {'location': {'type': "Point", 'coordinates': [123.0]}} + v = Validator(schema) + self.assertFalse(v.validate(doc)) + self.assertTrue('location' in v.errors) + self.assertTrue('Point' in v.errors['location']) + def test_point_integer_success(self): schema = {'location': {'type': 'point'}} doc = {'location': {'type': "Point", 'coordinates': [10, 123.0]}} @@ -290,6 +298,60 @@ def test_geometrycollection_fail(self): self.assertTrue('locations' in v.errors) self.assertTrue('GeometryCollection' in v.errors['locations']) + def test_feature_success(self): + schema = {'locations': {'type': 'feature'}} + doc = {"locations": {"type": "Feature", + "geometry": {"type": "Polygon", + "coordinates": [[[100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0]]]} + } + } + v = Validator(schema) + self.assertTrue(v.validate(doc)) + + def test_feature_fail(self): + schema = {'locations': {'type': 'feature'}} + doc = {"locations": {"type": "Feature", + "geometries": [{"type": "Polygon", + "coordinates": [[[100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 0.0]]]}] + } + } + v = Validator(schema) + self.assertFalse(v.validate(doc)) + self.assertTrue('locations' in v.errors) + self.assertTrue('Feature' in v.errors['locations']) + + def test_featurecollection_success(self): + schema = {'locations': {'type': 'featurecollection'}} + doc = {"locations": {"type": "FeatureCollection", + "features": [ + {"type": "Feature", + "geometry": {"type": "Point", + "coordinates": [102.0, 0.5]} + }] + } + } + v = Validator(schema) + self.assertTrue(v.validate(doc)) + + def test_featurecollection_fail(self): + schema = {'locations': {'type': 'featurecollection'}} + doc = {"locations": {"type": "FeatureCollection", + "geometry": {"type": "Point", + "coordinates": [100.0, 0.0]} + } + } + v = Validator(schema) + self.assertFalse(v.validate(doc)) + self.assertTrue('locations' in v.errors) + self.assertTrue('FeatureCollection' in v.errors['locations']) + def test_dependencies_with_defaults(self): schema = { 'test_field': {'dependencies': 'foo'},