#!/usr/bin/env bash # RON content validator — wrapper for the Rust validate_ron binary (#611). # # Usage: # tooling/validate-ron # tooling/validate-ron --check-name-collisions # # Examples: # tooling/validate-ron server/content/global/zone-types/rural_agricultural.ron zone_type # tooling/validate-ron server/content/global/culture-van-maanens-star.example.ron culture # tooling/validate-ron server/content/global/zone-identity-spec.example.ron zone # tooling/validate-ron --check-name-collisions server/content/global/ # # --check-name-collisions: # Scans all culture-*.ron files in the given directory. Extracts naming.given_names # and naming.family_names pools from each file. Reports any name that appears in more # than one culture's pool. Exits 1 on collision; exits 0 if no collisions found. # Output is machine-readable (one collision per line). set -euo pipefail # --- Name collision mode -------------------------------------------------- if [ "${1:-}" = "--check-name-collisions" ]; then if [ $# -lt 2 ]; then echo "Usage: tooling/validate-ron --check-name-collisions " exit 1 fi SCAN_DIR="$2" if [ ! -d "$SCAN_DIR" ]; then echo "Error: directory not found: $SCAN_DIR" exit 1 fi python3 - "$SCAN_DIR" <<'PYEOF' import sys import re import os import glob scan_dir = sys.argv[1] pattern = os.path.join(scan_dir, "culture-*.ron") files = sorted(glob.glob(pattern)) if not files: print(f"No culture-*.ron files found in: {scan_dir}") sys.exit(0) def extract_names_from_block(text, field): """Extract quoted string list from a named RON array field.""" # Match: field: [\n "name", "name", ...\n] # Handles multi-line arrays with comments inside. pat = re.compile( r'\b' + re.escape(field) + r'\s*:\s*\[([^\]]*)\]', re.DOTALL ) m = pat.search(text) if not m: return [] block = m.group(1) # Strip line comments before extracting quoted strings block = re.sub(r'//[^\n]*', '', block) return re.findall(r'"([^"]+)"', block) # culture_id -> set of names (given + family, tracked separately for reporting) given_by_culture = {} family_by_culture = {} for fpath in files: with open(fpath, 'r', encoding='utf-8') as f: text = f.read() # Extract culture id from the id: "..." field id_m = re.search(r'\bid\s*:\s*"([^"]+)"', text) culture_id = id_m.group(1) if id_m else os.path.basename(fpath) given_by_culture[culture_id] = set(extract_names_from_block(text, 'given_names')) family_by_culture[culture_id] = set(extract_names_from_block(text, 'family_names')) cultures = list(given_by_culture.keys()) collisions_found = False # Check given_names collisions all_given_names = {} # name -> list of cultures for culture, names in given_by_culture.items(): for name in names: all_given_names.setdefault(name, []).append(culture) for name, cultures_with_name in sorted(all_given_names.items()): if len(cultures_with_name) > 1: collisions_found = True print(f"COLLISION given_names: \"{name}\" in {', '.join(sorted(cultures_with_name))}") # Check family_names collisions all_family_names = {} # name -> list of cultures for culture, names in family_by_culture.items(): for name in names: all_family_names.setdefault(name, []).append(culture) for name, cultures_with_name in sorted(all_family_names.items()): if len(cultures_with_name) > 1: collisions_found = True print(f"COLLISION family_names: \"{name}\" in {', '.join(sorted(cultures_with_name))}") if not collisions_found: print(f"OK: no name collisions across {len(cultures)} culture(s): {', '.join(sorted(cultures))}") sys.exit(0) else: sys.exit(1) PYEOF exit $? fi # --- End name collision mode ---------------------------------------------- if [ $# -lt 2 ]; then echo "Usage: tooling/validate-ron " echo " tooling/validate-ron --check-name-collisions " echo "" echo "Validates a RON file against the Rust struct schema." echo "Schema types:" echo " zone_type — ZoneTypeTemplate (D-142 zone-type template)" echo " zone — ZoneSpec (legacy zone identity spec)" echo " culture — CultureProfile (culture profile)" echo "" echo "Name collision check:" echo " --check-name-collisions Scan culture-*.ron files for shared name pool entries" exit 1 fi # Check file exists before resolving — realpath gives unhelpful errors otherwise if [ ! -f "$1" ]; then echo "Error: file not found: $1" exit 1 fi FILE="$(realpath "$1")" SCHEMA="$2" cd "$(dirname "$0")/../server" exec cargo run --quiet --bin validate_ron -- "$FILE" "$SCHEMA"