From 535cfa75644726c8921932fe7ea528cc8715cc2d Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Mon, 3 Aug 2026 22:09:00 -0400 Subject: [PATCH] add support for provider level validation --- docs/source/configuration.rst | 2 + docs/source/cql2.rst | 2 +- docs/source/plugins.rst | 38 +- docs/source/transactions.rst | 28 +- pygeoapi/api/itemtypes.py | 22 + pygeoapi/plugin.py | 3 + pygeoapi/provider/base.py | 3 +- .../schemas/config/pygeoapi-config-0.x.yml | 11 +- .../resources/schemas/geojson/Feature.json | 505 ++++++++++++++++++ pygeoapi/validator/__init__.py | 28 + pygeoapi/validator/base.py | 81 +++ pygeoapi/validator/geojson.py | 92 ++++ tests/provider/test_base_provider.py | 2 +- tests/validator/__init__.py | 28 + tests/validator/test_geojson_validator.py | 95 ++++ 15 files changed, 933 insertions(+), 7 deletions(-) create mode 100644 pygeoapi/resources/schemas/geojson/Feature.json create mode 100644 pygeoapi/validator/__init__.py create mode 100644 pygeoapi/validator/base.py create mode 100644 pygeoapi/validator/geojson.py create mode 100644 tests/validator/__init__.py create mode 100644 tests/validator/test_geojson_validator.py diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index cc3798c09..933689808 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -290,6 +290,8 @@ default. - name: path.to.formatter # Python path of formatter definition attachment: true # whether or not to provide as an attachment or normal response geom: false # whether or not to include geometry + validator: + name: path.to.validator # Python path of validation definition hello-world: # name of process type: process # REQUIRED (collection, process, or stac-collection) diff --git a/docs/source/cql2.rst b/docs/source/cql2.rst index f1ceabdb5..4c97424ac 100644 --- a/docs/source/cql2.rst +++ b/docs/source/cql2.rst @@ -33,7 +33,7 @@ Queries The PostgreSQL provider uses `pygeofilter `_ allowing a range of filter expressions, see examples for: -* `Comparison predicates (`Advanced `_, `Case-insensitive `_) +* Comparison predicates (`Advanced `_, `Case-insensitive `_) * `Spatial predicates `_ * `Temporal predicates `_ diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 29977991c..e73acc64c 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -464,7 +464,7 @@ Below is a sample process definition as a Python dictionary: .. note:: - Additional processing plugins can also be found in ``pygeoapi/process``. + Additional processing plugins can be found in ``pygeoapi/process``. .. _example-custom-pygeoapi-formatter: @@ -503,6 +503,42 @@ The below template provides a minimal example (let's call the file ``mycooljsonf return out_data +Example: custom pygeoapi validator +---------------------------------- + +Python code +^^^^^^^^^^^ + +The below template provides a minimal example (let's call the file ``mycooldatavalidator.py``: + +.. code-block:: python + + from typing import Any + + from pygeoapi.validator.base import BaseValidator, ValidatorValidationError + + class MyCoolDataValidator(BaseValidator): + def __init__(self, validator_def): + """Inherit from parent class""" + + super().__init__(validator_def) + + def validate(self, data: Any, partial: bool = False) -> None: + if partial: # plugin does not support partial updates to a given item (PATCH) + msg = 'Partial validation not supported' + raise ValidatorValidationError(msg) + + # data is a dict of incoming data, validate accordingly + if 'some_property' not in data: + msg = 'Invalid data payload!' # to add more detailed messaging, pass user_msg="string of text" to ValidatorValidationError + raise ValidatorValidationError(msg) + + def __repr__(self): + return '' + +.. note:: + + Additional validator plugins can be found in ``pygeoapi/validator``. Featured plugins ---------------- diff --git a/docs/source/transactions.rst b/docs/source/transactions.rst index 4c6327174..ea577bc2a 100644 --- a/docs/source/transactions.rst +++ b/docs/source/transactions.rst @@ -7,8 +7,8 @@ pygeoapi supports the `OGC API - Features - Part 4: Create, Replace, Update and for transactional capabilities against feature and record data. To enable transactions in pygeoapi, a given resource provider needs to be editable (via the configuration resource provider -``editable: true`` property). Note that the feature or record provider MUST support create/update/delete. See the -:ref:`ogcapi-features` and :ref:`ogcapi-records` documentation for transaction support status of pygeoapi backends. +``editable: true`` property). Note that the feature or record provider MUST support create/update/delete. See +:ref:`ogcapi-features` and :ref:`ogcapi-records` for transaction support status of pygeoapi backends. Access control ^^^^^^^^^^^^^^ @@ -17,3 +17,27 @@ It should be made clear that authentication and authorization is beyond the resp if a pygeoapi user enables transactions, they must provide access control explicitly via another service. .. _`OGC API - Features - Part 4: Create, Replace, Update and Delete`: https://docs.ogc.org/DRAFTS/20-002.html + +Validation +^^^^^^^^^^ + +pygeoapi transaction support includes the option to implement custom validation when adding or updating features or records. + +To enable validation in transactions in pygeoapi, a given resource provider can specify a custom validator plugin to implement +custom business rules as needed to ensure data is valid prior to adding or updating a given provider backend. + +Given the example below: + +.. code-block:: yaml + + providers: + - type: feature + name: Elasticsearch + data /path/to/file + id_field: stn_id + editable: true + validator: + name: mycooldatapackage.mycooldatavalidator.MyCoolDataValidator + +The ``validator`` element refers to a Python module/class that implements a pygeoapi validator plugin. See :ref:`plugins` +for more information on implementing validator plugins. diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index 65c93aa0a..233f8d84a 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -797,6 +797,28 @@ def manage_collection_item( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) + if action in ['create', 'update']: + if p.validator is not None: + LOGGER.debug('Provider is configured for validation') + LOGGER.debug('Loading validator') + try: + v = load_plugin('validator', {'name': p.validator['name']}) + except Exception: + msg = 'Invalid validator configured' + return api.get_exception( + HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, + 'NoApplicableCode', msg) + + LOGGER.debug('Validating item') + try: + v.validate(request.data) + except Exception as err: + msg = err.user_msg or 'Item is not valid, please check and validate payload' # noqa + LOGGER.error(f'Validation errors: {err.message}') + return api.get_exception( + HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, + 'InvalidParameterValue', msg) + if action == 'create': LOGGER.debug('Creating item') try: diff --git a/pygeoapi/plugin.py b/pygeoapi/plugin.py index 32292c895..a487373f3 100644 --- a/pygeoapi/plugin.py +++ b/pygeoapi/plugin.py @@ -90,6 +90,9 @@ 'HTTP': 'pygeoapi.pubsub.http.HTTPPubSubClient', 'Kafka': 'pygeoapi.pubsub.kafka.KafkaPubSubClient', 'MQTT': 'pygeoapi.pubsub.mqtt.MQTTPubSubClient' + }, + 'validator': { + 'GeoJSON': 'pygeoapi.validator.geojson.GeoJSONValidator' } } diff --git a/pygeoapi/provider/base.py b/pygeoapi/provider/base.py index 00729c808..2a0afdee9 100644 --- a/pygeoapi/provider/base.py +++ b/pygeoapi/provider/base.py @@ -81,6 +81,7 @@ def __init__(self, provider_def): self.include_extra_query_parameters = provider_def.get('include_extra_query_parameters', False) # noqa self._fields = {} self.filename = None + self.validator = provider_def.get('validator') # CRS properties storage_crs_uri = provider_def.get('storage_crs', DEFAULT_STORAGE_CRS) @@ -337,7 +338,7 @@ class ProviderTypeError(ProviderGenericError): class ProviderInvalidQueryError(ProviderGenericError): """provider invalid query error""" - ogc_exception_code = 'InvalidQuery' + ogc_exception_code = 'InvalidParameterValue' http_status_code = HTTPStatus.BAD_REQUEST default_msg = "query error" diff --git a/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml b/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml index 73190d22c..48453c1f2 100644 --- a/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml +++ b/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml @@ -660,6 +660,15 @@ properties: description: whether to provide as an attachment required: - name + validator: + type: object + description: custom validator to apply on transactions + properties: + name: + type: string + description: name of validator + required: + - name required: - type - title @@ -753,4 +762,4 @@ required: - server - logging - metadata - - resources \ No newline at end of file + - resources diff --git a/pygeoapi/resources/schemas/geojson/Feature.json b/pygeoapi/resources/schemas/geojson/Feature.json new file mode 100644 index 000000000..30151f53a --- /dev/null +++ b/pygeoapi/resources/schemas/geojson/Feature.json @@ -0,0 +1,505 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://geojson.org/schema/Feature.json", + "title": "GeoJSON Feature", + "type": "object", + "required": [ + "type", + "properties", + "geometry" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Feature" + ] + }, + "id": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "properties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object" + } + ] + }, + "geometry": { + "oneOf": [ + { + "type": "null" + }, + { + "title": "GeoJSON Point", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Point" + ] + }, + "coordinates": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON LineString", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "LineString" + ] + }, + "coordinates": { + "type": "array", + "minItems": 2, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON Polygon", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Polygon" + ] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON MultiPoint", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MultiPoint" + ] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON MultiLineString", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MultiLineString" + ] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON MultiPolygon", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MultiPolygon" + ] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON GeometryCollection", + "type": "object", + "required": [ + "type", + "geometries" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "GeometryCollection" + ] + }, + "geometries": { + "type": "array", + "items": { + "oneOf": [ + { + "title": "GeoJSON Point", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Point" + ] + }, + "coordinates": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON LineString", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "LineString" + ] + }, + "coordinates": { + "type": "array", + "minItems": 2, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON Polygon", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Polygon" + ] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON MultiPoint", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MultiPoint" + ] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON MultiLineString", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MultiLineString" + ] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + { + "title": "GeoJSON MultiPolygon", + "type": "object", + "required": [ + "type", + "coordinates" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "MultiPolygon" + ] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + } + ] + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + } + ] + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } +} diff --git a/pygeoapi/validator/__init__.py b/pygeoapi/validator/__init__.py new file mode 100644 index 000000000..0fc3d9452 --- /dev/null +++ b/pygeoapi/validator/__init__.py @@ -0,0 +1,28 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2026 Tom Kralidis +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= diff --git a/pygeoapi/validator/base.py b/pygeoapi/validator/base.py new file mode 100644 index 000000000..ce8170702 --- /dev/null +++ b/pygeoapi/validator/base.py @@ -0,0 +1,81 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2026 Tom Kralidis +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +from http import HTTPStatus +import logging +from typing import Any + +from pygeoapi.error import GenericError + +LOGGER = logging.getLogger(__name__) + + +class BaseValidator: + """generic Validator ABC""" + + def __init__(self, validator_def): + """ + Initialize object + + :param validator_def: validator definition + + :returns: pygeoapi.validator.base.BaseValidator + """ + + self.errors = [] + + def validate(self, data: Any, partial: bool = False) -> list: + """ + Validate a data structure + + :param data: `Any` data type + :param partial: `bool` of whether data to be validated is a + partial resource (default `False`) + + :returns: `list` of validation errors + """ + + raise NotImplementedError() + + def __repr__(self): + return '' + + +class ValidatorGenericError(GenericError): + """validator generic error""" + + default_msg = 'generic validation error (check logs)' + + +class ValidatorValidationError(ValidatorGenericError): + """validator generic error""" + + default_msg = 'Data validation error' + http_status_code = HTTPStatus.BAD_REQUEST + ogc_exception_code = 'InvalidParameterValue' diff --git a/pygeoapi/validator/geojson.py b/pygeoapi/validator/geojson.py new file mode 100644 index 000000000..4235b3aa5 --- /dev/null +++ b/pygeoapi/validator/geojson.py @@ -0,0 +1,92 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2026 Tom Kralidis +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +import logging +import json +import os +from typing import Any + +from jsonschema import Draft202012Validator + +from pygeoapi.validator.base import BaseValidator, ValidatorValidationError + +LOGGER = logging.getLogger(__name__) + +THISDIR = os.path.dirname(os.path.realpath(__file__)) + + +class GeoJSONValidator(BaseValidator): + """GeoJSON validator""" + + def __init__(self, validator_def): + """ + Initialize object + + :returns: pygeoapi.validator.geojson.GeoJSONValidator + """ + + super().__init__(validator_def) + + def validate(self, data: Any, partial: bool = False) -> None: + """ + Validate a GeoJSON payload + + :param data: `Any` data type + :param partial: `bool` of whether data to be validated is a + partial resource (default `False`) + + :returns: `None` or `ValidatorValidationError` + """ + + if partial: + msg = 'Partial validation not supported' + raise ValidatorValidationError(msg) + + schema_file = os.path.join( + THISDIR, '..', 'resources', 'schemas', 'geojson', 'Feature.json') + + LOGGER.debug(f'Validating against {schema_file}') + with open(schema_file) as fh: + data_payload = json.loads(data) + schema_dict = json.load(fh) + + validator = Draft202012Validator(schema_dict) + + errors = [ + f'{list(err.path)}: {err.message}' + for err in validator.iter_errors(data_payload) + ] + + if errors: + msg = 'Invalid GeoJSON payload' + LOGGER.error(f'{msg}: {errors}') + raise ValidatorValidationError(msg) + + def __repr__(self): + return '' diff --git a/tests/provider/test_base_provider.py b/tests/provider/test_base_provider.py index 29df64e5d..f1ff5476f 100644 --- a/tests/provider/test_base_provider.py +++ b/tests/provider/test_base_provider.py @@ -377,7 +377,7 @@ def test_provider_exceptions_http_status_codes(exception_class, expected_code): @pytest.mark.parametrize("exception_class,expected_code", [ - (ProviderInvalidQueryError, "InvalidQuery"), + (ProviderInvalidQueryError, "InvalidParameterValue"), (ProviderItemNotFoundError, "NotFound"), (ProviderNoDataError, "InvalidParameterValue") ]) diff --git a/tests/validator/__init__.py b/tests/validator/__init__.py new file mode 100644 index 000000000..0fc3d9452 --- /dev/null +++ b/tests/validator/__init__.py @@ -0,0 +1,28 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2026 Tom Kralidis +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= diff --git a/tests/validator/test_geojson_validator.py b/tests/validator/test_geojson_validator.py new file mode 100644 index 000000000..9ed35d366 --- /dev/null +++ b/tests/validator/test_geojson_validator.py @@ -0,0 +1,95 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2026 Tom Kralidis +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +import json + +import pytest + +from pygeoapi.validator.base import ValidatorValidationError +from pygeoapi.validator.geojson import GeoJSONValidator + + +@pytest.fixture() +def validator_def(): + return {} + + +@pytest.fixture() +def valid_data(): + data = { + 'geometry': { + 'type': 'Point', + 'coordinates': [ + -130.44472222222223, + 54.28611111111111 + ] + }, + 'type': 'Feature', + 'properties': { + 'id': 1972, + 'foo': 'bar', + 'title': None, + }, + 'id': 48693 + } + + return json.dumps(data) + + +@pytest.fixture() +def invalid_data(): + data = { + 'geometree': { + 'type': 'Point', + 'coordinates': [ + -130.44472222222223, + 54.28611111111111 + ] + }, + 'type': 'Feature', + 'properties': { + 'id': 1972, + 'foo': 'bar', + 'title': None, + }, + 'id': 48693 + } + + return json.dumps(data) + + +def test_valid_data(validator_def, valid_data): + v = GeoJSONValidator(validator_def) + assert v.validate(valid_data) is None + + +def test_invalid_data(validator_def, invalid_data): + v = GeoJSONValidator(validator_def) + with pytest.raises(ValidatorValidationError): + v.validate(invalid_data)