"""Pure-Python validation for native-road package manifests.""" import json import os CONTRACT = "native-road-package/v1.1" RENDERABLE_ROLES = ("surface", "marking") VALID_ROLES = RENDERABLE_ROLES + ("semantic",) def _error(message): raise RuntimeError("Invalid native road manifest: " + message) def _source_name(value): return (isinstance(value, str) and value and value == os.path.basename(value) and value not in (".", "..")) def _validate_split(source, split): if not isinstance(split, dict): _error("layer '%s' splitBy must be an object" % source) prop = split.get("prop") cases = split.get("cases") if not isinstance(prop, str) or not prop or not isinstance(cases, list) or not cases: _error("layer '%s' has an invalid splitBy" % source) matches = set() defaults = 0 for case in cases: if not isinstance(case, dict) or not isinstance(case.get("material"), str): _error("layer '%s' has an invalid splitBy case" % source) is_default = case.get("default") is True has_match = "match" in case if is_default == has_match: _error("layer '%s' splitBy cases need exactly one of match/default" % source) if is_default: defaults += 1 else: match = case["match"] if match in matches: _error("layer '%s' has a duplicate splitBy match" % source) matches.add(match) if defaults != 1: _error("layer '%s' splitBy needs exactly one default case" % source) def load(native_road_dir): """Load a v1.1 manifest and ensure it declares exactly its GeoJSON files.""" manifest_path = os.path.join(native_road_dir, "manifest.json") if not os.path.isfile(manifest_path): raise RuntimeError("Native road manifest is missing: " + manifest_path) try: with open(manifest_path, "r", encoding="utf-8") as handle: manifest = json.load(handle) except (OSError, ValueError) as error: raise RuntimeError("Could not read native road manifest: " + str(error)) from error if not isinstance(manifest, dict) or manifest.get("contract") != CONTRACT: _error("unsupported contract") layers = manifest.get("layers") if not isinstance(layers, list) or not layers: _error("layers must be a non-empty array") declared = set() for spec in layers: if not isinstance(spec, dict): _error("layer entries must be objects") source = spec.get("source") role = spec.get("role") if not _source_name(source) or source in declared or role not in VALID_ROLES: _error("layer source or role is invalid") declared.add(source) if role in RENDERABLE_ROLES: if not isinstance(spec.get("materialLayer"), str) or not spec["materialLayer"]: _error("renderable layer '%s' needs materialLayer" % source) if "splitBy" in spec: _validate_split(source, spec["splitBy"]) elif "materialLayer" in spec or "splitBy" in spec: _error("semantic layer '%s' must not declare rendering" % source) layers_dir = os.path.join(native_road_dir, "layers") if not os.path.isdir(layers_dir): raise RuntimeError("Native road layers directory is missing: " + layers_dir) published = { name[:-8] for name in os.listdir(layers_dir) if name.endswith(".geojson") and os.path.isfile(os.path.join(layers_dir, name)) } if published != declared: missing = sorted(declared - published) extra = sorted(published - declared) _error("declared GeoJSON mismatch (missing=%s, extra=%s)" % (missing, extra)) return manifest