Compare commits
1
Commits
v2026.9.30
...
820ef5df2c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
820ef5df2c |
@@ -44,6 +44,10 @@ jobs:
|
||||
# schema has, and name nothing that is gone.
|
||||
- if: always()
|
||||
run: python3 tools/fork/privacy-check.py
|
||||
# The admin reads each expression field's allowed values and variables
|
||||
# from the schema; they're generated from the registry and must match it.
|
||||
- if: always()
|
||||
run: python3 tools/fork/expr-schema.py --check
|
||||
- if: always()
|
||||
run: python3 -m unittest discover -s tools/fork/tests
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
8Wyl9buv_eSbbGGhV-UZcjGHG_H77eorpuoI2u6wgnY
|
||||
r0pqQlntxFWW4X3bTLq57ObxRipXJXdzjsL9cyzz2Bo
|
||||
@@ -112,3 +112,18 @@ a fresh copy for each one. See `docs/spec/compat-tests.md`.
|
||||
tools/fork/run-compat.sh --store /srv/inbuxa-copy/rocks.db \
|
||||
--admin '[email protected]:PASSWORD' --recordings ~/compat
|
||||
```
|
||||
|
||||
## expr-schema.py
|
||||
|
||||
Writes each expression field's allowed constants and variables, read from the
|
||||
generated registry code, into the schema the server serves INBUXA Admin
|
||||
(`resources/schema/schema.json.gz` and its checksum). The admin uses them to
|
||||
offer plain choices instead of a free-text box.
|
||||
|
||||
```bash
|
||||
tools/fork/expr-schema.py # update the schema
|
||||
tools/fork/expr-schema.py --check # exit 1 if it's out of date (CI)
|
||||
```
|
||||
|
||||
Re-run it after anything that regenerates the registry, an upstream import
|
||||
included.
|
||||
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/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())
|
||||
Binary file not shown.
Reference in New Issue
Block a user