Skip to content

Commit 7b57063

Browse files
schema: add a python tool to manage schema files
Add a tool can be run by either a developer or a CI to ensure the schema files are sync'ed and/or update the generated files. Signed-off-by: John Mulligan <[email protected]>
1 parent a71ee57 commit 7b57063

File tree

1 file changed

+169
-0
lines changed

1 file changed

+169
-0
lines changed

sambacc/schema/tool.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#!/usr/bin/python3
2+
"""Convert or compare schema files written in YAML to the corresponding
3+
files stored in JSON.
4+
"""
5+
6+
import argparse
7+
import collections
8+
import json
9+
import os
10+
import pprint
11+
import subprocess
12+
import sys
13+
14+
import yaml
15+
16+
17+
nameparts = collections.namedtuple("nameparts", "full head ext")
18+
filepair = collections.namedtuple("filepair", "origin dest format")
19+
20+
21+
def _namesplit(name):
22+
head, ext = name.split(".", 1)
23+
return nameparts(name, head, ext)
24+
25+
26+
def _pyname(np):
27+
head = np.head.replace("-", "_")
28+
if np.ext.startswith("schema."):
29+
head += "_schema"
30+
ext = "py"
31+
return nameparts(f"{head}.{ext}", head, ext)
32+
33+
34+
def _format_black(path):
35+
# black does not formally have an api. Safeset way to use it is via
36+
# the cli
37+
path = os.path.abspath(path)
38+
# the --preview arg allows black to break up long strings that
39+
# the general check would discover and complain about. Otherwise
40+
# we'd be forced to ignore the formatting on these .py files.
41+
subprocess.run(["black", "-l78", "--preview", path], check=True)
42+
43+
44+
def match(files):
45+
yamls = []
46+
jsons = []
47+
pys = []
48+
for fname in files:
49+
try:
50+
np = _namesplit(fname)
51+
except ValueError:
52+
continue
53+
if np.ext == "schema.yaml":
54+
yamls.append(np)
55+
if np.ext == "schema.json":
56+
jsons.append(np)
57+
if np.ext == "py":
58+
pys.append(np)
59+
pairs = []
60+
for yname in yamls:
61+
for jname in jsons:
62+
if jname.head == yname.head:
63+
pairs.append(filepair(yname, jname, "JSON"))
64+
break
65+
else:
66+
pairs.append(filepair(yname, None, "JSON"))
67+
for pyname in pys:
68+
if _pyname(yname).head == pyname.head:
69+
pairs.append(filepair(yname, pyname, "PYTHON"))
70+
break
71+
else:
72+
pairs.append(filepair(yname, None, "PYTHON"))
73+
return pairs
74+
75+
76+
def report(func, path, yaml_file, json_file, fmt):
77+
needs_update = func(path, yaml_file, json_file, fmt)
78+
json_name = "---" if not json_file else json_file.full
79+
if not needs_update:
80+
print(f"{yaml_file.full} -> {fmt.lower()}:{json_name} OK")
81+
return None
82+
print(f"{yaml_file.full} -> {fmt.lower()}:{json_name} MISMATCH")
83+
return True
84+
85+
86+
def update_json(path, yaml_file, json_file):
87+
yaml_path = os.path.join(path, yaml_file.full)
88+
json_path = os.path.join(path, f"{yaml_file.head}.schema.json")
89+
with open(yaml_path) as fh:
90+
yaml_data = yaml.safe_load(fh)
91+
with open(json_path, "w") as fh:
92+
json.dump(yaml_data, fh, indent=2)
93+
94+
95+
def compare_json(path, yaml_file, json_file):
96+
if json_file is None:
97+
return True
98+
yaml_path = os.path.join(path, yaml_file.full)
99+
json_path = os.path.join(path, json_file.full)
100+
with open(yaml_path) as fh:
101+
yaml_data = yaml.safe_load(fh)
102+
with open(json_path) as fh:
103+
json_data = json.load(fh)
104+
return yaml_data != json_data
105+
106+
107+
def update_py(path, yaml_file, py_file):
108+
yaml_path = os.path.join(path, yaml_file.full)
109+
py_path = os.path.join(path, _pyname(yaml_file).full)
110+
with open(yaml_path) as fh:
111+
yaml_data = yaml.safe_load(fh)
112+
out = []
113+
out.append("#!/usr/bin/python3")
114+
out.append("# --- GENERATED FILE --- DO NOT EDIT --- #")
115+
out.append(f"# --- generated from: {yaml_file.full}")
116+
out.append("")
117+
out.append(
118+
"SCHEMA = " + pprint.pformat(yaml_data, width=800, sort_dicts=False)
119+
)
120+
content = "\n".join(out)
121+
with open(py_path, "w") as fh:
122+
fh.write(content)
123+
_format_black(py_path)
124+
125+
126+
def compare_py(path, yaml_file, py_file):
127+
if py_file is None:
128+
return True
129+
yaml_path = os.path.join(path, yaml_file.full)
130+
py_path = os.path.join(path, py_file.full)
131+
with open(yaml_path) as fh:
132+
yaml_data = yaml.safe_load(fh)
133+
with open(py_path) as fh:
134+
py_locals = {}
135+
exec(fh.read(), None, py_locals)
136+
py_data = py_locals.get("SCHEMA") or {}
137+
return yaml_data != py_data
138+
139+
140+
def update(path, yaml_data, other_file, fmt):
141+
if fmt == "PYTHON":
142+
return update_py(path, yaml_data, other_file)
143+
return update_json(path, yaml_data, other_file)
144+
145+
146+
def compare(path, yaml_data, other_file, fmt):
147+
if fmt == "PYTHON":
148+
return compare_py(path, yaml_data, other_file)
149+
return compare_json(path, yaml_data, other_file)
150+
151+
152+
def main():
153+
parser = argparse.ArgumentParser()
154+
parser.add_argument("DIR", default=os.path.dirname(__file__), nargs="?")
155+
parser.add_argument("--update", action="store_true")
156+
cli = parser.parse_args()
157+
158+
mismatches = []
159+
os.chdir(cli.DIR)
160+
fn = update if cli.update else compare
161+
pairs = match(os.listdir("."))
162+
for pair in pairs:
163+
mismatches.append(report(fn, ".", *pair))
164+
if any(mismatches):
165+
sys.exit(1)
166+
167+
168+
if __name__ == "__main__":
169+
main()

0 commit comments

Comments
 (0)