From 4f492d27467f7308590eb0f6693924c7a5b71bd5 Mon Sep 17 00:00:00 2001 From: sujata-m Date: Mon, 15 Dec 2025 16:33:50 +0530 Subject: [PATCH] BUGFIX-2737 ## Dev Board Ticket https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/2737 ## Changes - Restored custom field support across schemas (CustomNode/Edge/Zone) while aligning required fields with original behavior. - Relaxed edges/zones global required lists to let custom features omit `highway` (kept in branch-specific schemas). - Enhanced 0.2 compatibility guard: allow `ext:` on nodes, block custom content per dataset with specific messages (e.g., Custom Edge/Point/Polygon), and reject non-point `ext:`. - Added schema parity tests and fixtures for custom feature branches and 0.2 vs 0.3 behavior, including guard reason checks. - Improved diff tooling/reporting and parity fixtures to cover custom scenarios. ## Testing - Added 14 new test cases (all in tests/test_schema_parity.py) and 30 new fixture files under tests/schema/fixtures (covering nodes/edges/lines/points/polygons/zones). --- CHANGELOG.md | 7 + src/python_osw_validation/__init__.py | 41 ++++- .../opensidewalks.edges.schema-0.3.json | 45 +++++- .../opensidewalks.nodes.schema-0.3.json | 19 ++- .../opensidewalks.zones.schema-0.3.json | 61 ++++++- src/python_osw_validation/version.py | 2 +- .../fixtures/edges/invalid_bad_geometry.json | 17 ++ .../edges/invalid_custom_edge_schema02.json | 27 ++++ .../edges/invalid_missing_required.json | 15 ++ .../fixtures/edges/valid_custom_edge.json | 17 ++ .../edges/valid_custom_edge_no_highway.json | 16 ++ .../schema/fixtures/edges/valid_minimal.json | 17 ++ .../schema/fixtures/edges/valid_typical.json | 19 +++ .../fixtures/lines/invalid_bad_geometry.json | 11 ++ .../lines/invalid_custom_line_schema02.json | 14 ++ .../lines/invalid_missing_required.json | 11 ++ .../fixtures/lines/valid_custom_line.json | 14 ++ .../schema/fixtures/lines/valid_minimal.json | 11 ++ .../schema/fixtures/lines/valid_typical.json | 14 ++ .../fixtures/nodes/invalid_bad_geometry.json | 11 ++ .../nodes/invalid_missing_required.json | 10 ++ .../fixtures/nodes/valid_custom_node.json | 11 ++ .../schema/fixtures/nodes/valid_minimal.json | 11 ++ .../nodes/valid_node_ext_schema02.json | 16 ++ .../schema/fixtures/nodes/valid_typical.json | 16 ++ .../fixtures/points/invalid_bad_geometry.json | 11 ++ .../points/invalid_custom_point_schema02.json | 14 ++ .../points/invalid_missing_required.json | 11 ++ .../fixtures/points/valid_custom_point.json | 14 ++ .../schema/fixtures/points/valid_minimal.json | 11 ++ .../schema/fixtures/points/valid_typical.json | 14 ++ .../invalid_custom_polygon_schema02.json | 14 ++ .../polygons/valid_custom_polygon.json | 13 ++ .../zones/invalid_custom_zone_schema02.json | 15 ++ .../fixtures/zones/valid_custom_zone.json | 16 ++ .../zones/valid_custom_zone_no_highway.json | 15 ++ tests/test_schema_parity.py | 149 ++++++++++++++++++ .../unit_tests/test_osw_validation_extras.py | 2 +- 38 files changed, 735 insertions(+), 17 deletions(-) create mode 100644 tests/schema/fixtures/edges/invalid_bad_geometry.json create mode 100644 tests/schema/fixtures/edges/invalid_custom_edge_schema02.json create mode 100644 tests/schema/fixtures/edges/invalid_missing_required.json create mode 100644 tests/schema/fixtures/edges/valid_custom_edge.json create mode 100644 tests/schema/fixtures/edges/valid_custom_edge_no_highway.json create mode 100644 tests/schema/fixtures/edges/valid_minimal.json create mode 100644 tests/schema/fixtures/edges/valid_typical.json create mode 100644 tests/schema/fixtures/lines/invalid_bad_geometry.json create mode 100644 tests/schema/fixtures/lines/invalid_custom_line_schema02.json create mode 100644 tests/schema/fixtures/lines/invalid_missing_required.json create mode 100644 tests/schema/fixtures/lines/valid_custom_line.json create mode 100644 tests/schema/fixtures/lines/valid_minimal.json create mode 100644 tests/schema/fixtures/lines/valid_typical.json create mode 100644 tests/schema/fixtures/nodes/invalid_bad_geometry.json create mode 100644 tests/schema/fixtures/nodes/invalid_missing_required.json create mode 100644 tests/schema/fixtures/nodes/valid_custom_node.json create mode 100644 tests/schema/fixtures/nodes/valid_minimal.json create mode 100644 tests/schema/fixtures/nodes/valid_node_ext_schema02.json create mode 100644 tests/schema/fixtures/nodes/valid_typical.json create mode 100644 tests/schema/fixtures/points/invalid_bad_geometry.json create mode 100644 tests/schema/fixtures/points/invalid_custom_point_schema02.json create mode 100644 tests/schema/fixtures/points/invalid_missing_required.json create mode 100644 tests/schema/fixtures/points/valid_custom_point.json create mode 100644 tests/schema/fixtures/points/valid_minimal.json create mode 100644 tests/schema/fixtures/points/valid_typical.json create mode 100644 tests/schema/fixtures/polygons/invalid_custom_polygon_schema02.json create mode 100644 tests/schema/fixtures/polygons/valid_custom_polygon.json create mode 100644 tests/schema/fixtures/zones/invalid_custom_zone_schema02.json create mode 100644 tests/schema/fixtures/zones/valid_custom_zone.json create mode 100644 tests/schema/fixtures/zones/valid_custom_zone_no_highway.json create mode 100644 tests/test_schema_parity.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c936e..6787619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change log +### 0.3.1 - 2025-02-12 +- Restored custom field support across schemas (CustomNode/Edge/Zone) while aligning required fields with original behavior. +- Relaxed edges/zones global required lists to let custom features omit `highway` (kept in branch-specific schemas). +- Enhanced 0.2 compatibility guard: allow `ext:` on nodes, block custom content per dataset with specific messages (e.g., Custom Edge/Point/Polygon), and reject non-point `ext:`. +- Added schema parity tests and fixtures for custom feature branches and 0.2 vs 0.3 behavior, including guard reason checks. +- Improved diff tooling/reporting and parity fixtures to cover custom scenarios. + ### 0.3.0 - Default to OSW 0.3 dataset-specific schemas (edges, lines, nodes, points, polygons, zones) with filename-driven selection; removed legacy monolithic/geometry schema files. - Enforce the six canonical OSW 0.3 filenames inside datasets; reject non-standard names and detect duplicates/missing required files (with new unit tests). diff --git a/src/python_osw_validation/__init__.py b/src/python_osw_validation/__init__.py index fd64670..22a0721 100644 --- a/src/python_osw_validation/__init__.py +++ b/src/python_osw_validation/__init__.py @@ -124,15 +124,24 @@ def _schema_key_from_text(self, text: Optional[str]) -> Optional[str]: return key return None - def _contains_disallowed_features_for_02(self, geojson_data: Dict[str, Any]) -> bool: - """Detect Tree coverage or Custom Point/Line/Polygon in legacy 0.2 datasets.""" + def _contains_disallowed_features_for_02(self, geojson_data: Dict[str, Any]) -> set: + """Detect Tree coverage or Custom content in legacy 0.2 datasets. + + Returns a set of reason tags, e.g. {"tree", "custom_ext", "custom_token"}. + Empty set means no 0.2-only violations detected. + """ + reasons = set() for feat in geojson_data.get("features", []): props = feat.get("properties") or {} + geom = feat.get("geometry") or {} + geom_type = geom.get("type") if isinstance(geom, dict) else None + is_point = isinstance(geom_type, str) and geom_type.lower() == "point" + val = props.get("natural") if isinstance(val, str) and val.strip().lower() in {"tree", "wood"}: - return True + reasons.add("tree") if any(k in props for k in ("leaf_cycle", "leaf_type")): - return True + reasons.add("tree") for k, v in props.items(): target = "" if isinstance(v, str): @@ -142,8 +151,8 @@ def _contains_disallowed_features_for_02(self, geojson_data: Dict[str, Any]) -> if any(tok in target for tok in ["custom point", "custom_point", "custompoint", "custom line", "custom_line", "customline", "custom polygon", "custom_polygon", "custompolygon"]): - return True - return False + reasons.add("custom_token") + return reasons # ---------------------------- # Schema selection @@ -475,9 +484,25 @@ def validate_osw_errors(self, file_path: str, max_errors: int) -> bool: schema_url = geojson_data.get('$schema') if isinstance(schema_url, str) and '0.2/schema.json' in schema_url: - if self._contains_disallowed_features_for_02(geojson_data): + reasons = self._contains_disallowed_features_for_02(geojson_data) + if reasons: + dataset_key = self._schema_key_from_text(file_path) or "data" + custom_label_map = { + "edges": "Custom Edge", + "lines": "Custom Line", + "polygons": "Custom Polygon", + "zones": "Custom Polygon/Zone", + "points": "Custom Point", + "nodes": "Custom Node", + } + parts = [] + if "tree" in reasons: + parts.append("Tree coverage") + if "custom_ext" in reasons or "custom_token" in reasons: + parts.append(custom_label_map.get(dataset_key, "Custom content")) + msg = f"0.2 schema does not support " + " and ".join(parts) self.log_errors( - message="0.2 schema does not support Tree coverage, Custom Point, Custom Line, and Custom Polygon", + message=msg, filename=os.path.basename(file_path), feature_index=None, ) diff --git a/src/python_osw_validation/schema/opensidewalks.edges.schema-0.3.json b/src/python_osw_validation/schema/opensidewalks.edges.schema-0.3.json index e7afe75..591ff5a 100644 --- a/src/python_osw_validation/schema/opensidewalks.edges.schema-0.3.json +++ b/src/python_osw_validation/schema/opensidewalks.edges.schema-0.3.json @@ -308,10 +308,49 @@ "required": [ "_id", "_u_id", - "_v_id", - "highway" + "_v_id" ], "anyOf": [ + { + "title": "CustomEdgeFields", + "type": "object", + "additionalProperties": false, + "properties": { + "_id": { + "minLength": 1, + "type": "string" + }, + "_u_id": { + "minLength": 1, + "type": "string" + }, + "_v_id": { + "minLength": 1, + "type": "string" + }, + "foot": { + "description": "A field that indicates whether an edge can be used by pedestrians.", + "enum": [ + "designated", + "destination", + "no", + "permissive", + "private", + "use_sidepath", + "yes" + ], + "type": "string" + } + }, + "required": [ + "_id", + "_u_id", + "_v_id" + ], + "patternProperties": { + "^ext:.*$": {} + } + }, { "title": "AlleyFields", "type": "object", @@ -1895,4 +1934,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/python_osw_validation/schema/opensidewalks.nodes.schema-0.3.json b/src/python_osw_validation/schema/opensidewalks.nodes.schema-0.3.json index 298c64a..35d01cf 100644 --- a/src/python_osw_validation/schema/opensidewalks.nodes.schema-0.3.json +++ b/src/python_osw_validation/schema/opensidewalks.nodes.schema-0.3.json @@ -196,6 +196,23 @@ "_id" ], "anyOf": [ + { + "title": "CustomNodeFields", + "type": "object", + "additionalProperties": false, + "properties": { + "_id": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "_id" + ], + "patternProperties": { + "^ext:.*$": {} + } + }, { "title": "BareNodeFields", "type": "object", @@ -417,4 +434,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/python_osw_validation/schema/opensidewalks.zones.schema-0.3.json b/src/python_osw_validation/schema/opensidewalks.zones.schema-0.3.json index 3e650de..87f480e 100644 --- a/src/python_osw_validation/schema/opensidewalks.zones.schema-0.3.json +++ b/src/python_osw_validation/schema/opensidewalks.zones.schema-0.3.json @@ -225,10 +225,65 @@ }, "required": [ "_id", - "_w_id", - "highway" + "_w_id" ], "anyOf": [ + { + "title": "CustomZoneFields", + "type": "object", + "additionalProperties": false, + "properties": { + "_id": { + "minLength": 1, + "type": "string" + }, + "_w_id": { + "items": { + "type": "string" + }, + "type": "array" + }, + "foot": { + "description": "A field that indicates whether an edge can be used by pedestrians.", + "enum": [ + "designated", + "destination", + "no", + "permissive", + "private", + "use_sidepath", + "yes" + ], + "type": "string" + }, + "name": { + "description": "A field for a designated name for an entity. Example: an official name for a trail.", + "type": "string" + }, + "surface": { + "description": "A field for the surface material of the path.", + "enum": [ + "asphalt", + "concrete", + "dirt", + "grass", + "grass_paver", + "gravel", + "paved", + "paving_stones", + "unpaved" + ], + "type": "string" + } + }, + "required": [ + "_id", + "_w_id" + ], + "patternProperties": { + "^ext:.*$": {} + } + }, { "title": "PedestrianZoneFields", "type": "object", @@ -302,4 +357,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/python_osw_validation/version.py b/src/python_osw_validation/version.py index 0404d81..e1424ed 100644 --- a/src/python_osw_validation/version.py +++ b/src/python_osw_validation/version.py @@ -1 +1 @@ -__version__ = '0.3.0' +__version__ = '0.3.1' diff --git a/tests/schema/fixtures/edges/invalid_bad_geometry.json b/tests/schema/fixtures/edges/invalid_bad_geometry.json new file mode 100644 index 0000000..339a6c6 --- /dev/null +++ b/tests/schema/fixtures/edges/invalid_bad_geometry.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": { + "_id": "edge-4", + "_u_id": "node-f", + "_v_id": "node-g", + "footway": "sidewalk", + "highway": "footway" + } + } + ] +} diff --git a/tests/schema/fixtures/edges/invalid_custom_edge_schema02.json b/tests/schema/fixtures/edges/invalid_custom_edge_schema02.json new file mode 100644 index 0000000..61c6847 --- /dev/null +++ b/tests/schema/fixtures/edges/invalid_custom_edge_schema02.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.2/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": { + "_id": "sw_3234", + "highway": "footway", + "footway": "traffic_island", + "_u_id": "1", + "_v_id": "2" + } + }, + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": { + "_id": "cus_123", + "_u_id": "3", + "_v_id": "5", + "ext:test_field": "custom_value" + } + } + ] +} diff --git a/tests/schema/fixtures/edges/invalid_missing_required.json b/tests/schema/fixtures/edges/invalid_missing_required.json new file mode 100644 index 0000000..6d164cb --- /dev/null +++ b/tests/schema/fixtures/edges/invalid_missing_required.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": { + "_id": "edge-3", + "_u_id": "node-e", + "footway": "sidewalk" + } + } + ] +} diff --git a/tests/schema/fixtures/edges/valid_custom_edge.json b/tests/schema/fixtures/edges/valid_custom_edge.json new file mode 100644 index 0000000..09172b2 --- /dev/null +++ b/tests/schema/fixtures/edges/valid_custom_edge.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": { + "_id": "custom-edge-1", + "_u_id": "node-1", + "_v_id": "node-2", + "highway": "service", + "foot": "yes" + } + } + ] +} diff --git a/tests/schema/fixtures/edges/valid_custom_edge_no_highway.json b/tests/schema/fixtures/edges/valid_custom_edge_no_highway.json new file mode 100644 index 0000000..836e793 --- /dev/null +++ b/tests/schema/fixtures/edges/valid_custom_edge_no_highway.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": { + "_id": "custom-edge-2", + "_u_id": "node-3", + "_v_id": "node-4", + "foot": "yes" + } + } + ] +} diff --git a/tests/schema/fixtures/edges/valid_minimal.json b/tests/schema/fixtures/edges/valid_minimal.json new file mode 100644 index 0000000..66c5742 --- /dev/null +++ b/tests/schema/fixtures/edges/valid_minimal.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": { + "_id": "edge-1", + "_u_id": "node-a", + "_v_id": "node-b", + "footway": "sidewalk", + "highway": "footway" + } + } + ] +} diff --git a/tests/schema/fixtures/edges/valid_typical.json b/tests/schema/fixtures/edges/valid_typical.json new file mode 100644 index 0000000..900ac59 --- /dev/null +++ b/tests/schema/fixtures/edges/valid_typical.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[-1, 0], [0, 1]]}, + "properties": { + "_id": "edge-2", + "_u_id": "node-c", + "_v_id": "node-d", + "footway": "crossing", + "highway": "footway", + "surface": "asphalt", + "width": 2.5 + } + } + ] +} diff --git a/tests/schema/fixtures/lines/invalid_bad_geometry.json b/tests/schema/fixtures/lines/invalid_bad_geometry.json new file mode 100644 index 0000000..eba4967 --- /dev/null +++ b/tests/schema/fixtures/lines/invalid_bad_geometry.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": {"_id": "line-3"} + } + ] +} diff --git a/tests/schema/fixtures/lines/invalid_custom_line_schema02.json b/tests/schema/fixtures/lines/invalid_custom_line_schema02.json new file mode 100644 index 0000000..82318d0 --- /dev/null +++ b/tests/schema/fixtures/lines/invalid_custom_line_schema02.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.2/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0,0],[1,1]]}, + "properties": { + "_id": "cl-02", + "ext:test_field": "custom" + } + } + ] +} diff --git a/tests/schema/fixtures/lines/invalid_missing_required.json b/tests/schema/fixtures/lines/invalid_missing_required.json new file mode 100644 index 0000000..169fc78 --- /dev/null +++ b/tests/schema/fixtures/lines/invalid_missing_required.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": {} + } + ] +} diff --git a/tests/schema/fixtures/lines/valid_custom_line.json b/tests/schema/fixtures/lines/valid_custom_line.json new file mode 100644 index 0000000..040bafa --- /dev/null +++ b/tests/schema/fixtures/lines/valid_custom_line.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0,0],[1,1]]}, + "properties": { + "_id": "custom-line-1", + "length": 10 + } + } + ] +} diff --git a/tests/schema/fixtures/lines/valid_minimal.json b/tests/schema/fixtures/lines/valid_minimal.json new file mode 100644 index 0000000..c90164f --- /dev/null +++ b/tests/schema/fixtures/lines/valid_minimal.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [2, 2]]}, + "properties": {"_id": "line-1"} + } + ] +} diff --git a/tests/schema/fixtures/lines/valid_typical.json b/tests/schema/fixtures/lines/valid_typical.json new file mode 100644 index 0000000..0718c34 --- /dev/null +++ b/tests/schema/fixtures/lines/valid_typical.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[1, 1], [1, 2]]}, + "properties": { + "_id": "line-2", + "barrier": "fence" + } + } + ] +} diff --git a/tests/schema/fixtures/nodes/invalid_bad_geometry.json b/tests/schema/fixtures/nodes/invalid_bad_geometry.json new file mode 100644 index 0000000..bb53faf --- /dev/null +++ b/tests/schema/fixtures/nodes/invalid_bad_geometry.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": {"_id": "node-4"} + } + ] +} diff --git a/tests/schema/fixtures/nodes/invalid_missing_required.json b/tests/schema/fixtures/nodes/invalid_missing_required.json new file mode 100644 index 0000000..eba8158 --- /dev/null +++ b/tests/schema/fixtures/nodes/invalid_missing_required.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"_id": "node-3"} + } + ] +} diff --git a/tests/schema/fixtures/nodes/valid_custom_node.json b/tests/schema/fixtures/nodes/valid_custom_node.json new file mode 100644 index 0000000..542f9cf --- /dev/null +++ b/tests/schema/fixtures/nodes/valid_custom_node.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": {"_id": "custom-node-1"} + } + ] +} diff --git a/tests/schema/fixtures/nodes/valid_minimal.json b/tests/schema/fixtures/nodes/valid_minimal.json new file mode 100644 index 0000000..487958b --- /dev/null +++ b/tests/schema/fixtures/nodes/valid_minimal.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": {"_id": "node-1"} + } + ] +} diff --git a/tests/schema/fixtures/nodes/valid_node_ext_schema02.json b/tests/schema/fixtures/nodes/valid_node_ext_schema02.json new file mode 100644 index 0000000..67a654c --- /dev/null +++ b/tests/schema/fixtures/nodes/valid_node_ext_schema02.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.2/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": {"_id": "n1"} + }, + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [1, 1]}, + "properties": {"_id": "n2", "ext:test_field": "custom_value"} + } + ] +} diff --git a/tests/schema/fixtures/nodes/valid_typical.json b/tests/schema/fixtures/nodes/valid_typical.json new file mode 100644 index 0000000..05104dc --- /dev/null +++ b/tests/schema/fixtures/nodes/valid_typical.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [1.2, 3.4]}, + "properties": { + "_id": "node-2", + "barrier": "kerb", + "kerb": "lowered", + "tactile_paving": "yes" + } + } + ] +} diff --git a/tests/schema/fixtures/points/invalid_bad_geometry.json b/tests/schema/fixtures/points/invalid_bad_geometry.json new file mode 100644 index 0000000..4915efb --- /dev/null +++ b/tests/schema/fixtures/points/invalid_bad_geometry.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]}, + "properties": {"_id": "point-3"} + } + ] +} diff --git a/tests/schema/fixtures/points/invalid_custom_point_schema02.json b/tests/schema/fixtures/points/invalid_custom_point_schema02.json new file mode 100644 index 0000000..c0c5361 --- /dev/null +++ b/tests/schema/fixtures/points/invalid_custom_point_schema02.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.2/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [1, 2]}, + "properties": { + "_id": "cp-02", + "custom_point": true + } + } + ] +} diff --git a/tests/schema/fixtures/points/invalid_missing_required.json b/tests/schema/fixtures/points/invalid_missing_required.json new file mode 100644 index 0000000..9512115 --- /dev/null +++ b/tests/schema/fixtures/points/invalid_missing_required.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [1, 2]}, + "properties": {} + } + ] +} diff --git a/tests/schema/fixtures/points/valid_custom_point.json b/tests/schema/fixtures/points/valid_custom_point.json new file mode 100644 index 0000000..599dbca --- /dev/null +++ b/tests/schema/fixtures/points/valid_custom_point.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [0.5, 1.5]}, + "properties": { + "_id": "custom-point-1", + "ext:note": "demo" + } + } + ] +} diff --git a/tests/schema/fixtures/points/valid_minimal.json b/tests/schema/fixtures/points/valid_minimal.json new file mode 100644 index 0000000..ddceed0 --- /dev/null +++ b/tests/schema/fixtures/points/valid_minimal.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [5, 5]}, + "properties": {"_id": "point-1"} + } + ] +} diff --git a/tests/schema/fixtures/points/valid_typical.json b/tests/schema/fixtures/points/valid_typical.json new file mode 100644 index 0000000..ce18b5a --- /dev/null +++ b/tests/schema/fixtures/points/valid_typical.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [6.1, 7.2]}, + "properties": { + "_id": "point-2", + "natural": "tree" + } + } + ] +} diff --git a/tests/schema/fixtures/polygons/invalid_custom_polygon_schema02.json b/tests/schema/fixtures/polygons/invalid_custom_polygon_schema02.json new file mode 100644 index 0000000..7da1070 --- /dev/null +++ b/tests/schema/fixtures/polygons/invalid_custom_polygon_schema02.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.2/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Polygon", "coordinates": [[[0,0],[1,0],[1,1],[0,1],[0,0]]]}, + "properties": { + "_id": "cpoly-02", + "ext:test_field": "custom" + } + } + ] +} diff --git a/tests/schema/fixtures/polygons/valid_custom_polygon.json b/tests/schema/fixtures/polygons/valid_custom_polygon.json new file mode 100644 index 0000000..8742444 --- /dev/null +++ b/tests/schema/fixtures/polygons/valid_custom_polygon.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Polygon", "coordinates": [[[0,0],[1,0],[1,1],[0,1],[0,0]]]}, + "properties": { + "_id": "custom-poly-1" + } + } + ] +} diff --git a/tests/schema/fixtures/zones/invalid_custom_zone_schema02.json b/tests/schema/fixtures/zones/invalid_custom_zone_schema02.json new file mode 100644 index 0000000..f4ee43d --- /dev/null +++ b/tests/schema/fixtures/zones/invalid_custom_zone_schema02.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.2/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Polygon", "coordinates": [[[0,0],[1,0],[1,1],[0,1],[0,0]]]}, + "properties": { + "_id": "cz-02", + "_w_id": ["way-1"], + "ext:test_field": "custom" + } + } + ] +} diff --git a/tests/schema/fixtures/zones/valid_custom_zone.json b/tests/schema/fixtures/zones/valid_custom_zone.json new file mode 100644 index 0000000..2e9b3ab --- /dev/null +++ b/tests/schema/fixtures/zones/valid_custom_zone.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Polygon", "coordinates": [[[0,0],[1,0],[1,1],[0,1],[0,0]]]}, + "properties": { + "_id": "custom-zone-1", + "_w_id": ["way-1"], + "highway": "pedestrian", + "foot": "yes" + } + } + ] +} diff --git a/tests/schema/fixtures/zones/valid_custom_zone_no_highway.json b/tests/schema/fixtures/zones/valid_custom_zone_no_highway.json new file mode 100644 index 0000000..91b3986 --- /dev/null +++ b/tests/schema/fixtures/zones/valid_custom_zone_no_highway.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://sidewalks.washington.edu/opensidewalks/0.3/schema.json", + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Polygon", "coordinates": [[[0,0],[1,0],[1,1],[0,1],[0,0]]]}, + "properties": { + "_id": "custom-zone-2", + "_w_id": ["way-2"], + "foot": "yes" + } + } + ] +} diff --git a/tests/test_schema_parity.py b/tests/test_schema_parity.py new file mode 100644 index 0000000..6c1e8b1 --- /dev/null +++ b/tests/test_schema_parity.py @@ -0,0 +1,149 @@ +import json +from pathlib import Path + +import jsonschema_rs + +SCHEMA_DIR = Path(__file__).parent.parent / "src" / "python_osw_validation" / "schema" +FIXTURES_DIR = Path(__file__).parent / "schema" / "fixtures" + +PAIRS = { + "nodes": ("nodes.schema.json", "opensidewalks.nodes.schema-0.3.json"), + "edges": ("edges.schema.json", "opensidewalks.edges.schema-0.3.json"), + "zones": ("zones.schema.json", "opensidewalks.zones.schema-0.3.json"), +} + + +def load(path: Path): + with path.open("r", encoding="utf-8") as f: + return json.load(f) + + +def validator(schema_name: str, customized: bool): + fname = PAIRS[schema_name][1 if customized else 0] + schema = load(SCHEMA_DIR / fname) + return jsonschema_rs.Draft7Validator(schema) + + +def run_fixture(schema_name: str, fixture_name: str): + data = load(FIXTURES_DIR / schema_name / fixture_name) + return data + + +# --- Nodes --- + +def test_nodes_accept_custom_node_fields(): + data = run_fixture("nodes", "valid_custom_node.json") + orig = validator("nodes", customized=False) + ours = validator("nodes", customized=True) + assert orig.is_valid(data) + assert ours.is_valid(data) + + +# --- Edges --- + +def test_edges_accept_custom_edge_fields(): + data = run_fixture("edges", "valid_custom_edge.json") + orig = validator("edges", customized=False) + ours = validator("edges", customized=True) + assert orig.is_valid(data) + assert ours.is_valid(data) + + +def test_edges_accept_custom_edge_without_highway(): + data = run_fixture("edges", "valid_custom_edge_no_highway.json") + orig = validator("edges", customized=False) + ours = validator("edges", customized=True) + assert orig.is_valid(data) + assert ours.is_valid(data) + + +def test_edges_schema_02_rejects_custom_extensions(): + data = run_fixture("edges", "invalid_custom_edge_schema02.json") + # In 0.2 datasets, custom extensions (ext:*) are not allowed; this is enforced + # by the validator's 0.2 compatibility guard. + from src.python_osw_validation import OSWValidation + guard = OSWValidation(zipfile_path="dummy.zip") + reasons = guard._contains_disallowed_features_for_02(data) + assert reasons == {"custom_token"} + + +def test_nodes_schema_02_allows_ext_fields(): + data = run_fixture("nodes", "valid_node_ext_schema02.json") + from src.python_osw_validation import OSWValidation + guard = OSWValidation(zipfile_path="dummy.zip") + reasons = guard._contains_disallowed_features_for_02(data) + assert reasons == set() + + +def test_custom_point_schema03_accepts(): + data = run_fixture("points", "valid_custom_point.json") + orig = validator("points", customized=False) + ours = validator("points", customized=True) + assert orig.is_valid(data) + assert ours.is_valid(data) + + +def test_custom_point_schema02_rejects_custom_flag(): + data = run_fixture("points", "invalid_custom_point_schema02.json") + from src.python_osw_validation import OSWValidation + guard = OSWValidation(zipfile_path="dummy.zip") + reasons = guard._contains_disallowed_features_for_02(data) + assert reasons == {"custom_token"} + + +def test_custom_line_schema03_accepts(): + data = run_fixture("lines", "valid_custom_line.json") + orig = validator("lines", customized=False) + ours = validator("lines", customized=True) + assert orig.is_valid(data) + assert ours.is_valid(data) + + +def test_custom_line_schema02_rejects_ext(): + data = run_fixture("lines", "invalid_custom_line_schema02.json") + from src.python_osw_validation import OSWValidation + guard = OSWValidation(zipfile_path="dummy.zip") + reasons = guard._contains_disallowed_features_for_02(data) + assert reasons == {"custom_token"} + + +def test_custom_polygon_schema03_accepts(): + data = run_fixture("polygons", "valid_custom_polygon.json") + orig = validator("polygons", customized=False) + ours = validator("polygons", customized=True) + assert orig.is_valid(data) + assert ours.is_valid(data) + + +def test_custom_polygon_schema02_rejects_ext(): + data = run_fixture("polygons", "invalid_custom_polygon_schema02.json") + from src.python_osw_validation import OSWValidation + guard = OSWValidation(zipfile_path="dummy.zip") + reasons = guard._contains_disallowed_features_for_02(data) + assert reasons == {"custom_token"} + + +def test_custom_zone_schema02_rejects_ext(): + data = run_fixture("zones", "invalid_custom_zone_schema02.json") + from src.python_osw_validation import OSWValidation + guard = OSWValidation(zipfile_path="dummy.zip") + reasons = guard._contains_disallowed_features_for_02(data) + assert reasons == {"custom_token"} + + +# --- Zones --- + +def test_zones_accept_custom_zone_fields(): + data = run_fixture("zones", "valid_custom_zone.json") + orig = validator("zones", customized=False) + ours = validator("zones", customized=True) + assert orig.is_valid(data) + assert ours.is_valid(data) + + +def test_zones_accept_custom_zone_without_highway(): + data = run_fixture("zones", "valid_custom_zone_no_highway.json") + orig = validator("zones", customized=False) + ours = validator("zones", customized=True) + assert orig.is_valid(data) + assert ours.is_valid(data) diff --git a/tests/unit_tests/test_osw_validation_extras.py b/tests/unit_tests/test_osw_validation_extras.py index 320a5c9..0f4818a 100644 --- a/tests/unit_tests/test_osw_validation_extras.py +++ b/tests/unit_tests/test_osw_validation_extras.py @@ -620,7 +620,7 @@ def _feature(props): finally: os.unlink(path_custom) self.assertFalse(res2) - self.assertTrue(any("0.2 schema does not support Tree coverage" in e for e in (v2.errors or [])), + self.assertTrue(any("0.2 schema does not support" in e for e in (v2.errors or [])), f"Errors were: {v2.errors}") path_wood = self._write_geojson({**base, "features": [_feature({"natural": "wood", "leaf_cycle": "mixed"})]})