#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2026 Coffey Labs # SPDX-License-Identifier: AGPL-3.0-or-later """Tell INBUXA Admin what each expression field accepts. Every expression field in the registry has a context: the constants it may evaluate to (DKIM verification: relaxed, strict or disable) and the variables its conditions may read (sender_domain, local_port...). The server enforces both, but the schema it serves the admin describes every expression field as only an `x:Expression` object, so the admin can offer nothing better than a free-text box. This reads those contexts from the generated registry code and writes them into the served schema, on each expression field's type: "type": {"type": "object", "objectName": "x:Expression", "expression": {"constants": ["relaxed", "strict", "disable"], "variables": ["sender", "sender_domain", ...]}} The registry code is the source, so re-run this after anything that regenerates it (an upstream import, a new expression field). `--check` exits 1 when the schema is out of date; CI runs it. """ import argparse import base64 import gzip import hashlib import json import re import sys from pathlib import Path root = Path(__file__).resolve().parents[2] REGISTRY = root / 'crates' / 'registry' / 'src' / 'schema' SCHEMA = root / 'resources' / 'schema' / 'schema.json.gz' SCHEMA_HASH = root / 'resources' / 'schema' / 'schema.json.sha256' def names(enum, text): """Variant → wire name, from the `Enum::Variant => "name"` arms.""" return dict(re.findall(rf'{enum}::(\w+) => "([^"]+)"', text)) def lists(text): """Every `pub static NAME: &[ExpressionConstant|Variable] = &[...]`.""" out = {} for name, kind, body in re.findall( r'pub static (\w+): &\[(ExpressionConstant|ExpressionVariable)\] = &\[(.*?)\];', text, re.S ): out[name] = (kind, re.findall(rf'{kind}::(\w+)', body)) return out def contexts(text): """(struct, Property variant, variables list name, constants list name) per context.""" out = [] for block in re.finditer(r'(?m)^impl (\w+) \{(.*?)^\}', text, re.S): struct, body = block.group(1), block.group(2) for ctx in re.finditer(r'ExpressionContext \{(.*?)\n\s*\}\n', body, re.S): fields = ctx.group(1) prop = re.search(r'property: Property::(\w+),', fields) var = re.search(r'allowed_variables: (&\[\]|\w+),', fields) const = re.search(r'allowed_constants: (&\[\]|\w+),', fields) if prop and var and const: out.append((struct, prop.group(1), var.group(1), const.group(1))) return out def build(): enums = (REGISTRY / 'enums.rs').read_text(encoding='utf-8') enums_impl = (REGISTRY / 'enums_impl.rs').read_text(encoding='utf-8') props = names('Property', (REGISTRY / 'properties_impl.rs').read_text(encoding='utf-8')) const_names = names('ExpressionConstant', enums_impl) var_names = names('ExpressionVariable', enums_impl) known = lists(enums) def resolve(ref, kind, wire): if ref == '&[]': return [] found = known.get(ref) if not found or found[0] != kind: raise SystemExit(f'expr-schema: no {kind} list named {ref}') return [wire[v] for v in found[1]] table = {} for struct, prop, var, const in contexts((REGISTRY / 'structs_impl.rs').read_text(encoding='utf-8')): table[(f'x:{struct}', props[prop])] = { 'constants': resolve(const, 'ExpressionConstant', const_names), 'variables': resolve(var, 'ExpressionVariable', var_names), } return table def apply(schema, table): """Write the table into the schema; returns the (object, field) pairs it couldn't place.""" missing = [] for (obj, field), expr in sorted(table.items()): target = schema['fields'].get(obj, {}).get('properties', {}).get(field) if target is None or target['type'].get('objectName') != 'x:Expression': missing.append(f'{obj}.{field}') continue target['type']['expression'] = expr return missing def encode(schema): text = json.dumps(schema, ensure_ascii=False, separators=(',', ':')) out = gzip.compress(text.encode('utf-8'), compresslevel=9, mtime=0) digest = base64.urlsafe_b64encode(hashlib.sha256(out).digest()).decode().rstrip('=') return out, digest def main(): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument('--check', action='store_true', help='exit 1 if the schema is out of date') args = parser.parse_args() before = SCHEMA.read_bytes() schema = json.loads(gzip.decompress(before)) table = build() missing = apply(schema, table) if missing: print('expr-schema: expression contexts with no matching schema field:', file=sys.stderr) for m in missing: print(f' {m}', file=sys.stderr) return 1 current = json.loads(gzip.decompress(before)) if current == schema: print(f'expr-schema: {len(table)} expression fields, schema up to date') return 0 if args.check: print('expr-schema: schema is out of date; run tools/fork/expr-schema.py', file=sys.stderr) return 1 out, digest = encode(schema) SCHEMA.write_bytes(out) SCHEMA_HASH.write_text(digest, encoding='utf-8') print(f'expr-schema: wrote {len(table)} expression fields into {SCHEMA.relative_to(root)}') return 0 if __name__ == '__main__': sys.exit(main())