Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve GeoJSON validation #1004

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1727,8 +1727,15 @@ encoded in GeoJSON_ format. All GeoJSON objects supported by MongoDB_ are availa
- ``MultiPolygon``
- ``GeometryCollection``

These are implemented as native Eve data types (see :ref:`schema`) so they are
are subject to proper validation.
Eve supports also GeoJSON object Feature and FeatureCollection that are not
explicitely mentioned in MongoDB_ documentation. All these objects are
implemented as native Eve data types (see :ref:`schema`) so they are
are subject to the proper validation.

GeoJSON specification allows object to contain any number of members (name/value
pairs). Eve validation was implemented to be more strict, allowing only two
members. This restriction can be disabled by setting config variable
ALLOW_CUSTOM_FIELDS_IN_GEOJSON to True.

In the example below we are extending the `people` endpoint by adding
a ``location`` field is of type Point_.
Expand Down
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
1 change: 1 addition & 0 deletions src/al
Submodule al added at 15d190