Fixes/upgrades

This commit is contained in:
2026-07-30 21:12:29 -04:00
parent e00b3a177e
commit 9494549861
13 changed files with 823 additions and 60 deletions

View File

@@ -0,0 +1,189 @@
"""`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,
)