190 lines
6.2 KiB
Python
190 lines
6.2 KiB
Python
"""`ModelDecl` -> Rust source text.
|
|
|
|
This module classifies shapes and defaults; `templates/` spells the Rust.
|
|
Emitted structs are for serialization — plain serde derives, no `repr(C)`,
|
|
no padding, no alignment control.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
|
|
from pydantic_to_rust.idents import to_rust_ident
|
|
from pydantic_to_rust.ir import DefaultValue, ModelDecl, TypeShape
|
|
|
|
_DEFAULT_DERIVES = (
|
|
"Debug",
|
|
"Clone",
|
|
"::serde::Serialize",
|
|
"::serde::Deserialize",
|
|
)
|
|
|
|
_TEMPLATES = Path(__file__).parent / "templates"
|
|
|
|
|
|
def _rust_string(value: str) -> str:
|
|
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
return f'"{escaped}"'
|
|
|
|
|
|
def _rust_f64(value: float) -> str:
|
|
"""Always carry a decimal point, so `0.65` cannot read as an integer."""
|
|
if value == int(value):
|
|
return f"{int(value)}.0"
|
|
return repr(float(value))
|
|
|
|
|
|
def _environment() -> Environment:
|
|
env = Environment(
|
|
loader=FileSystemLoader(_TEMPLATES),
|
|
undefined=StrictUndefined,
|
|
keep_trailing_newline=True,
|
|
)
|
|
env.filters["rust_string"] = _rust_string
|
|
env.filters["rust_f64"] = _rust_f64
|
|
return env
|
|
|
|
|
|
_ENV = _environment()
|
|
|
|
|
|
def _classify_shape(shape: TypeShape) -> dict:
|
|
"""One node per reachable `TypeShape` variant, for the type template."""
|
|
if shape.primitive is not None:
|
|
return {"kind": "primitive", "primitive": shape.primitive.value}
|
|
if shape.ref is not None:
|
|
return {"kind": "ref", "name": shape.ref}
|
|
if shape.array_element is not None and shape.array_length is not None:
|
|
return {
|
|
"kind": "array",
|
|
"element": _classify_shape(shape.array_element),
|
|
"length": shape.array_length,
|
|
}
|
|
if shape.list_inner is not None:
|
|
return {"kind": "vec", "inner": _classify_shape(shape.list_inner)}
|
|
if shape.optional_inner is not None:
|
|
return {"kind": "option", "inner": _classify_shape(shape.optional_inner)}
|
|
if shape.enum_variants is not None:
|
|
# An inline `Literal[...]` has no name to emit.
|
|
return {"kind": "primitive", "primitive": "string"}
|
|
if shape.union_branches is not None:
|
|
return {"kind": "opaque"}
|
|
if shape.map_value is not None:
|
|
key = (
|
|
_classify_shape(shape.map_key)
|
|
if shape.map_key is not None
|
|
else {"kind": "primitive", "primitive": "string"}
|
|
)
|
|
return {"kind": "map", "key": key, "value": _classify_shape(shape.map_value)}
|
|
raise ValueError(f"no variant set on TypeShape: {shape}")
|
|
|
|
|
|
def _classify_value(value) -> dict:
|
|
"""A JSON-shaped value, for the value template."""
|
|
if value is None:
|
|
return {"kind": "unit"}
|
|
if isinstance(value, bool):
|
|
return {"kind": "bool", "value": value}
|
|
if isinstance(value, int):
|
|
return {"kind": "int", "value": value}
|
|
if isinstance(value, float):
|
|
return {"kind": "float", "value": value}
|
|
if isinstance(value, str):
|
|
return {"kind": "string", "value": value}
|
|
if isinstance(value, (list, tuple)):
|
|
if not value:
|
|
return {"kind": "empty_vec"}
|
|
return {"kind": "vec", "elements": [_classify_value(v) for v in value]}
|
|
if isinstance(value, dict):
|
|
if not value:
|
|
return {"kind": "empty_map"}
|
|
return {
|
|
"kind": "map",
|
|
"pairs": [
|
|
{"key": k, "value": _classify_value(v)} for k, v in value.items()
|
|
],
|
|
}
|
|
return {"kind": "unit"}
|
|
|
|
|
|
def _classify_default(shape: TypeShape, default: DefaultValue) -> dict:
|
|
"""One node per default kind, for the default template."""
|
|
if default.kind == "null":
|
|
if shape.optional_inner is not None:
|
|
return {"kind": "none"}
|
|
return {"kind": "unit"}
|
|
if default.kind == "boolean":
|
|
return {"kind": "bool", "value": default.literal}
|
|
if default.kind == "integer":
|
|
return {"kind": "int", "value": int(default.literal)}
|
|
if default.kind == "number":
|
|
return {"kind": "float", "value": default.literal}
|
|
if default.kind == "string":
|
|
return {"kind": "string", "value": default.literal}
|
|
if default.kind == "empty_seq":
|
|
return {"kind": "empty_vec"}
|
|
if default.kind == "empty_map":
|
|
return {"kind": "empty_map"}
|
|
if default.kind == "tuple":
|
|
return {
|
|
"kind": "array",
|
|
"elements": [_classify_value(v) for v in default.literal],
|
|
}
|
|
if default.kind == "enum_variant":
|
|
return {
|
|
"kind": "enum_variant",
|
|
"type": default.literal["type"],
|
|
"variant": default.literal["variant"],
|
|
}
|
|
if default.kind == "compound_model":
|
|
# Defer to the referenced type's own Default rather than replaying
|
|
# the factory's field values.
|
|
return {"kind": "delegate", "type": default.literal["type"]}
|
|
return {"kind": "unit"}
|
|
|
|
|
|
def emit_rust_struct(
|
|
model: ModelDecl,
|
|
*,
|
|
derives: tuple[str, ...] = _DEFAULT_DERIVES,
|
|
extra_attrs: tuple[str, ...] = (),
|
|
) -> str:
|
|
"""Render one model as a struct, its default functions, and an `impl
|
|
Default`. Returns a single block; the caller joins blocks into a file."""
|
|
fields = []
|
|
default_fns = []
|
|
for decl in model.fields:
|
|
shape = _classify_shape(decl.shape)
|
|
default_fn = None
|
|
if decl.default is not None:
|
|
default_fn = f"__default_{model.name}_{decl.name}"
|
|
default_fns.append(
|
|
{
|
|
"name": default_fn,
|
|
"type": shape,
|
|
"body": _classify_default(decl.shape, decl.default),
|
|
}
|
|
)
|
|
fields.append(
|
|
{
|
|
"ident": to_rust_ident(decl.name),
|
|
"type": shape,
|
|
"default_fn": default_fn,
|
|
}
|
|
)
|
|
|
|
doc_lines = []
|
|
if model.docstring:
|
|
doc_lines = [line.rstrip() for line in model.docstring.strip().splitlines()]
|
|
|
|
return _ENV.get_template("struct.rs.j2").render(
|
|
name=model.name,
|
|
doc_lines=doc_lines,
|
|
derives=list(derives),
|
|
extra_attrs=list(extra_attrs),
|
|
fields=fields,
|
|
default_fns=default_fns,
|
|
)
|