Skip to content

Commit e277215

Browse files
authored
Merge pull request #39 from TaskarCenterAtUW/develop
[v0.2.12] Develop to Main
2 parents 76adda8 + 67f6e49 commit e277215

File tree

9 files changed

+3836
-113
lines changed

9 files changed

+3836
-113
lines changed

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# Change log
22

3+
### 0.2.12
4+
5+
#### Added
6+
- Per-geometry schema support: auto-picks Point/LineString/Polygon schemas with sensible defaults.
7+
- Structured per-feature **issues** output (former “fixme”): one best, human-friendly message per feature.
8+
- Friendly error formatter:
9+
- Compacts `Enum` errors.
10+
- Summarizes `anyOf` by unioning required keys → “must include one of: …”.
11+
- `_feature_index_from_error()` to reliably extract `feature_index` from `jsonschema_rs` error paths.
12+
- `_get_colset()` utility for safe set extraction with diagnostics for missing columns.
13+
- Unit tests covering helpers, schema selection, and issues aggregation.
14+
15+
#### Changed
16+
- `validate()` now **streams** `jsonschema_rs` errors; legacy `errors` list remains but is capped by `max_errors`.
17+
- `ValidationResult` now includes `issues`.
18+
- Schema selection prefers geometry from the first feature; falls back to filename heuristics (`nodes/points`, `edges/lines`, `zones/polygons`).
19+
20+
#### Fixed
21+
- Robust GeoJSON/extension handling:
22+
- Safe fallback to index when `_id` is missing.
23+
- Non-serializable property detection in extensions (with clear messages).
24+
- Safer flattening of `_w_id` (list-like) for zone validations.
25+
26+
#### Migration Notes
27+
- Prefer consuming `ValidationResult.issues` for per-feature UX and tooling.
28+
329
### 0.2.11
430

531
- Fixed [BUG-2065](https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/2065/)

src/python_osw_validation/__init__.py

Lines changed: 333 additions & 112 deletions
Large diffs are not rendered by default.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from typing import Optional
2+
import re
3+
4+
def _feature_index_from_error(err) -> Optional[int]:
5+
"""
6+
Return the index after 'features' in the instance path, else None.
7+
Works with jsonschema_rs errors.
8+
"""
9+
path = list(getattr(err, "instance_path", []))
10+
for i, seg in enumerate(path):
11+
if seg == "features" and i + 1 < len(path) and isinstance(path[i + 1], int):
12+
return path[i + 1]
13+
return None
14+
15+
def _err_kind(err) -> str:
16+
"""
17+
Best-effort classification of error kind.
18+
Prefers jsonschema_rs 'kind', falls back to 'validator', then message.
19+
"""
20+
kobj = getattr(err, "kind", None)
21+
if kobj is not None:
22+
return type(kobj).__name__.split("_")[-1] # e.g. 'AnyOf', 'Enum', 'Required'
23+
v = getattr(err, "validator", None)
24+
if isinstance(v, str):
25+
return v[0].upper() + v[1:] # 'anyOf' -> 'AnyOf'
26+
msg = getattr(err, "message", "") or ""
27+
return "AnyOf" if "anyOf" in msg else ""
28+
29+
30+
def _clean_enum_message(err) -> str:
31+
"""Compact enum error (strip ‘…or N other candidates’)."""
32+
msg = getattr(err, "message", "") or ""
33+
msg = re.sub(r"\s*or\s+\d+\s+other candidates", "", msg)
34+
return msg.split("\n")[0]
35+
36+
37+
def _pretty_message(err, schema) -> str:
38+
"""
39+
Convert a jsonschema_rs error to a concise, user-friendly string.
40+
41+
Special handling:
42+
- Enum → compact message
43+
- AnyOf → summarize the union of 'required' fields across branches:
44+
"must include one of: <fields>"
45+
"""
46+
kind = _err_kind(err)
47+
48+
if kind == "Enum":
49+
return _clean_enum_message(err)
50+
51+
if kind == "AnyOf":
52+
# Follow schema_path to the anyOf node; union of 'required' keys in branches.
53+
sub = schema
54+
try:
55+
for seg in getattr(err, "schema_path", []):
56+
sub = sub[seg]
57+
58+
required = set()
59+
60+
def crawl(node):
61+
if isinstance(node, dict):
62+
if isinstance(node.get("required"), list):
63+
required.update(node["required"])
64+
for key in ("allOf", "anyOf", "oneOf"):
65+
if isinstance(node.get(key), list):
66+
for child in node[key]:
67+
crawl(child)
68+
elif isinstance(node, list):
69+
for child in node:
70+
crawl(child)
71+
72+
crawl(sub)
73+
74+
if required:
75+
props = ", ".join(sorted(required))
76+
return f"must include one of: {props}"
77+
except Exception:
78+
pass
79+
80+
# Default: first line from library message
81+
return (getattr(err, "message", "") or "").split("\n")[0]
82+
83+
84+
def _rank_for(err) -> tuple:
85+
"""
86+
Ranking for 'best' error per feature.
87+
Prefer Enum > (Type/Required/Const) > (Pattern/Minimum/Maximum) > others.
88+
"""
89+
kind = _err_kind(err)
90+
order = (
91+
0 if kind == "Enum" else
92+
1 if kind in {"Type", "Required", "Const"} else
93+
2 if kind in {"Pattern", "Minimum", "Maximum"} else
94+
3
95+
)
96+
length = len(getattr(err, "message", "") or "")
97+
return (order, length)

0 commit comments

Comments
 (0)