Skip to content

Commit f4412ef

Browse files
vivoeivaclaude
andcommitted
Add validate_xml.py - XSD and Schematron validation tool
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ccaf48e commit f4412ef

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

tools/validate_xml.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
"""Validate a BehaviorTree.CPP XML file against XSD and/or Schematron schemas.
3+
4+
Exit codes:
5+
0 all enabled validations passed
6+
1 one or more validation errors
7+
2 usage error or file not found
8+
"""
9+
10+
import argparse
11+
import shutil
12+
import subprocess
13+
import sys
14+
from pathlib import Path
15+
16+
try:
17+
from lxml import etree, isoschematron
18+
_LXML = True
19+
except ImportError:
20+
_LXML = False
21+
22+
_SCRIPT_DIR = Path(__file__).resolve().parent
23+
_DEFAULT_XSD = _SCRIPT_DIR / "generated" / "bt4.xsd"
24+
_DEFAULT_SCH = _SCRIPT_DIR / "generated" / "bt4.sch"
25+
26+
27+
def validate_xsd(xml_file: str, schema_file: str, force_lxml: bool = False) -> bool:
28+
if not force_lxml and shutil.which("xmllint"):
29+
result = subprocess.run(
30+
["xmllint", "--noout", "--schema", schema_file, xml_file],
31+
capture_output=True, text=True,
32+
)
33+
if result.returncode != 0:
34+
sys.stderr.write(result.stderr)
35+
return False
36+
return True
37+
38+
if not _LXML:
39+
print("error: neither xmllint nor lxml is available for XSD validation", file=sys.stderr)
40+
sys.exit(2)
41+
42+
schema = etree.XMLSchema(etree.parse(schema_file))
43+
doc = etree.parse(xml_file)
44+
if not schema.validate(doc):
45+
for err in schema.error_log:
46+
print(f"{xml_file}:{err.line}: {err.message}", file=sys.stderr)
47+
return False
48+
return True
49+
50+
51+
def validate_schematron(xml_file: str, sch_file: str) -> bool:
52+
if not _LXML:
53+
print("error: lxml is required for Schematron validation (pip install lxml)", file=sys.stderr)
54+
sys.exit(2)
55+
56+
sch = isoschematron.Schematron(etree.parse(sch_file), store_report=True)
57+
ok = sch.validate(etree.parse(xml_file))
58+
if not ok:
59+
ns = "http://purl.oclc.org/dsdl/svrl"
60+
for fa in sch.validation_report.findall(f".//{{{ns}}}failed-assert"):
61+
loc = fa.get("location")
62+
text = fa.findtext(f"{{{ns}}}text", "").strip()
63+
print(f"{xml_file}: {loc}: {text}", file=sys.stderr)
64+
return ok
65+
66+
67+
def main():
68+
parser = argparse.ArgumentParser(
69+
description="Validate a BehaviorTree.CPP XML file against XSD and Schematron schemas.",
70+
formatter_class=argparse.RawDescriptionHelpFormatter,
71+
epilog="""
72+
examples:
73+
%(prog)s myfile.xml
74+
%(prog)s --schema custom.xsd --schematron custom.sch myfile.xml
75+
%(prog)s --no-xsd myfile.xml
76+
%(prog)s --no-sch myfile.xml
77+
""",
78+
)
79+
parser.add_argument("xml_file", help="BehaviorTree XML file to validate")
80+
parser.add_argument(
81+
"-s", "--schema",
82+
default=str(_DEFAULT_XSD),
83+
metavar="FILE",
84+
help=f"XSD schema (default: {_DEFAULT_XSD})",
85+
)
86+
parser.add_argument(
87+
"-t", "--schematron",
88+
default=str(_DEFAULT_SCH),
89+
metavar="FILE",
90+
help=f"Schematron schema (default: {_DEFAULT_SCH})",
91+
)
92+
parser.add_argument("--no-xsd", action="store_true", help="skip XSD validation")
93+
parser.add_argument("--no-sch", action="store_true", help="skip Schematron validation")
94+
parser.add_argument("--lxml", action="store_true", help="force use of lxml for XSD validation")
95+
96+
args = parser.parse_args()
97+
98+
if not Path(args.xml_file).exists():
99+
parser.error(f"file not found: {args.xml_file}")
100+
101+
passed = True
102+
103+
if not args.no_xsd:
104+
if not Path(args.schema).exists():
105+
parser.error(f"XSD schema not found: {args.schema}")
106+
if not validate_xsd(args.xml_file, args.schema, force_lxml=args.lxml):
107+
passed = False
108+
109+
if not args.no_sch:
110+
if not Path(args.schematron).exists():
111+
parser.error(f"Schematron schema not found: {args.schematron}")
112+
if not validate_schematron(args.xml_file, args.schematron):
113+
passed = False
114+
115+
if passed:
116+
print(f"{args.xml_file} validates OK")
117+
sys.exit(0 if passed else 1)
118+
119+
120+
if __name__ == "__main__":
121+
main()

0 commit comments

Comments
 (0)