Files
inbuxa-server/tools/registry-gen/generate.py
jcoffey-dev 16af21c154
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Canceled after 10m59s
Merge upstream v1.0.0
2026-10-27 14:39:45 -07:00

1678 lines
64 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Coffey Labs LLC
# SPDX-License-Identifier: AGPL-3.0-only
"""
Generate INBUXA's registry code from the public schema file.
python3 generate.py --edition agpl --out out/
python3 generate.py --edition full --out out/
Reads resources/schema/schema.json.gz (and, for the agpl edition,
resources/schema/schema_oss.json.gz, which is what tells open enum variants
from Enterprise ones) and writes:
crates/registry/src/schema/{enums,enums_impl,properties,properties_impl,
structs,structs_impl}.rs
Everything the schema does not carry (field declaration order, index
definitions, expression contexts, and so on) lives in special_cases.py.
The output is formatted with `rustfmt --edition 2024` (rustfmt 1.x), which
must be on PATH. In the agpl edition, Enterprise content is first emitted
between marker comments, formatted together with the open code, and then
removed line by line; this is the same process that produced the reference
files, so the layout artifacts it leaves behind come out identical.
"""
import argparse
import gzip
import json
import os
import re
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import special_cases as SC # noqa: E402
HEADER = """/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
// This file is auto-generated. Do not edit directly.
"""
# The full edition is ours: it carries the fork's notice line as well.
MODIFIED = """ *
* Modified by Coffey Labs in 2026 for INBUXA.
"""
# Marker comments around Enterprise-only lines (agpl edition only).
MARK_BEGIN = '// @@ENTERPRISE-BEGIN'
MARK_END = '// @@ENTERPRISE-END'
CFG_ENT = '#[cfg(feature = "enterprise")]'
CFG_NOT_ENT = '#[cfg(not(feature = "enterprise"))]'
# ---------------------------------------------------------------------------
# Naming
# ---------------------------------------------------------------------------
def variant_ident(name):
"""Schema enum value / variant name -> Rust identifier."""
name = SC.IDENT_SOURCE_NAMES.get(name, name)
name = name.replace('+', 'Plus')
name = re.sub(r'(?<=GMT)-(?=\d)', 'Minus', name)
parts = [p for p in re.split(r'[^A-Za-z0-9]+', name) if p]
ident = ''.join(p[0].upper() + p[1:] for p in parts)
if ident[:1].isdigit():
ident = 'V' + ident
return ident
RUST_KEYWORDS = set('''as async await break const continue crate dyn else enum extern false fn
for if impl in let loop match mod move mut pub ref return self static struct super trait true
type unsafe use where while abstract become box do final macro override priv typeof unsized
virtual yield try gen'''.split())
def snake(name):
"""camelCase property name -> snake_case Rust field name."""
s = re.sub(r'([A-Z])', r'_\1', name).lower()
return s + '_' if s in RUST_KEYWORDS else s
def pascal(name):
"""camelCase property name -> PascalCase `Property` variant."""
name = name.lstrip('@')
return SC.PROPERTY_IDENTS.get(name, name[:1].upper() + name[1:])
def strip_x(name):
return name[2:] if name.startswith('x:') else name
# ---------------------------------------------------------------------------
# Schema model
# ---------------------------------------------------------------------------
def load_gz_json(path):
return json.loads(gzip.decompress(Path(path).read_bytes()))
class Schema:
def __init__(self, full, oss, edition):
self.d = full
self.oss = oss
self.edition = edition
self.agpl = edition == 'agpl'
# ---- Enterprise classification (agpl only) ----
def ent_enum(self, name):
return self.oss is not None and name not in self.oss['enums']
def oss_variant_names(self, enum):
names = [v['name'] for v in self.oss['enums'][enum]]
return {SC.OSS_NAME_RENAMES.get(n, n) for n in names}
def ent_variant(self, enum, name):
if self.oss is None:
return False
if enum not in self.oss['enums']:
return True
return name not in self.oss_variant_names(enum)
def ent_schema(self, name):
return self.oss is not None and name not in self.oss['schemas']
def ent_object(self, name):
return self.oss is not None and name not in self.oss['objects']
def prepare(self):
d = self.d
# x: schemas, alphabetical (schema.json order)
self.schemas = [k for k in d['schemas'] if k.startswith('x:')]
self.fields = {}
for s in self.schemas:
if s not in d['fields']:
continue
props = d['fields'][s]['properties']
order = SC.FIELD_ORDER.get(s)
if order is None:
order = list(props)
print(f'warning: {s} has no FIELD_ORDER entry; using schema order', file=sys.stderr)
missing = [p for p in props if p not in order]
if missing:
print(f'warning: {s}: {missing} not in FIELD_ORDER; appended', file=sys.stderr)
order = list(order) + missing
defaults = d['fields'][s].get('defaults', {})
virtual = SC.VIRTUAL_FIELDS.get(s, [])
self.fields[s] = [Field(self, s, n, props[n], defaults, n in virtual)
for n in order if n in props]
# Property ids: first sighting, walking schemas alphabetically.
self.prop_ids = {'@type': 0, 'id': 1, 'text': 2}
for s in sorted(self.fields):
for f in self.fields[s]:
self.prop_ids.setdefault(f.name, len(self.prop_ids))
# Properties used only by Enterprise fields.
used_open = {'@type', 'id', 'text'}
for s, fs in self.fields.items():
if self.ent_schema(s):
continue
for f in fs:
if not f.ent:
used_open.add(f.name)
self.ent_props = {n for n in self.prop_ids if n not in used_open}
# Registry objects: x: objects and singletons, alphabetical.
self.objects = [k for k, v in d['objects'].items()
if k.startswith('x:') and v['type'] in ('object', 'singleton')]
def schema_fields(self, s, include_ent=None):
"""Fields of a schema for this edition (all fields in full mode)."""
return self.fields.get(s, [])
def ent_field(self, schema, field):
if self.oss is None:
return False
if self.d['fields'][schema]['properties'][field].get('enterprise'):
return True
of = self.oss['fields'].get(schema)
return of is None or field not in of['properties']
class Field:
def __init__(self, schema, sname, name, prop, defaults, virtual):
self.s = schema
self.schema = sname
self.name = name
self.prop = prop
self.t = prop['type']
self.update = prop['update']
self.virtual = virtual
self.has_default = name in defaults
self.default = defaults.get(name)
self.ent = schema.ent_field(sname, name)
self.rust = snake(name)
self.pident = pascal(name)
self.nullable = bool(self.t.get('nullable'))
# ---------------------------------------------------------------------------
# Line buffer with Enterprise regions
# ---------------------------------------------------------------------------
class Out:
def __init__(self, schema):
self.s = schema
self.lines = []
def __call__(self, *lines):
for l in lines:
self.lines.extend(l.split('\n'))
def ent(self, lines):
"""Lines that only exist in the Enterprise build."""
if isinstance(lines, str):
lines = lines.split('\n')
if self.s.agpl:
self.lines.append(MARK_BEGIN)
self.lines.extend(lines)
self.lines.append(MARK_END)
else:
self.lines.extend(lines)
def agpl_only(self, lines):
"""Lines that only exist in the open build (cfg(not(enterprise)) remnants)."""
if isinstance(lines, str):
lines = lines.split('\n')
if self.s.agpl:
self.lines.extend(lines)
def text(self):
return '\n'.join(self.lines) + '\n'
def strip_enterprise(text):
out, depth = [], 0
for line in text.split('\n'):
if MARK_BEGIN in line:
depth += 1
continue
if MARK_END in line:
depth -= 1
continue
if not depth:
out.append(line)
return '\n'.join(out)
def rustfmt(text):
r = subprocess.run(['rustfmt', '--edition', '2024', '--emit', 'stdout'],
input=text, capture_output=True, text=True, cwd='/')
if r.returncode != 0:
sys.stderr.write(r.stderr)
raise SystemExit('rustfmt failed')
return r.stdout
def finish(schema, body, fmt=True):
header = HEADER
if not schema.agpl:
header = header.replace(' */\n', MODIFIED + ' */\n', 1)
text = header + body
if os.environ.get('REGISTRY_GEN_RAW'):
Path(os.environ['REGISTRY_GEN_RAW']).write_text(text)
if fmt:
text = rustfmt(text)
if schema.agpl:
text = strip_enterprise(text).rstrip('\n') + '\n'
return text
# ---------------------------------------------------------------------------
# enums.rs / enums_impl.rs
# ---------------------------------------------------------------------------
def generated_enums(s):
for name in s.d['enums']:
if name in SC.EXTERNAL_ENUMS:
continue
yield name
# Variant enums schema.json lost to a name clash (see VARIANT_ENUM_NAMES).
yield from SC.VARIANT_ENUM_NAMES.values()
def enum_variants(s, name):
"""[(ident, schema name, id, enterprise)] in schema order; id = list index."""
for sch, ename in SC.VARIANT_ENUM_NAMES.items():
if ename == name:
ent = s.agpl and s.ent_schema(sch)
return [(variant_ident(v['name']), v['name'], i, ent)
for i, v in enumerate(s.d['schemas'][sch]['variants'])]
return [(variant_ident(v['name']), v['name'], i, s.ent_variant(name, v['name']))
for i, v in enumerate(s.d['enums'][name])]
def enum_is_ent(s, name):
for sch, ename in SC.VARIANT_ENUM_NAMES.items():
if ename == name:
return s.agpl and s.ent_schema(sch)
return s.agpl and s.ent_enum(name)
def gen_enums(s):
o = Out(s)
first = True
for name in generated_enums(s):
vs = enum_variants(s, name)
item = ['#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]',
'#[repr(u16)]', f'pub enum {name} {{']
body = []
first_open = next((i for i, v in enumerate(vs) if not v[3]), 0) if s.agpl else 0
for i, (ident, _, vid, ent) in enumerate(vs):
if s.agpl and ent:
attr = ['#[cfg_attr(feature = "enterprise", default)]'] if i == 0 else []
body.append(('ent', attr + [f'{ident} = {vid},']))
else:
if i == 0:
attr = ['#[default]']
elif s.agpl and i == first_open:
attr = ['#[cfg_attr(not(feature = "enterprise"), default)]']
else:
attr = []
body.append(('open', attr + [f'{ident} = {vid},']))
if enum_is_ent(s, name):
o.ent(item + flatten(body) + ['}', ''])
continue
if not first:
pass
o(*item)
emit_body(o, body)
o('}', '')
first = False
for name, ty, values in SC.EXPRESSION_STATICS:
o(f'pub static {name}: &[{ty}] = &[')
for v in values:
o(f' {ty}::{v},')
o('];', '')
return finish(s, o.text().rstrip('\n') + '\n')
def flatten(body):
out = []
for _, lines in body:
out.extend(lines)
return out
def emit_body(o, body):
for kind, lines in body:
if kind == 'ent':
o.ent(lines)
else:
o(*lines)
def gen_enums_impl(s):
o = Out(s)
o('use crate::schema::prelude::*;', '')
for name in generated_enums(s):
emit_enum_impl(o, s, name, enum_variants(s, name), enum_is_ent(s, name),
name in SC.ENUMS_WITH_ALL)
return finish(s, o.text().rstrip('\n') + '\n')
def emit_enum_impl(o, s, name, vs, whole_ent=False, with_all=False):
"""EnumImpl + serde impls. vs = [(ident, json name, id, enterprise)]."""
has_ent = s.agpl and any(v[3] for v in vs)
start = len(o.lines)
o(f'impl EnumImpl for {name} {{')
def parse_fn(variants, cfg):
# rustfmt may leave the macro body alone, so indent it as final.
lines = ([f' {cfg}'] if cfg else []) + [
' fn parse(value: &str) -> Option<Self> {',
' hashify::map! {',
' value.as_bytes(),',
f' {name},']
for ident, sname, _, _ in variants:
lines.append(f' b"{sname}" => {name}::{ident},')
lines += [' }', ' .copied()', ' }']
return lines
def arm(text, ent):
if ent and not whole_ent:
o.ent([text])
elif not ent or s.agpl or True:
o(text)
if has_ent and not whole_ent:
o(*parse_fn([v for v in vs if not v[3]], CFG_NOT_ENT))
o.ent(parse_fn(vs, CFG_ENT))
else:
o(*parse_fn(vs, None))
o('', 'fn as_str(&self) -> &\'static str {', ' match self {')
for ident, sname, _, ent in vs:
arm(f'{name}::{ident} => "{sname}",', ent)
o(' }', '}', '')
o('fn to_id(&self) -> u16 {', ' *self as u16', '}', '')
o('fn from_id(id: u16) -> Option<Self> {', ' match id {')
for ident, sname, vid, ent in vs:
arm(f'{vid} => Some({name}::{ident}),', ent)
o(' _ => None,', ' }', '}', '')
o(f'const COUNT: usize = {max(v[2] for v in vs) + 1};', '}', '')
if with_all:
o(f'impl {name} {{', f' pub const ALL: &[{name}] = &[')
for ident, sname, vid, ent in vs:
arm(f'{name}::{ident},', ent)
o(' ];', '}', '')
o(f'impl serde::Serialize for {name} {{',
' fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>',
' where',
' S: serde::Serializer,',
' {',
' serializer.serialize_str(self.as_str())',
' }',
'}', '',
f'impl<\'de> serde::Deserialize<\'de> for {name} {{',
' fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>',
' where',
' D: serde::Deserializer<\'de>,',
' {',
' let s = Cow::<str>::deserialize(deserializer)?;',
' Self::parse(&s).ok_or_else(|| serde::de::Error::unknown_variant(&s, &[]))',
' }',
'}', '')
if whole_ent:
block = o.lines[start:]
del o.lines[start:]
o.ent(block)
# ---------------------------------------------------------------------------
# properties.rs / properties_impl.rs
# ---------------------------------------------------------------------------
def object_variants(s):
return [(strip_x(n), strip_x(n), i, s.agpl and s.ent_object(n)) for i, n in enumerate(s.objects)]
def property_variants(s):
vs = [(pascal(n), n, i, s.agpl and n in s.ent_props) for n, i in s.prop_ids.items()]
return sorted(vs, key=lambda v: (v[1] != '@type', v[0]))
def gen_properties(s):
o = Out(s)
o('use crate::schema::prelude::*;')
o('#[derive(Debug, Clone, Serialize, PartialEq, Eq)]', '#[serde(untagged)]',
'pub enum ObjectInner {')
for ident, _, _, ent in object_variants(s):
line = f'{ident}(Box<{ident}>),'
o.ent([line]) if ent else o(line)
o('}', '')
o('#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]', '#[repr(u16)]', 'pub enum ObjectType {')
for ident, _, i, ent in object_variants(s):
line = f'{ident} = {i},'
o.ent([line]) if ent else o(line)
o('}', '')
o('#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]', '#[repr(u16)]', 'pub enum Property {')
for ident, _, i, ent in property_variants(s):
line = f'{ident} = {i},'
o.ent([line]) if ent else o(line)
o('}')
return finish(s, o.text())
# ---------------------------------------------------------------------------
# Rust types
# ---------------------------------------------------------------------------
STRING_FORMAT_TYPES = {
'ipAddress': 'IpAddr',
'ipNetwork': 'IpAddrOrMask',
'socketAddress': 'SocketAddr',
}
NUMBER_FORMAT_TYPES = {
'duration': 'Duration',
'unsignedInteger': 'u64',
'size': 'u64',
'integer': 'i64',
'float': 'Float',
}
def enum_type(name):
return SC.EXTERNAL_ENUMS.get(name) or name
def base_type(t):
"""Rust type of a schema type, ignoring nullability."""
k = t['type']
if k == 'string':
return STRING_FORMAT_TYPES.get(t.get('format'), 'String')
if k == 'number':
return NUMBER_FORMAT_TYPES[t['format']]
if k == 'boolean':
return 'bool'
if k == 'utcDateTime':
return 'UTCDateTime'
if k == 'enum':
return enum_type(t['enumName'])
if k == 'object':
return strip_x(t['objectName'])
if k == 'objectId':
return 'Id'
if k == 'blobId':
return 'BlobId'
if k == 'objectList':
return f"List<{strip_x(t['objectName'])}>"
if k == 'set':
return f"Map<{base_type(t['class'])}>"
if k == 'map':
return f"VecMap<{base_type(t['keyClass'])}, {base_type(t['valueClass'])}>"
raise ValueError(t)
def rust_type(f):
ty = SC.RUST_TYPE_OVERRIDES.get((f.schema, f.name)) or base_type(f.t)
return f'Option<{ty}>' if f.nullable else ty
def schema_kind(s, name):
return s.d['schemas'][name]['type']
def variants(s, name):
"""[(ident, json name, payload schema or None, enterprise)]"""
out = []
enum = None
for v in s.d['schemas'][name]['variants']:
out.append((variant_ident(v['name']), v['name'], v.get('schemaName'),
s.agpl and v.get('schemaName') is not None and s.ent_schema(v['schemaName'])))
return out
def variant_enum(name):
"""Name of the enum listing a multi-variant schema's variants."""
return SC.VARIANT_ENUM_NAMES.get(name, strip_x(name) + 'Type')
def edition_schemas(s):
"""x: schemas to generate, alphabetical, with their Enterprise flag."""
return [(n, s.agpl and s.ent_schema(n)) for n in s.schemas]
# ---------------------------------------------------------------------------
# structs.rs
# ---------------------------------------------------------------------------
def gen_structs(s):
o = Out(s)
o('use crate::schema::prelude::*;')
for name, ent in edition_schemas(s):
lines = ['#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]']
rn = strip_x(name)
body = []
if schema_kind(s, name) == 'multiple':
lines += ['#[serde(tag = "@type")]', f'pub enum {rn} {{']
for ident, jname, payload, vent in variants(s, name):
line = f'{ident}({strip_x(payload)}),' if payload else f'{ident},'
body.append(('ent' if vent else 'open', [line]))
else:
lines += ['#[serde(default)]', f'pub struct {rn} {{']
for f in s.fields[name]:
if f.virtual:
continue
fl = []
if f.rust != f.name:
fl.append(f'#[serde(rename = "{f.name}")]')
fl.append(f'pub {f.rust}: {rust_type(f)},')
body.append(('ent' if f.ent else 'open', fl))
if ent:
o.ent(lines + flatten(body) + ['}', ''])
else:
o(*lines)
emit_body(o, body)
o('}', '')
return finish(s, o.text().rstrip('\n') + '\n')
# ---------------------------------------------------------------------------
# Field helpers shared by structs_impl.rs and properties_impl.rs
# ---------------------------------------------------------------------------
def kind_of(t):
k = t['type']
if k == 'string':
return {'ipAddress': 'ip', 'ipNetwork': 'ipmask', 'socketAddress': 'socket'}.get(
t.get('format'), 'string')
if k == 'number':
return {'duration': 'duration', 'unsignedInteger': 'u64', 'size': 'u64',
'integer': 'i64', 'float': 'float'}[t['format']]
return {'boolean': 'bool', 'utcDateTime': 'datetime', 'enum': 'enum', 'object': 'object',
'objectId': 'id', 'blobId': 'blob', 'objectList': 'list', 'set': 'set',
'map': 'map'}[k]
def fkind(f):
ov = SC.RUST_TYPE_OVERRIDES.get((f.schema, f.name))
if ov == 'Id':
return 'id'
return kind_of(f.t)
def is_expr(t):
return t['type'] == 'object' and t.get('objectName') == 'x:Expression'
def is_secret(f):
return (f.t.get('format') in ('secret', 'secretText')
or (f.schema, f.name) in SC.MASKED_FIELDS)
def parse_duration(v):
"""Schema duration default -> milliseconds."""
if isinstance(v, (int, float)):
return int(v)
m = re.fullmatch(r'(\d+)\s*(ms|s|m|h|d|w)?', v.strip())
if not m:
raise ValueError(v)
n = int(m.group(1))
return n * {'ms': 1, None: 1, 's': 1000, 'm': 60000, 'h': 3600000,
'd': 86400000, 'w': 604800000}[m.group(2)]
def rust_str(v):
v = SC.STRING_LITERAL_RENAMES.get(v, v)
return '"' + v.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + '"'
def float_lit(v):
r = repr(float(v))
return r
def num_literal(t, v, f=None):
k = kind_of(t)
if k == 'duration':
return f'Duration::from_millis({parse_duration(v)})'
if k == 'float':
return f'Float::new({float_lit(v)}f64)'
if k == 'i64':
return f'{int(v)}i64'
if k == 'u64':
if t.get('format') == 'size' and (f is None or
(f.schema, f.name) not in SC.SIZE_DEFAULT_SUFFIX):
return f'{int(v)}'
return f'{int(v)}u64'
raise ValueError(t)
def schema_struct_fields(s, name):
"""Stored (non-virtual) fields of a single schema, in declaration order."""
return [f for f in s.fields[name] if not f.virtual]
def default_expr(s, f, t=None, value=None):
"""Rust expression for a field's default (None -> Default::default())."""
t = t or f.t
if value is None:
if not f.has_default:
return None
value = f.default
k = kind_of(t)
nullable = t.get('nullable')
inner = value_literal(s, t, value, f)
if inner is None:
return None
return f'Some({inner})' if nullable else inner
def value_literal(s, t, v, f=None):
k = kind_of(t)
if k == 'string':
if v is None:
return None
return f'{rust_str(v)}.to_string()'
if k in ('ip', 'ipmask', 'socket'):
return f'{base_type(t)}::from_str({rust_str(v)}).unwrap()'
if k in ('u64', 'i64', 'float', 'duration'):
return num_literal(t, v, f)
if k == 'bool':
return 'true' if v else 'false'
if k == 'enum':
return f"{enum_type(t['enumName'])}::{variant_ident(v)}"
if k == 'set':
keys = set_default_order(f, v)
if t['class']['type'] == 'enum' and t['class']['enumName'] in s.d['enums']:
order = [e['name'] for e in s.d['enums'][t['class']['enumName']]]
keys = sorted(keys, key=lambda x: order.index(x) if x in order else len(order))
items = [value_literal(s, t['class'], x) for x in keys]
return f"Map::new(vec![{', '.join(items)}])"
if k == 'object':
return object_literal(s, t['objectName'], v)
if k == 'list':
# objectList defaults are JSON objects keyed by position ("0", "1", ...).
vals = [v[k] for k in sorted(v, key=int)] if isinstance(v, dict) else v
items = [object_literal(s, t['objectName'], x) for x in vals]
return f"List::from_iter([{', '.join(items)}])"
if k == 'map':
items = [f"({value_literal(s, t['keyClass'], key)}, {value_literal(s, t['valueClass'], val)})"
for key, val in v.items()]
return f"VecMap::from_iter([{', '.join(items)}])"
raise ValueError((t, v))
def set_default_order(f, v):
keys = list(v) if isinstance(v, dict) else list(v)
if f is not None:
order = SC.SET_DEFAULT_ORDER.get((f.schema, f.name))
if order:
return order
return keys
def object_literal(s, oname, v):
name = strip_x(oname)
if oname == 'x:Expression':
return expression_literal(v)
sch = s.d['schemas'][oname]
if sch['type'] == 'multiple':
vname = v['@type']
var = next(x for x in sch['variants'] if x['name'] == vname)
ident = variant_ident(vname)
if not var.get('schemaName'):
return f'{name}::{ident}'
rest = {k: x for k, x in v.items() if k != '@type'}
if not rest:
return f'{name}::{ident}(Default::default())'
return f"{name}::{ident}({struct_literal(s, var['schemaName'], rest)})"
return struct_literal(s, oname, v)
def struct_literal(s, sname, v):
fields = schema_struct_fields(s, sname)
parts = []
for f in fields:
if f.name in v:
parts.append(f'{f.rust}: {default_expr(s, f, value=v[f.name])}')
if len(parts) < len(fields):
parts.append('..Default::default()')
return f"{strip_x(sname)} {{ {', '.join(parts)} }}"
def expression_literal(v):
"""Expression default: else_ first, then match_ when the key is present."""
if isinstance(v, str):
v = {'default': v}
parts = []
e = v.get('else', v.get('default'))
if e is not None:
parts.append(f'else_: {rust_str(e)}.to_string()')
if 'match' in v:
ms = ', '.join(f"ExpressionMatch {{ if_: {rust_str(m['if'])}.to_string(), "
f"then: {rust_str(m['then'])}.to_string() }}" for m in v['match'])
parts.append(f'match_: List::from_iter([{ms}])')
else:
parts.append('..Default::default()')
return f"Expression {{ {', '.join(parts)} }}"
# ---------------------------------------------------------------------------
# Derived model shared by structs_impl.rs and properties_impl.rs
# ---------------------------------------------------------------------------
def type_children(t):
"""Schema names referenced by a field type (objects, lists, sets, maps)."""
k = t['type']
if k in ('object', 'objectList'):
return [t['objectName']]
out = []
for c in ('class', 'keyClass', 'valueClass'):
if c in t:
out += type_children(t[c])
return out
class Model:
"""Facts computed over the full schema (Enterprise included)."""
def __init__(self, s):
self.s = s
d = s.d
self.object_set = set(s.objects)
self.perms = {v['name'] for v in d['enums']['Permission']}
# Types reachable from stored objects get a Pickle impl.
self.pickled = set()
stack = [o for o in s.objects if o not in SC.NOT_STORED_OBJECTS]
while stack:
n = stack.pop()
if n in self.pickled or n not in d['schemas']:
continue
self.pickled.add(n)
stack.extend(self.children(n))
# Types with an index function (fixed point).
self.indexable = set()
changed = True
while changed:
changed = False
for n in s.schemas:
if n not in self.indexable and self._indexable(n):
self.indexable.add(n)
changed = True
# Types with expression contexts (fixed point over multi variants).
self.has_ctx = {n for n in s.schemas if schema_kind(s, n) == 'single'
and any(self.ctx_of(f) for f in schema_struct_fields(s, n))}
changed = True
while changed:
changed = False
for n in s.schemas:
if n in self.has_ctx or schema_kind(s, n) != 'multiple':
continue
if any(v[2] in self.has_ctx for v in variants(s, n) if v[2]):
self.has_ctx.add(n)
changed = True
def children(self, n):
sch = self.s.d['schemas'][n]
if sch['type'] == 'multiple':
return [v['schemaName'] for v in sch['variants'] if v.get('schemaName')]
out = []
for f in schema_struct_fields(self.s, n):
out += type_children(f.t)
return out
def _indexable(self, n):
s = self.s
# Only stored types are indexed (an object's own index fn always exists).
if n not in self.pickled:
return False
if schema_kind(s, n) == 'multiple':
if n in SC.TYPE_INDEXED_OBJECTS:
return True
return any(v[2] in self.indexable for v in variants(s, n) if v[2])
for f in schema_struct_fields(s, n):
if index_ops(f) or fk_target(f) or self.nested_index(f):
return True
return False
def nested_index(self, f):
k = kind_of(f.t)
if k in ('object', 'list'):
return f.t['objectName'] in self.indexable
if k == 'map':
vc = f.t['valueClass']
return vc['type'] == 'object' and vc['objectName'] in self.indexable
return False
def ctx_of(self, f):
if not is_expr(f.t):
return None
return SC.EXPRESSION_CONTEXTS.get((f.schema, f.name))
def object_flags(self, n):
s = self.s
obj = s.d['objects'][n]
flags = []
singleton = obj['type'] == 'singleton'
if singleton:
flags.append('OBJ_SINGLETON')
var_fields = self.variant_field_names(n)
if var_fields and all('accountId' in fs for fs in var_fields):
flags.append('OBJ_FILTER_ACCOUNT')
if any('memberTenantId' in fs for fs in var_fields):
flags.append('OBJ_FILTER_TENANT')
if n in SC.SEQ_ID_OBJECTS:
flags.append('OBJ_SEQ_ID')
if n in SC.NOT_STORED_OBJECTS:
flags.append('OBJ_NOT_STORED')
prefix = obj['permissionPrefix']
ops = ['Get', 'Update'] if singleton else ['Get', 'Create', 'Update', 'Destroy', 'Query']
for op in ['Get', 'Create', 'Update', 'Destroy', 'Query']:
if op in ops and prefix + op not in self.perms:
flags.append('OBJ_NO_' + op.upper())
return ' | '.join(flags) or '0'
def variant_field_names(self, n):
s = self.s
if schema_kind(s, n) == 'multiple':
return [{f.name for f in s.fields.get(v[2], [])} if v[2] else set()
for v in variants(s, n)]
return [{f.name for f in s.fields.get(n, [])}]
def index_ops(f):
return SC.INDEX_OPS.get(f.schema, {}).get(f.name, [])
def fk_target(f):
"""(ObjectType ident, view filter or None) for objectId fields."""
t = f.t
while t['type'] == 'set':
t = t['class']
if t['type'] != 'objectId':
return None
obj = strip_x(t['objectName'])
if '/' in obj:
base, view = obj.split('/', 1)
return base, f'Some({base}Type::{variant_ident(view)}.to_id())'
return obj, 'None'
def is_set(f):
return f.t['type'] == 'set'
# ---------------------------------------------------------------------------
# structs_impl.rs
# ---------------------------------------------------------------------------
def P(f):
return f'Property::{f.pident}'
def scalar_checks(f, t, prop):
"""Validation lines for one value (`value: &T`)."""
k = kind_of(t)
ov = SC.RUST_TYPE_OVERRIDES.get((f.schema, f.name))
if ov == 'Id':
k = 'id'
elif ov and ov.startswith('Map<u64'):
return []
lines = []
if k == 'string':
lines += ['if value.is_empty() {', f' errors.push(ValidationError::required({prop}));', '}']
if 'maxLength' in t:
n = t['maxLength']
lines += [f'if value.len() > {n} {{',
f' errors.push(ValidationError::max_length({prop}, {n}));', '}']
elif k == 'blob':
lines += ['if value.is_empty() {', f' errors.push(ValidationError::required({prop}));', '}']
elif k == 'id' and ov != 'Id':
lines += ['if !value.is_valid() {', f' errors.push(ValidationError::required({prop}));', '}']
elif k in ('datetime', 'ip', 'ipmask', 'socket', 'id'):
lines += ['if !value.is_valid() {', f' errors.push(ValidationError::invalid({prop}, value));', '}']
elif k in ('u64', 'i64', 'float', 'duration'):
if k == 'duration' and ('min' in t or 'max' in t):
lines += ['if !value.is_valid() {', f' errors.push(ValidationError::invalid({prop}, value));', '}']
bounds = [('min', '<', 'min_value'), ('max', '>', 'max_value')]
if (f.schema, f.name) in SC.VALIDATE_MAX_FIRST:
bounds.reverse()
for key, op, fn in bounds:
if key in t:
lit, push = bound_literals(t, t[key])
lines += [f'if *value {op} {lit} {{',
f' errors.push(ValidationError::{fn}({prop}, {push}));', '}']
elif k == 'object':
if (f.schema, f.name) in SC.VALIDATE_IF_NOT_EMPTY:
lines += ['if !value.match_.is_empty() || !value.else_.is_empty() {',
' value.validate(errors);', '}']
else:
lines += ['value.validate(errors);']
return lines
def bound_literals(t, v):
k = kind_of(t)
if k == 'duration':
ms = parse_duration(v)
return f'Duration::from_millis({ms})', f'{ms}'
if k == 'float':
return f'Float::new({float_lit(v)})', f'{int(v)}' if float(v).is_integer() else f'{v}'
if k == 'i64':
return f'({int(v)})', f'{int(v)}'
return f'{int(v)}', f'{int(v)}'
def validate_lines(f):
t = f.t
k = kind_of(t)
prop = P(f)
if k == 'set':
inner = scalar_checks(f, t['class'], prop)
lines = (['for value in value.iter() {'] + indent(inner) + ['}']) if inner else []
if 'minItems' in t:
n = t['minItems']
lines += [f'if value.len() < {n} {{', f' errors.push(ValidationError::min_items({prop}, {n}));', '}']
elif k in ('list', 'map'):
inner_t = ({'type': 'object', 'objectName': t['objectName']} if k == 'list'
else t['valueClass'])
inner = scalar_checks(f, inner_t, prop)
lines = (['for value in value.values() {'] + indent(inner) + ['}']) if inner else []
if 'minItems' in t:
n = t['minItems']
lines += [f'if value.len() < {n} {{', f' errors.push(ValidationError::min_items({prop}, {n}));', '}']
else:
lines = scalar_checks(f, t, prop)
if not lines:
return []
if f.nullable:
return [f'if let Some(value) = &self.{f.rust} {{'] + indent(lines) + ['}']
return [f'let value = &self.{f.rust};'] + lines
def indent(lines, n=1):
return [(' ' * n + l) if l else l for l in lines]
def index_lines(m, f):
out = []
fk = fk_target(f)
if fk:
obj, view = fk
if is_set(f):
out += [f'for id in self.{f.rust}.iter() {{',
f' i.foreign_key(ObjectType::{obj}, Some(*id), {view});', '}']
elif f.nullable:
out += [f'i.foreign_key(ObjectType::{obj}, self.{f.rust}, {view});']
else:
out += [f'i.foreign_key(ObjectType::{obj}, self.{f.rust}.into(), {view});']
for op in index_ops(f):
kind = op[0]
if kind == 'composite':
_, cprop, how, other = op
arg = (f'IndexValue::U32(self.{other}.document_id())' if how == 'docid'
else f'&self.{other}')
out += [f'i.unique_global_composite(Property::{cprop}, &self.{f.rust}, {arg});']
continue
call = f'i.{kind}(Property::{op[1]}, '
if is_set(f):
out += [f'for value in self.{f.rust}.iter() {{', f' {call}value);', '}']
elif f.nullable:
out += [f'if let Some(value) = &self.{f.rust} {{', f' {call}value);', '}']
else:
out += [f'{call}&self.{f.rust});']
if m.nested_index(f):
k = kind_of(f.t)
if k == 'object':
if f.nullable:
out += [f'if let Some(value) = &self.{f.rust} {{', ' value.index(i);', '}']
else:
out += [f'self.{f.rust}.index(i);']
else:
out += [f'for item in self.{f.rust}.values() {{', ' item.index(i);', '}']
return out
def emit_fields(o, s, fields, fn):
"""Emit fn(f) lines for each field, Enterprise fields wrapped."""
any_open = False
for f in fields:
lines = fn(f)
if not lines:
continue
if f.ent:
o.ent(lines)
else:
any_open = True
o(*lines)
return any_open
def has_lines(fields, fn, ent=None):
return any(fn(f) for f in fields if ent is None or f.ent == ent)
def emit_validate_fn(o, s, fields):
if not any(validate_lines(f) for f in fields):
o('fn validate(&self, _: &mut Vec<ValidationError>) -> bool {', ' true', '}')
return
o('fn validate(&self, errors: &mut Vec<ValidationError>) -> bool {', ' let neb = errors.len();')
emit_fields(o, s, fields, validate_lines)
o(' errors.len() == neb', '}')
def emit_index_fn(o, m, s, fields, pub=False):
fn = lambda f: index_lines(m, f)
open_ = has_lines(fields, fn, False)
ent = has_lines(fields, fn, True)
if not open_ and not ent:
o("fn index<'x>(&'x self, _: &mut IndexBuilder<'x>) {}")
return
if not open_ and s.agpl:
o('#[cfg_attr(not(feature = "enterprise"), allow(unused_variables))]')
o("fn index<'x>(&'x self, i: &mut IndexBuilder<'x>) {")
emit_fields(o, s, fields, fn)
o('}')
def emit_trailing_ent(o, s, name):
if name in SC.TRAILING_ENTERPRISE_ITEM and s.agpl:
o('')
o.ent([])
def emit_single(o, m, s, name):
rn = strip_x(name)
fields = schema_struct_fields(s, name)
allf = s.fields[name]
is_obj = name in m.object_set
# 1. ObjectImpl / inherent validate (+ index)
if is_obj:
o(f'impl ObjectImpl for {rn} {{',
f' const FLAGS: u64 = {m.object_flags(name)};',
' const VERSION: u8 = 0;',
f' const OBJECT: ObjectType = ObjectType::{rn};', '')
emit_validate_fn(o, s, fields)
o('')
emit_index_fn(o, m, s, fields)
else:
o(f'impl {rn} {{')
emit_validate_fn(o, s, fields)
if name in m.indexable:
o('')
emit_index_fn(o, m, s, fields)
emit_trailing_ent(o, s, name)
o('}', '')
# 2. Expression contexts
ctxf = [f for f in fields if m.ctx_of(f)]
if ctxf:
o(f'impl {rn} {{')
for f in ctxf:
vars_, consts = m.ctx_of(f)
d = default_expr(s, f)
lines = [f'pub fn ctx_{f.rust}(&self) -> ExpressionContext<\'_> {{',
' ExpressionContext {',
f' expr: &self.{f.rust},',
f' default: {"Some(" + d + ")" if d else "None"},',
f' property: {P(f)},',
f' allowed_variables: {vars_ or "&[]"},',
f' allowed_constants: {consts or "&[]"},',
' }', '}', '']
o.ent(lines) if f.ent else o(*lines)
calls = []
o('pub fn expression_ctxs(&self) -> Vec<ExpressionContext<\'_>> {')
if any(f.ent for f in ctxf) and s.agpl:
# vec![] with Enterprise entries (formatted vertically).
o(' vec![')
for f in ctxf:
line = f' self.ctx_{f.rust}(),'
o.ent([line]) if f.ent else o(line)
o(' ]')
else:
o(' vec![' + ', '.join(f'self.ctx_{f.rust}()' for f in ctxf) + ']')
o('}', '}', '')
# 3. Pickle
if name in m.pickled:
emit_pickle_single(o, m, s, name, fields, is_obj)
# 4. Default
o(f'impl Default for {rn} {{', ' fn default() -> Self {', ' Self {')
for f in fields:
d = (SC.DEFAULT_OVERRIDES.get((f.schema, f.name), {}).get('default')
or default_expr(s, f)
or ('false' if fkind(f) == 'bool' and not f.nullable else 'Default::default()'))
# Indented as final: rustfmt leaves a struct literal alone when one
# of its lines (a long string default) cannot be made to fit.
line = f' {f.rust}: {d},'
o.ent([line]) if f.ent else o(line)
o(' }', ' }', '}', '')
# 5. IntoValue
o(f'impl IntoValue for {rn} {{',
" fn into_value(self) -> JmapValue<'static> {",
' self.into_value_with(ValueOptions::default())', ' }', '',
" fn into_value_with(self, opts: ValueOptions) -> JmapValue<'static> {",
f' let mut map = jmap_tools::Map::with_capacity({len(fields) + 2});')
for f in fields:
val = f'self.{f.rust}.into_value_with(opts)'
if is_secret(f):
val = (f'if opts.include_secrets {{ {val} }} else '
'{ JmapValue::Str(MASKED_PASSWORD.into()) }')
line = f'map.insert_unchecked({P(f)}, {val});'
if is_secret(f) and f.nullable:
line = f'if self.{f.rust}.is_some() {{ {line} }}'
o.ent([line]) if f.ent else o(line)
o(' JmapValue::Object(map)', ' }', '}', '')
# 6. Patch
o(f'impl RegistryJsonPropertyPatch for {rn} {{',
" fn patch_property<'x>(&mut self, mut pointer: JsonPointerPatch<'_>, value: JmapValue<'x>) -> PatchResult<'x> {",
' match pointer.next_property() {')
for f in allf:
line = patch_arm(f)
o.ent([line]) if f.ent else o(line)
o(' Some(Property::Type) => Ok(MaybeUnpatched::Unpatched { property: Property::Type, value }),',
' _ => Err(PatchError::new(pointer, "Invalid property")),',
' }', ' }', '}', '')
def string_validators(f):
v = SC.STRING_VALIDATORS.get((f.schema, f.name))
if v is not None:
return v
t = f.t
while t['type'] == 'set':
t = t['class']
if t['type'] == 'string' and t.get('format') == 'emailAddress' and f.update != 'serverSet':
return ['Email']
return []
def patch_arm(f):
arm = f'Some({P(f)})'
if f.update == 'serverSet':
return f'{arm} => pointer.assert_server_set(),'
if f.virtual:
return f'Some(property @ {P(f)}) => Ok(MaybeUnpatched::Unpatched {{ property, value }}),'
ptr = 'pointer'
if f.update == 'immutable':
ptr += '.assert_read_only()?'
if f.name == 'accountId':
ptr += '.assert_can_set_account()?'
if f.name == 'memberTenantId':
# inbuxa: MT, only a caller allowed to move objects between tenants
ptr += '.assert_can_set_tenant()?'
vs = string_validators(f)
if vs:
ptr += '.with_validators(&[' + ', '.join(f'StringValidator::{v}' for v in vs) + '])'
return f'{arm} => self.{f.rust}.patch({ptr}, value),'
def emit_pickle_single(o, m, s, name, fields, is_obj):
rn = strip_x(name)
flagged = [f for f in fields if f.nullable or kind_of(f.t) == 'bool']
flag_no = {f.name: i for i, f in enumerate(flagged)}
o(f'impl Pickle for {rn} {{', ' fn pickle(&self, out: &mut Vec<u8>) {')
if flagged:
if s.agpl and all(f.ent for f in flagged):
o('#[cfg_attr(not(feature = "enterprise"), allow(unused_mut))]')
o('let mut bitmap = 0u64;')
for f in flagged:
cond = f'self.{f.rust}' if not f.nullable else f'self.{f.rust}.is_some()'
lines = [f'if {cond} {{', f' bitmap |= 1 << {flag_no[f.name]};', '}']
o.ent(lines) if f.ent else o(*lines)
o(f'let block = crate::pickle::begin_block(out, {len(fields)}, {len(flagged)}, '
f'{"bitmap" if flagged else "0"});')
for f in fields:
if kind_of(f.t) == 'bool' and not f.nullable:
continue
if f.nullable:
lines = [f'if let Some(value) = &self.{f.rust} {{', ' value.pickle(out);', '}']
else:
lines = [f'self.{f.rust}.pickle(out);']
if f.ent:
if s.agpl and not f.nullable:
o.agpl_only([CFG_NOT_ENT, '{',
' let block = crate::pickle::begin_block(out, 0, 0, 0);',
' crate::pickle::end_block(out, block);', '}'])
o.ent(lines)
else:
o(*lines)
o('crate::pickle::end_block(out, block);', '}', '')
o("fn unpickle(stream: &mut crate::pickle::PickledStream<'_>) -> Option<Self> {")
if is_obj:
o('if stream.version() > Self::VERSION {', ' return None;', '}')
o('let block = stream.open_block()?;')
for i, f in enumerate(fields):
d = (SC.DEFAULT_OVERRIDES.get((f.schema, f.name), {}).get('unpickle')
or default_expr(s, f) or 'Default::default()')
ty = rust_type(f)
if kind_of(f.t) == 'bool' and not f.nullable:
if not f.has_default:
d = 'false'
lines = [f'let field{i}: {ty} = if block.has({i}) {{ block.flag({flag_no[f.name]}) }} else {{ {d} }};']
elif f.nullable:
lines = [f'let field{i}: {ty} = if block.has({i}) {{',
f' if block.flag({flag_no[f.name]}) {{ Some(Pickle::unpickle(stream)?) }} else {{ None }}',
f'}} else {{ {d} }};']
else:
lines = [f'let field{i}: {ty} = if block.has({i}) {{ Pickle::unpickle(stream)? }} else {{ {d} }};']
if f.ent:
if s.agpl and not f.nullable and kind_of(f.t) != 'bool':
o.agpl_only([CFG_NOT_ENT, f'if block.has({i}) {{',
' let block = stream.open_block()?;',
' stream.close_block(block);', '}'])
o.ent(lines)
else:
o(*lines)
o('stream.close_block(block);', 'Some(Self {')
for i, f in enumerate(fields):
line = f'{f.rust}: field{i},'
o.ent([line]) if f.ent else o(line)
o('})', '}', '}', '')
def emit_multi(o, m, s, name):
rn = strip_x(name)
vs = variants(s, name)
is_obj = name in m.object_set
def arms(fn):
for idx, (ident, jname, payload, vent) in enumerate(vs):
lines = fn(idx, ident, jname, payload)
if lines is None:
continue
o.ent(lines) if vent else o(*lines)
# 1. ObjectImpl / validate
if is_obj:
o(f'impl ObjectImpl for {rn} {{',
f' const FLAGS: u64 = {m.object_flags(name)};',
' const VERSION: u8 = 0;',
f' const OBJECT: ObjectType = ObjectType::{rn};', '')
else:
o(f'impl {rn} {{')
# a type whose variants carry no payload never reads these parameters
# (full edition only: no open type is like that)
unit_only = not any(p for _, _, p, _ in vs)
errors_arg, opts_arg = ('_errors', '_opts') if unit_only else ('errors', 'opts')
o(f'fn validate(&self, {errors_arg}: &mut Vec<ValidationError>) -> bool {{', 'match self {')
arms(lambda i, v, j, p: [f'{rn}::{v}(inner) => inner.validate(errors),'] if p
else [f'{rn}::{v} => true,'])
o('}', '}')
if is_obj or name in m.indexable:
o('')
if name in m.indexable:
typed = name in SC.TYPE_INDEXED_OBJECTS
o("fn index<'x>(&'x self, i: &mut IndexBuilder<'x>) {", 'match self {')
def ix(i, v, j, p):
body = [f'i.typ({i});'] if typed else []
if p and p in m.indexable:
body.append('object.index(i);')
return [f'{rn}::{v}(object) => {{'] + body + ['}']
pat = f'{rn}::{v}(_)' if p else f'{rn}::{v}'
return [f'{pat} => {{'] + body + ['}']
arms(ix)
o('}', '}')
else:
o("fn index<'x>(&'x self, _: &mut IndexBuilder<'x>) {}")
emit_trailing_ent(o, s, name)
o('}', '')
# 2. Default
ident, jname, payload, vent = vs[0]
o(f'impl Default for {rn} {{', ' fn default() -> Self {',
f' {rn}::{ident}(Default::default())' if payload else f' {rn}::{ident}',
' }', '}', '')
# 3. Pickle
if name in m.pickled:
o(f'impl Pickle for {rn} {{', ' fn pickle(&self, out: &mut Vec<u8>) {', 'match self {')
arms(lambda i, v, j, p: [f'{rn}::{v}(inner) => {{', f'{i}u16.pickle(out);', 'inner.pickle(out);', '}']
if p else [f'{rn}::{v} => {{', f'{i}u16.pickle(out);', '}'])
o('}', '}', '')
o("fn unpickle(stream: &mut crate::pickle::PickledStream<'_>) -> Option<Self> {")
if is_obj:
o('if stream.version() > Self::VERSION {', ' return None;', '}')
o('match u16::unpickle(stream)? {')
arms(lambda i, v, j, p: [f'{i} => Pickle::unpickle(stream).map({rn}::{v}),'] if p
else [f'{i} => Some({rn}::{v}),'])
o('_ => None,', '}', '}', '}', '')
# 4. IntoValue
o(f'impl IntoValue for {rn} {{',
" fn into_value(self) -> JmapValue<'static> {",
' self.into_value_with(ValueOptions::default())', ' }', '',
f" fn into_value_with(self, {opts_arg}: ValueOptions) -> JmapValue<'static> {{", 'match self {')
arms(lambda i, v, j, p: [f'{rn}::{v}(obj) => {{', 'let mut obj = obj.into_value_with(opts);',
f'obj.as_object_mut().unwrap().insert_unchecked(Property::Type, JmapValue::Str("{j}".into()));',
'obj', '}'] if p else
[f'{rn}::{v} => {{', 'let mut obj = jmap_tools::Map::new();',
f'obj.insert_unchecked(Property::Type, JmapValue::Str("{j}".into()));',
'JmapValue::Object(obj)', '}'])
o('}', '}', '}', '')
# 5. Patch
o(f'impl RegistryJsonPatch for {rn} {{',
" fn patch<'x>(&mut self, pointer: JsonPointerPatch<'_>, value: JmapValue<'x>) -> PatchResult<'x> {",
'if !pointer.has_next() {', 'match object_type(&pointer, &value)? {')
ve = variant_enum(name)
arms(lambda i, v, j, p: [f'{ve}::{v} => *self = {rn}::{v}(Default::default()),'] if p
else [f'{ve}::{v} => *self = {rn}::{v},'])
o('}', '}', 'match self {')
arms(lambda i, v, j, p: [f'{rn}::{v}(inner) => inner.patch(pointer, value),'] if p
else [f'{rn}::{v} => pointer.assert_eof(),'])
o('}', '}', '}', '')
# 6. object_type (+ expression contexts)
o(f'impl {rn} {{', f'pub fn object_type(&self) -> {ve} {{', 'match self {')
arms(lambda i, v, j, p: [f'{rn}::{v}(_) => {ve}::{v},'] if p
else [f'{rn}::{v} => {ve}::{v},'])
o('}', '}')
if name in m.has_ctx:
o('', "pub fn expression_ctxs(&self) -> Vec<ExpressionContext<'_>> {", 'match self {')
arms(lambda i, v, j, p: [f'{rn}::{v}(obj) => obj.expression_ctxs(),']
if p and p in m.has_ctx else None)
if any(not (v[2] and v[2] in m.has_ctx) for v in vs):
o('_ => vec![],')
o('}', '}')
o('}', '')
def gen_structs_impl(s):
m = Model(s)
o = Out(s)
o('use crate::schema::prelude::*;', '')
for name, ent in edition_schemas(s):
start = len(o.lines)
if schema_kind(s, name) == 'multiple':
emit_multi(o, m, s, name)
else:
emit_single(o, m, s, name)
if ent:
block = o.lines[start:]
del o.lines[start:]
o.ent(block)
return finish(s, o.text().rstrip('\n') + '\n')
# ---------------------------------------------------------------------------
# properties_impl.rs
# ---------------------------------------------------------------------------
INDEX_VALUE_TYPES = {'string': 'Keyword', 'id': 'Id', 'ip': 'IpMask', 'ipmask': 'IpMask',
'enum': 'Enum', 'u64': 'Number', 'i64': 'Number', 'float': 'Number',
'duration': 'Number', 'bool': 'Boolean', 'datetime': 'Number'}
def index_value_type(f):
t = f.t
while t['type'] == 'set':
t = t['class']
k = kind_of(t)
if SC.RUST_TYPE_OVERRIDES.get((f.schema, f.name)) == 'Id':
k = 'id'
return INDEX_VALUE_TYPES[k]
def index_schemas(m, name, ent=False, seen=None):
"""[(Property ident, IndexSchemaType, IndexSchemaValueType, enterprise)]"""
s = m.s
seen = set() if seen is None else seen
if name in seen:
return []
seen = seen | {name}
out = []
if schema_kind(s, name) == 'multiple':
if name in SC.TYPE_INDEXED_OBJECTS:
out.append(('Type', 'Search', 'Enum', ent))
for v in variants(s, name):
if v[2] and v[2] in m.indexable:
out += index_schemas(m, v[2], ent or v[3], seen)
return out
for f in schema_struct_fields(s, name):
fe = ent or f.ent
for op in index_ops(f):
if op[0] == 'text':
out.append((op[1], 'Search', 'Text', fe))
elif op[0] in ('search', 'unique'):
vt = index_value_type(f)
# Enum-valued field indexes are not queryable (only @type is).
if vt != 'Enum':
out.append((op[1], op[0].capitalize(), vt, fe))
if m.nested_index(f):
k = kind_of(f.t)
child = f.t['objectName'] if k in ('object', 'list') else f.t['valueClass']['objectName']
out += index_schemas(m, child, fe, seen)
return out
def emit_arms(o, s, rows):
"""rows: [(lines, enterprise)]"""
for lines, ent in rows:
if isinstance(lines, str):
lines = [lines]
o.ent(lines) if ent else o(*lines)
def emit_id_accessors(o, s, objs, json_name, rust_name):
"""`{rust_name}()` and `set_{rust_name}()` on ObjectInner, for every object
(or variant) that has the Id field `json_name`."""
acc_rows, set_rows = [], []
for n, r, e in objs:
if schema_kind(s, n) == 'multiple':
vs = [v for v in variants(s, n) if v[2] and
any(f.name == json_name for f in schema_struct_fields(s, v[2]))]
if not vs:
continue
get = [([f'ObjectInner::{r}(inner) => match inner.as_ref() {{'], False)]
st = [([f'ObjectInner::{r}(inner) => match inner.as_mut() {{'], False)]
for ident, _, payload, vent in vs:
af = next(f for f in schema_struct_fields(s, payload) if f.name == json_name)
get.append(([f'{r}::{ident}(obj) => ' + (f'obj.{rust_name},' if af.nullable else f'Some(obj.{rust_name}),')], vent))
st.append(([f'{r}::{ident}(obj) => obj.{rust_name} = ' + ('Some(id),' if af.nullable else 'id,')], vent))
every = len(vs) == len(variants(s, n))
get.append(((['_ => None,'] if not every else []) + ['},'], False))
st.append(((['_ => {}'] if not every else []) + ['},'], False))
if e:
acc_rows.append(([l for ls, _ in get for l in ls], True))
set_rows.append(([l for ls, _ in st for l in ls], True))
else:
acc_rows += get
set_rows += st
else:
af = next((f for f in schema_struct_fields(s, n) if f.name == json_name), None)
if af is None:
continue
acc_rows.append((f'ObjectInner::{r}(obj) => ' + (f'obj.{rust_name},' if af.nullable else f'Some(obj.{rust_name}),'), e or af.ent))
set_rows.append((f'ObjectInner::{r}(obj) => obj.{rust_name} = ' + ('Some(id),' if af.nullable else 'id,'), e or af.ent))
o(f'pub fn {rust_name}(&self) -> Option<Id> {{', 'match self {')
emit_arms(o, s, acc_rows)
o('_ => None,', '}', '}', '')
o(f'pub fn set_{rust_name}(&mut self, id: Id) {{', 'match self {')
emit_arms(o, s, set_rows)
o('_ => {}', '}', '}', '')
def gen_properties_impl(s):
m = Model(s)
o = Out(s)
o('use crate::schema::prelude::*;', '')
emit_enum_impl(o, s, 'ObjectType', object_variants(s))
emit_enum_impl(o, s, 'Property', property_variants(s))
objs = [(n, strip_x(n), s.agpl and s.ent_object(n)) for n in s.objects]
json_of = {pascal(n): n for n in s.prop_ids}
json_of['Type'] = '@type'
def perm(n, op):
prefix = s.d['objects'][n]['permissionPrefix']
name = prefix + op
return f'Permission::{variant_ident(name)}' if name in m.perms else None
# impl ObjectType
o('impl ObjectType {', 'pub fn flags(&self) -> u64 {', 'match self {')
emit_arms(o, s, [(f'ObjectType::{r} => {r}::FLAGS,', e) for n, r, e in objs])
o('}', '}', '')
o('pub fn indexes(&self) -> Vec<IndexSchema> {', 'match self {')
for n, r, e in objs:
# Objects that are not stored have nothing to query by.
rows = [] if n in SC.NOT_STORED_OBJECTS else index_schemas(m, n)
uniq, seen = [], {}
for row in rows:
key = row[:3]
if key in seen:
if not row[3] and uniq[seen[key]][3]:
uniq[seen[key]] = row
continue
seen[key] = len(uniq)
uniq.append(row)
if not uniq:
continue
uniq.sort(key=lambda x: (x[0] != 'Text', json_of.get(x[0], x[0])))
lines = [f'ObjectType::{r} => vec![']
body = [(f'IndexSchema::new(Property::{p}, IndexSchemaType::{st}, IndexSchemaValueType::{vt}),',
ent) for p, st, vt, ent in uniq]
if e:
o.ent(lines + [b for b, _ in body] + ['],'])
else:
o(*lines)
emit_arms(o, s, body)
o('],')
o('_ => vec![],', '}', '}', '')
for fn, op, fallback, wildcard in (('get_permission', 'Get', None, False),
('query_permission', 'Query', None, True),
('create_permission', 'Create', 'Update', False),
('update_permission', 'Update', None, False),
('destroy_permission', 'Destroy', 'Update', False)):
o(f'pub fn {fn}(&self) -> Permission {{', 'match self {')
rows = []
for n, r, e in objs:
# Singletons are created and destroyed by updating them.
singleton = s.d['objects'][n]['type'] == 'singleton'
p = perm(n, op) or (fallback and singleton and perm(n, fallback))
if p:
rows.append((f'ObjectType::{r} => {p},', e))
elif not wildcard:
rows.append((f'ObjectType::{r} => unreachable!(),', e))
emit_arms(o, s, rows)
if wildcard:
o('_ => unreachable!(),')
o('}', '}', '')
o('}', '')
# impl ObjectInner
o('impl ObjectInner {')
# accountId everywhere; memberTenantId (inbuxa: MT) in the full edition only,
# the agpl edition has no such fields.
for json_name, rust_name in (('accountId', 'account_id'), ('memberTenantId', 'member_tenant_id')):
if json_name == 'memberTenantId' and s.agpl:
continue
emit_id_accessors(o, s, objs, json_name, rust_name)
o('pub fn to_pickled_vec(&self) -> Vec<u8> {', 'match &self {')
emit_arms(o, s, [(f'ObjectInner::{r}(obj) => obj.to_pickled_vec(),' if n not in SC.NOT_STORED_OBJECTS
else f'ObjectInner::{r}(_) => unimplemented!("{r} is not stored"),', e)
for n, r, e in objs])
o('}', '}', '')
o("pub fn unpickle(object: ObjectType, stream: &mut crate::pickle::PickledStream<'_>) -> Option<Self> {",
'match object {')
emit_arms(o, s, [(f'ObjectType::{r} => Pickle::unpickle(stream).map(|obj| ObjectInner::{r}(Box::new(obj))),'
if n not in SC.NOT_STORED_OBJECTS
else f'ObjectType::{r} => unimplemented!("{r} is not stored"),', e)
for n, r, e in objs])
o('}', '}', '')
o("pub fn deserialize<'de, D>(object: ObjectType, deserializer: D) -> Result<Self, D::Error>",
'where', "D: serde::Deserializer<'de>,", '{', 'match object {')
emit_arms(o, s, [(f'ObjectType::{r} => {r}::deserialize(deserializer).map(|obj| ObjectInner::{r}(Box::new(obj))),', e)
for n, r, e in objs])
o('}', '}', '')
o("pub fn expression_ctxs(&self) -> Option<Vec<ExpressionContext<'_>>> {", 'match &self {')
emit_arms(o, s, [(f'ObjectInner::{r}(obj) => Some(obj.expression_ctxs()),', e)
for n, r, e in objs if n in m.has_ctx])
o('_ => None,', '}', '}', '}', '')
# impl Object
o('impl Object {', 'pub fn flags(&self) -> u64 {', 'match &self.inner {')
emit_arms(o, s, [(f'ObjectInner::{r}(_) => {r}::FLAGS,', e) for n, r, e in objs])
o('}', '}', '', 'pub fn object_type(&self) -> ObjectType {', 'match &self.inner {')
emit_arms(o, s, [(f'ObjectInner::{r}(_) => ObjectType::{r},', e) for n, r, e in objs])
o('}', '}', '', "pub fn object_variant(&self) -> Option<&'static str> {", 'match &self.inner {')
emit_arms(o, s, [(f'ObjectInner::{r}(obj) => Some(obj.object_type().as_str()),', e)
for n, r, e in objs if schema_kind(s, n) == 'multiple'])
o('_ => None,', '}', '}', '')
o('pub fn validate(&self, errors: &mut Vec<ValidationError>) -> bool {', 'match &self.inner {')
emit_arms(o, s, [(f'ObjectInner::{r}(obj) => obj.validate(errors),', e) for n, r, e in objs])
o('}', '}', '')
o("pub fn index<'x>(&'x self, i: &mut IndexBuilder<'x>) {", 'match &self.inner {')
emit_arms(o, s, [(f'ObjectInner::{r}(obj) => obj.index(i),', e) for n, r, e in objs])
o('}', '}', '')
o("pub fn patch<'x>(&mut self, pointer: JsonPointerPatch<'_>, value: JmapValue<'x>) -> PatchResult<'x> {",
'match &mut self.inner {')
emit_arms(o, s, [(f'ObjectInner::{r}(obj) => obj.patch(pointer, value),', e) for n, r, e in objs])
o('}', '}', '}', '')
o('impl IntoValue for Object {',
"fn into_value(self) -> JmapValue<'static> {",
'self.into_value_with(ValueOptions::default())', '}', '',
"fn into_value_with(self, opts: ValueOptions) -> JmapValue<'static> {", 'match self.inner {')
emit_arms(o, s, [(f'ObjectInner::{r}(obj) => (*obj).into_value_with(opts),', e) for n, r, e in objs])
o('}', '}', '}', '')
o('impl From<ObjectType> for ObjectInner {', 'fn from(obj: ObjectType) -> Self {', 'match obj {')
emit_arms(o, s, [(f'ObjectType::{r} => ObjectInner::{r}(Box::default()),', e) for n, r, e in objs])
o('}', '}', '}', '')
for n, r, e in objs:
lines = [f'impl From<{r}> for ObjectInner {{', f'fn from(value: {r}) -> Self {{',
f'ObjectInner::{r}(Box::new(value))', '}', '}', '',
f'impl From<Object> for {r} {{', 'fn from(obj: Object) -> Self {', 'match obj.inner {',
f'ObjectInner::{r}(obj) => *obj,', '_ => unreachable!(),', '}', '}', '}', '']
o.ent(lines) if e else o(*lines)
return finish(s, o.text().rstrip('\n') + '\n')
# ---------------------------------------------------------------------------
# Driver
# ---------------------------------------------------------------------------
OUTPUTS = {
'crates/registry/src/schema/enums.rs': 'gen_enums',
'crates/registry/src/schema/enums_impl.rs': 'gen_enums_impl',
'crates/registry/src/schema/properties.rs': 'gen_properties',
'crates/registry/src/schema/properties_impl.rs': 'gen_properties_impl',
'crates/registry/src/schema/structs.rs': 'gen_structs',
'crates/registry/src/schema/structs_impl.rs': 'gen_structs_impl',
}
# crates/http/src/form/mod.rs is not generated: it is the hand-written
# contact-form HTTP handler (no "auto-generated" line, nothing from the schema).
def main():
ap = argparse.ArgumentParser(description=__doc__.strip().split('\n')[0])
ap.add_argument('--edition', choices=('agpl', 'full'), required=True)
ap.add_argument('--schema', default='resources/schema/schema.json.gz',
help='upstream schema (gzip JSON)')
ap.add_argument('--oss-schema', default=None,
help='upstream open-source schema (gzip JSON); defaults to '
'schema_oss.json.gz next to --schema; required for agpl')
ap.add_argument('--out', required=True, help='output root directory')
ap.add_argument('--only', action='append', default=[],
help='generate only outputs whose path contains this text')
args = ap.parse_args()
full = load_gz_json(args.schema)
oss = None
if args.edition == 'agpl':
oss_path = args.oss_schema or str(Path(args.schema).with_name('schema_oss.json.gz'))
oss = load_gz_json(oss_path)
s = Schema(full, oss, args.edition)
s.prepare()
for rel, fn in OUTPUTS.items():
if args.only and not any(x in rel for x in args.only):
continue
text = globals()[fn](s)
path = Path(args.out) / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
print(f'{rel}: {text.count(chr(10))} lines')
if __name__ == '__main__':
main()