Skip to content

Commit

Permalink
Merge branch 'improve_geojson_validation_#1004' into v0.8
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolaiarocci committed Apr 6, 2017
2 parents 1e4343c + b216a02 commit ca724e6
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 6 deletions.
9 changes: 8 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ Here you can see the full list of changes between each Eve release.
Development
-----------

Version 0.8
~~~~~~~~~~~
- New: ``ALLOW_CUSTOM_FIELDS_IN_GEOJSON`` allows custom fields in GeoJSON
(Martin Fous).
- New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects.
Closes #769 (Martin Fous).

Version 0.7.3
~~~~~~~~~~~~~
- Dev: use official Alabaster theme instead of custom fork.
- Dev: Use official Alabaster theme instead of custom fork.
- Fix: docstrings typos (Martin Fous).
- Docs: explain that ``ALLOW_UNKNOWN`` can also be used to expose
the whole document as found in the database, with no explicit validation
Expand Down
11 changes: 9 additions & 2 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1727,8 +1727,8 @@ 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.
All these objects are implemented as native Eve data types (see :ref:`schema`)
so they are are subject to the proper validation.
In the example below we are extending the `people` endpoint by adding
a ``location`` field is of type Point_.
Expand All @@ -1750,6 +1750,13 @@ Storing a contact along with its location is pretty straightforward:
$ curl -d '[{"firstname": "barack", "lastname": "obama", "location": {"type":"Point","coordinates":[100.0,10.0]}}]' -H 'Content-Type: application/json' http://127.0.0.1:5000/people
HTTP/1.1 201 OK
Eve also supports GeoJSON ``Feature`` and ``FeatureCollection`` objects, which
are not explicitely mentioned in MongoDB_ documentation. 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 ``ALLOW_CUSTOM_FIELDS_IN_GEOJSON`` to
``True``.
Querying GeoJSON Data
~~~~~~~~~~~~~~~~~~~~~
As a general rule all MongoDB `geospatial query operators`_ and their associated
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

0 comments on commit ca724e6

Please sign in to comment.