Skip to content

Commit c9f6ea0

Browse files
Fix v0.9 basic catalog examples and add v0.8 versions (google#762)
1 parent bc60657 commit c9f6ea0

47 files changed

Lines changed: 7623 additions & 55 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: Validate A2UI Specification Examples
16+
17+
on:
18+
push:
19+
branches: [ main ]
20+
paths:
21+
- 'specification/**/json/**'
22+
- '.github/workflows/validate_specifications.yml'
23+
- 'specification/scripts/validate.py'
24+
pull_request:
25+
paths:
26+
- 'specification/**/json/**'
27+
- '.github/workflows/validate_specifications.yml'
28+
- 'specification/scripts/validate.py'
29+
30+
jobs:
31+
validate:
32+
runs-on: ubuntu-latest
33+
34+
steps:
35+
- uses: actions/checkout@v4
36+
37+
- name: Install pnpm
38+
uses: pnpm/action-setup@v4
39+
with:
40+
version: 10
41+
42+
- name: Set up Node.js
43+
uses: actions/setup-node@v4
44+
with:
45+
node-version: '20'
46+
47+
- name: Set up Python
48+
uses: actions/setup-python@v5
49+
with:
50+
python-version: '3.12'
51+
52+
- name: Install dependencies
53+
working-directory: ./specification/v0_9/test
54+
run: pnpm install
55+
56+
- name: Run validation script
57+
run: python3 specification/scripts/validate.py

specification/scripts/validate.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import json
5+
import subprocess
6+
import glob
7+
import sys
8+
import shutil
9+
10+
def run_ajv(schema_path, data_paths, refs=None):
11+
"""Runs ajv validate via subprocess. Batch validates multiple data paths."""
12+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
13+
# Try to find local ajv in specification/v0_9/test first
14+
local_ajv = os.path.join(repo_root, "specification", "v0_9", "test", "node_modules", ".bin", "ajv")
15+
16+
if os.path.exists(local_ajv):
17+
cmd = [local_ajv, "validate", "-s", schema_path, "--spec=draft2020", "--strict=false", "-c", "ajv-formats"]
18+
else:
19+
# Fallback to pnpm dlx with both packages
20+
cmd = ["pnpm", "dlx", "--package=ajv-cli", "--package=ajv-formats", "ajv", "validate", "-s", schema_path, "--spec=draft2020", "--strict=false", "-c", "ajv-formats"]
21+
22+
if refs:
23+
for ref in refs:
24+
cmd.extend(["-r", ref])
25+
26+
for data_path in data_paths:
27+
cmd.extend(["-d", data_path])
28+
29+
result = subprocess.run(cmd, capture_output=True, text=True)
30+
return result.returncode == 0, result.stdout + result.stderr
31+
32+
def validate_messages(root_schema, example_files, refs=None, temp_dir="temp_val"):
33+
"""Validates a list of JSON files where each file contains a list of messages."""
34+
os.makedirs(temp_dir, exist_ok=True)
35+
success = True
36+
37+
for example_file in sorted(example_files):
38+
print(f" Validating {os.path.basename(example_file)}...")
39+
with open(example_file, 'r') as f:
40+
try:
41+
messages = json.load(f)
42+
except json.JSONDecodeError as e:
43+
print(f" [FAIL] Invalid JSON: {e}")
44+
success = False
45+
continue
46+
47+
if not isinstance(messages, list):
48+
messages = [messages]
49+
50+
temp_data_paths = []
51+
for i, msg in enumerate(messages):
52+
temp_data_path = os.path.join(temp_dir, f"msg_{os.path.basename(example_file)}_{i}.json")
53+
with open(temp_data_path, 'w') as f:
54+
json.dump(msg, f)
55+
temp_data_paths.append(temp_data_path)
56+
57+
if not temp_data_paths:
58+
print(" [SKIP] No messages to validate")
59+
continue
60+
61+
is_valid, output = run_ajv(root_schema, temp_data_paths, refs)
62+
if not is_valid:
63+
print(f" [FAIL] Validation failed for {os.path.basename(example_file)}:")
64+
print(output.strip())
65+
success = False
66+
else:
67+
print(f" [PASS]")
68+
69+
return success
70+
71+
def main():
72+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
73+
74+
overall_success = True
75+
76+
# Configuration for versions
77+
configs = {
78+
"v0_8": {
79+
"root_schema": "specification/v0_8/json/server_to_client_with_standard_catalog.json",
80+
"refs": [],
81+
"examples": "specification/v0_8/json/catalogs/basic/examples/*.json"
82+
},
83+
"v0_9": {
84+
"root_schema": "specification/v0_9/json/server_to_client.json",
85+
"refs": [
86+
"specification/v0_9/json/common_types.json",
87+
"specification/v0_9/json/basic_catalog.json"
88+
],
89+
"examples": "specification/v0_9/json/catalogs/basic/examples/*.json"
90+
}
91+
}
92+
93+
for version, config in configs.items():
94+
print(f"\n=== Validating {version} ===")
95+
96+
version_temp_dir = os.path.join(repo_root, f"temp_val_{version}")
97+
if os.path.exists(version_temp_dir):
98+
shutil.rmtree(version_temp_dir)
99+
os.makedirs(version_temp_dir, exist_ok=True)
100+
101+
root_schema = os.path.join(repo_root, config["root_schema"])
102+
if not os.path.exists(root_schema):
103+
print(f"Error: Root schema not found at {root_schema}")
104+
overall_success = False
105+
continue
106+
107+
refs = []
108+
for ref in config["refs"]:
109+
ref_path = os.path.join(repo_root, ref)
110+
if "basic_catalog.json" in ref:
111+
# v0.9 basic_catalog needs aliasing to catalog.json as expected by server_to_client.json
112+
with open(ref_path, 'r') as f:
113+
catalog = json.load(f)
114+
if "$id" in catalog:
115+
catalog["$id"] = catalog["$id"].replace("basic_catalog.json", "catalog.json")
116+
alias_path = os.path.join(version_temp_dir, "catalog.json")
117+
with open(alias_path, 'w') as f:
118+
json.dump(catalog, f)
119+
refs.append(alias_path)
120+
else:
121+
refs.append(ref_path)
122+
123+
example_pattern = os.path.join(repo_root, config["examples"])
124+
example_files = glob.glob(example_pattern)
125+
126+
if not example_files:
127+
print(f"No examples found for {version} matching {example_pattern}")
128+
else:
129+
if not validate_messages(root_schema, example_files, refs, version_temp_dir):
130+
overall_success = False
131+
132+
if os.path.exists(version_temp_dir):
133+
shutil.rmtree(version_temp_dir)
134+
135+
if not overall_success:
136+
print("\nOverall Validation: FAILED")
137+
sys.exit(1)
138+
else:
139+
print("\nOverall Validation: PASSED")
140+
141+
if __name__ == "__main__":
142+
main()

0 commit comments

Comments
 (0)