Skip to content

Commit

Permalink
Improve GeoJSON validation
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
Martin456 authored and nicolaiarocci committed May 3, 2017
1 parent bc753b0 commit 71e38e4
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 3 deletions.
2 changes: 2 additions & 0 deletions eve/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions eve/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 29 additions & 2 deletions eve/io/mongo/geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

Expand Down Expand Up @@ -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]])
25 changes: 24 additions & 1 deletion eve/io/mongo/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
62 changes: 62 additions & 0 deletions eve/tests/io/mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]}}
Expand Down Expand Up @@ -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'},
Expand Down

0 comments on commit 71e38e4

Please sign in to comment.