Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e61a475859 |
@@ -44,6 +44,10 @@ jobs:
|
|||||||
# schema has, and name nothing that is gone.
|
# schema has, and name nothing that is gone.
|
||||||
- if: always()
|
- if: always()
|
||||||
run: python3 tools/fork/privacy-check.py
|
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()
|
- if: always()
|
||||||
run: python3 -m unittest discover -s tools/fork/tests
|
run: python3 -m unittest discover -s tools/fork/tests
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ fn legacy_setting(name: &str, is_set: impl Fn(&str) -> bool) -> Option<String> {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! brand_version {
|
macro_rules! brand_version {
|
||||||
() => {
|
() => {
|
||||||
"2026.9.28.4"
|
"2026.9.28.3"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
|||||||
wDJZ1KdKs21tjHD-UBI9R_XwPEET7Iul8XEXbPlgBPE
|
k496pjVWlQ2p4bkZCh8agzaCKMLD4c3Z9WtoxpckYDU
|
||||||
@@ -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 \
|
tools/fork/run-compat.sh --store /srv/inbuxa-copy/rocks.db \
|
||||||
--admin '[email protected]:PASSWORD' --recordings ~/compat
|
--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())
|
||||||
Reference in New Issue
Block a user