forked from 1b-agent/opencadstudio-upstream
Bundle the first-party Python scripting plugin
This commit is contained in:
1 parent
889042c1f1
commit
de51c1a17f
26 files changed
+11459
-27
No files matched your search
@@ -0,0 +1,891 @@
|
||||
// Codegen for `ocs`'s generic entity conversion functions (`dict_to_entity`
|
||||
// / `entity_to_dict`), driven by `entity_manifest.json` plus the type
|
||||
// registry embedded in `ocs_plugin_api` (`get_embedded_type_registry_json`,
|
||||
// traced by `crates/ocs_plugin_api/build.rs` in the OCS fork).
|
||||
//
|
||||
// Adapted from the RustPython-flavored port of `schoeller/ocs_python_repl`'s
|
||||
// `build/generate.rs` (a PyO3-based generator over the same registry +
|
||||
// manifest shape) — see `project_opencad_python_generic_entity_crud_plan.md`
|
||||
// in this session's memory for the full design rationale. The two crates
|
||||
// target different Python runtimes (PyO3/real CPython vs. RustPython), so
|
||||
// the leaf conversion code differs, but the manifest schema and the
|
||||
// registry-driven-with-overrides approach carries over directly.
|
||||
//
|
||||
// `include!`'d directly into `build.rs` (not a separate crate) so it shares
|
||||
// `build.rs`'s own dependency graph without adding a workspace member.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use ocs_plugin_api::{FieldInfo, TypeId, TypeInfo, TypeKind, TypeRegistry};
|
||||
use serde::Deserialize;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Manifest schema
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Manifest {
|
||||
/// Entity kinds exposed to `ocs.add`/`ocs.update`/`entity_to_dict`. The
|
||||
/// central, easy-to-widen filter for Phase 2 (see the plan doc).
|
||||
pub type_filter: Vec<String>,
|
||||
/// `EntityCommon` fields promoted onto every entity dict. Only `handle`
|
||||
/// and `layer` are supported today — the rest of `EntityCommon` (color,
|
||||
/// line weight, XDATA, ...) has its own existing `ocs.*` accessors and is
|
||||
/// deliberately out of scope for this generic path.
|
||||
#[serde(default = "default_base_fields")]
|
||||
pub base_fields: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub overrides: std::collections::BTreeMap<String, EntityOverride>,
|
||||
}
|
||||
|
||||
fn default_base_fields() -> Vec<String> {
|
||||
vec!["handle".to_string(), "layer".to_string()]
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone)]
|
||||
pub struct EntityOverride {
|
||||
/// Rust expression constructing a fresh value when this entity has no Default.
|
||||
#[serde(default)]
|
||||
pub rust_constructor: Option<String>,
|
||||
/// Existing entities can be inspected and patched, but `ocs.add` cannot
|
||||
/// create one without host-side reference setup.
|
||||
#[serde(default)]
|
||||
pub update_only: bool,
|
||||
#[serde(default)]
|
||||
pub fields: std::collections::BTreeMap<String, FieldOverride>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone)]
|
||||
pub struct FieldOverride {
|
||||
/// Emit a getter but reject patches to this field.
|
||||
#[serde(default)]
|
||||
pub read_only: bool,
|
||||
/// Rename the dict key for this field.
|
||||
#[serde(default)]
|
||||
pub python_name: Option<String>,
|
||||
/// Raw Rust *value* expression (not yet wrapped in `vm.new_pyobj`),
|
||||
/// with `VAR` standing in for the entity's local variable name.
|
||||
/// Requires `rust_setter` too.
|
||||
#[serde(default)]
|
||||
pub rust_getter: Option<String>,
|
||||
/// Raw Rust *statement* (semicolon-terminated), with `VAR` standing in
|
||||
/// for the entity's local variable name and `dict`/`vm` bound as usual.
|
||||
/// Requires `rust_getter` too.
|
||||
#[serde(default)]
|
||||
pub rust_setter: Option<String>,
|
||||
/// Drop this field from both directions entirely (e.g. a field whose
|
||||
/// type this generator doesn't support yet, such as a tagged enum).
|
||||
#[serde(default)]
|
||||
pub exclude: bool,
|
||||
/// `ocs.add()` errors if this field is missing rather than silently
|
||||
/// defaulting it to zero/empty (Felix's Phase 1 review: "add should
|
||||
/// reject missing required geometry rather than defaulting to zero").
|
||||
/// Only meaningful on the generic (non-`rust_getter`/`rust_setter`)
|
||||
/// path — `ocs.update()` never requires a field (a merge onto an
|
||||
/// already-valid entity never needs it re-specified).
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
pub fn load(json: &str) -> Self {
|
||||
serde_json::from_str(json).expect("entity_manifest.json should be valid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
fn get<'a>(registry: &'a TypeRegistry, name: &str) -> Option<&'a TypeInfo> {
|
||||
registry.types.get(&TypeId::new(name))
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Type classification
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const BUILTIN: &[&str] = &[
|
||||
"f64", "f32", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "bool", "String", "str",
|
||||
];
|
||||
/// Types with their own hand-written conversion helpers in `ocs_module.rs`
|
||||
/// (`vector3_to_py`/`py_to_vector3`, etc.) rather than generated ones.
|
||||
const SPECIAL: &[&str] = &["Vector3", "Vector2", "Handle"];
|
||||
|
||||
enum Classify {
|
||||
/// A C-like enum: every variant is a unit variant. Represented in Python
|
||||
/// as its variant name (a plain string).
|
||||
UnitEnum,
|
||||
/// An enum whose variants carry one payload. Represented as
|
||||
/// `{ "kind": "Variant", "value": ... }` so nested geometry such as
|
||||
/// hatch boundary edges remains explicit and round-trippable.
|
||||
TaggedEnum,
|
||||
/// A struct whose only field is `bits` (an acadrust bitflags newtype,
|
||||
/// e.g. `PolylineFlags`/`VertexFlags`). Represented in Python as a plain
|
||||
/// integer via `.bits()`/`::from_bits(..)` — the named boolean accessors
|
||||
/// some of these types have (`is_closed`/`set_closed`, ...) are exposed
|
||||
/// per-field instead, via a manifest override, not generically.
|
||||
BitFlags,
|
||||
/// An ordinary struct: represented in Python as a nested dict.
|
||||
Struct,
|
||||
}
|
||||
|
||||
fn classify<'a>(name: &str, info: &'a TypeInfo) -> Classify {
|
||||
match info.kind {
|
||||
TypeKind::Struct => {
|
||||
if info.fields.len() == 1 && info.fields[0].name == "bits" {
|
||||
Classify::BitFlags
|
||||
} else {
|
||||
Classify::Struct
|
||||
}
|
||||
}
|
||||
TypeKind::Enum => {
|
||||
if info.variants.iter().any(|v| !v.fields.is_empty()) {
|
||||
if info.variants.iter().all(|v| v.fields.len() == 1) {
|
||||
Classify::TaggedEnum
|
||||
} else {
|
||||
panic!("entity_manifest.json: {name} has a tagged variant without exactly one payload");
|
||||
}
|
||||
} else {
|
||||
Classify::UnitEnum
|
||||
}
|
||||
}
|
||||
other => panic!("entity_manifest.json: {name} has unsupported registry kind {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn bits_field_type(info: &TypeInfo) -> &str {
|
||||
info.fields[0].type_id.as_str()
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Helper-type closure (topologically ordered so each generated fn only calls
|
||||
// ones already emitted above it)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
fn helper_closure(manifest: &Manifest, registry: &TypeRegistry) -> Vec<String> {
|
||||
let mut needed: BTreeSet<String> = BTreeSet::new();
|
||||
let mut queue: Vec<String> = Vec::new();
|
||||
|
||||
for kind in &manifest.type_filter {
|
||||
let Some(info) = get(registry, kind) else {
|
||||
panic!("entity_manifest.json: unknown entity kind {kind}");
|
||||
};
|
||||
let override_def = manifest.overrides.get(kind);
|
||||
for f in &info.fields {
|
||||
if f.name == "common" {
|
||||
continue;
|
||||
}
|
||||
if override_def
|
||||
.and_then(|o| o.fields.get(&f.name))
|
||||
.map(|o| o.exclude || o.rust_getter.is_some())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// Excluded, or fully hand-supplied by the override — its
|
||||
// type (if any) never needs a generated conversion pair.
|
||||
continue;
|
||||
}
|
||||
queue.push(f.type_id.as_str().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(name) = queue.pop() {
|
||||
if BUILTIN.contains(&name.as_str()) || SPECIAL.contains(&name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if !needed.insert(name.clone()) {
|
||||
continue;
|
||||
}
|
||||
let Some(info) = get(registry, &name) else {
|
||||
panic!("entity_manifest.json: type {name} (reached transitively) is not in the type registry");
|
||||
};
|
||||
for f in &info.fields {
|
||||
queue.push(f.type_id.as_str().to_string());
|
||||
}
|
||||
for v in &info.variants {
|
||||
for f in &v.fields {
|
||||
queue.push(f.type_id.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fixpoint topo sort: repeatedly emit any type whose remaining
|
||||
// dependencies (among `needed`) are already ordered.
|
||||
let mut ordered = Vec::new();
|
||||
let mut remaining: Vec<String> = needed.into_iter().collect();
|
||||
while !remaining.is_empty() {
|
||||
let mut progressed = false;
|
||||
for name in remaining.clone() {
|
||||
let info = get(registry, &name).unwrap();
|
||||
let deps_pending = info
|
||||
.fields
|
||||
.iter()
|
||||
.chain(info.variants.iter().flat_map(|v| v.fields.iter()))
|
||||
.any(|f| {
|
||||
let t = f.type_id.as_str();
|
||||
t != name && remaining.iter().any(|r| r == t)
|
||||
});
|
||||
if !deps_pending {
|
||||
ordered.push(name.clone());
|
||||
remaining.retain(|n| n != &name);
|
||||
progressed = true;
|
||||
}
|
||||
}
|
||||
if !progressed {
|
||||
// Shouldn't happen for our closed, cycle-free set of types, but
|
||||
// don't hang the build if it ever does.
|
||||
ordered.append(&mut remaining);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ordered
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Leaf conversion expressions (single value, not sequence/optional wrapping)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
fn snake_case(name: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for (i, ch) in name.chars().enumerate() {
|
||||
if ch.is_uppercase() && i > 0 {
|
||||
out.push('_');
|
||||
}
|
||||
out.push(ch.to_ascii_lowercase());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn native_rust_type(type_id: &str) -> &'static str {
|
||||
match type_id {
|
||||
"f64" | "f32" => "f64",
|
||||
"i8" => "i8",
|
||||
"i16" => "i16",
|
||||
"i32" => "i32",
|
||||
"i64" => "i64",
|
||||
"u8" => "u8",
|
||||
"u16" => "u16",
|
||||
"u32" => "u32",
|
||||
"u64" => "u64",
|
||||
"bool" => "bool",
|
||||
"String" | "str" => "String",
|
||||
other => panic!("not a native type: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_native(type_id: &str) -> bool {
|
||||
BUILTIN.contains(&type_id)
|
||||
}
|
||||
|
||||
fn default_expr(type_id: &str) -> String {
|
||||
match type_id {
|
||||
"f64" | "f32" => "0.0".to_string(),
|
||||
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" => "0".to_string(),
|
||||
"bool" => "false".to_string(),
|
||||
"String" | "str" => "String::new()".to_string(),
|
||||
"Vector3" => "acadrust::types::Vector3::ZERO".to_string(),
|
||||
"Vector2" => "acadrust::types::Vector2::ZERO".to_string(),
|
||||
"Handle" => "acadrust::Handle::new(0)".to_string(),
|
||||
other => format!("default_{}()", snake_case(other)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `item_expr` must already be a `&T` expression (typically a bare place like
|
||||
/// `&value.field`, sometimes already parenthesized). Result: a
|
||||
/// `PyResult<PyObjectRef>` expression.
|
||||
fn leaf_to_py(registry: &TypeRegistry, type_id: &str, item_expr: &str) -> String {
|
||||
// A plain function argument (`f(item_expr)`) never needs parens; a method
|
||||
// call or cast directly on `item_expr` (`item_expr.method()`,
|
||||
// `item_expr as T`) does, since `&` binds looser than both.
|
||||
let parenthesized = format!("({item_expr})");
|
||||
match type_id {
|
||||
"f64" | "f32" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "bool" => {
|
||||
format!("Ok(vm.new_pyobj(*{parenthesized}))")
|
||||
}
|
||||
"String" | "str" => format!("Ok(vm.new_pyobj({parenthesized}.clone()))"),
|
||||
"Vector3" => format!("vector3_to_py_dict(vm, {item_expr})"),
|
||||
"Vector2" => format!("vector2_to_py(vm, {item_expr})"),
|
||||
"Handle" => format!("Ok(vm.new_pyobj({parenthesized}.value()))"),
|
||||
other => {
|
||||
let info = get(registry, other)
|
||||
.unwrap_or_else(|| panic!("type {other} missing from registry"));
|
||||
match classify(other, info) {
|
||||
Classify::UnitEnum => {
|
||||
format!("Ok(vm.new_pyobj({}_name({item_expr}).to_owned()))", snake_case(other))
|
||||
}
|
||||
Classify::TaggedEnum => format!("{}_to_dict(vm, {item_expr})", snake_case(other)),
|
||||
Classify::BitFlags => format!("Ok(vm.new_pyobj({parenthesized}.bits() as i64))"),
|
||||
Classify::Struct => format!("{}_to_dict(vm, {item_expr})", snake_case(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `value_expr` must already be an owned `PyObjectRef` expression. Result: a `PyResult<T>` expression.
|
||||
fn leaf_from_py(registry: &TypeRegistry, type_id: &str, value_expr: &str) -> String {
|
||||
if is_native(type_id) {
|
||||
return format!("{value_expr}.try_into_value::<{}>(vm)", native_rust_type(type_id));
|
||||
}
|
||||
match type_id {
|
||||
"Vector3" => format!("py_to_vector3_dict({value_expr}, vm)"),
|
||||
"Vector2" => format!("py_to_vector2({value_expr}, vm)"),
|
||||
"Handle" => format!("{value_expr}.try_into_value::<u64>(vm).map(acadrust::Handle::new)"),
|
||||
other => {
|
||||
let info = get(registry, other)
|
||||
.unwrap_or_else(|| panic!("type {other} missing from registry"));
|
||||
match classify(other, info) {
|
||||
Classify::UnitEnum => format!(
|
||||
"{value_expr}.try_into_value::<String>(vm).and_then(|s| str_to_{}(s, vm))",
|
||||
snake_case(other)
|
||||
),
|
||||
Classify::TaggedEnum => format!("dict_to_{}({value_expr}, vm)", snake_case(other)),
|
||||
Classify::BitFlags => {
|
||||
let path = format!("acadrust::entities::{other}");
|
||||
let bits_ty = bits_field_type(info);
|
||||
format!(
|
||||
"{value_expr}.try_into_value::<i64>(vm).and_then(|n| <{bits_ty}>::try_from(n).map({path}::from_bits).map_err(|_| vm.new_value_error(\"{other} bits out of range\".to_owned())))"
|
||||
)
|
||||
}
|
||||
Classify::Struct => format!("dict_to_{}({value_expr}, vm)", snake_case(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Field-level (sequence/optional-aware) getter/setter expressions
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// `field_place` is a place expression for the field itself (e.g. `"value.radius"`),
|
||||
/// reached through an already-borrowed parent. Result: a `PyResult<PyObjectRef>` expression.
|
||||
fn getter_for_field(
|
||||
registry: &TypeRegistry,
|
||||
type_id: &str,
|
||||
optional: bool,
|
||||
is_sequence: bool,
|
||||
field_place: &str,
|
||||
) -> String {
|
||||
if is_sequence {
|
||||
let inner = leaf_to_py(registry, type_id, "item");
|
||||
format!(
|
||||
"{{ let items = {field_place}.iter().map(|item| {inner}).collect::<PyResult<Vec<_>>>()?; Ok(vm.ctx.new_list(items).into()) }}"
|
||||
)
|
||||
} else if optional {
|
||||
let inner = leaf_to_py(registry, type_id, "v");
|
||||
format!("match &{field_place} {{ None => Ok(vm.ctx.none()), Some(v) => {inner} }}")
|
||||
} else {
|
||||
leaf_to_py(registry, type_id, &format!("&{field_place}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// `py_name` is the dict key to read; `keep_expr` is the expression to use
|
||||
/// when the key is absent (or explicitly `None`, for non-`Option` fields) —
|
||||
/// the fresh constructor's value for `ocs.add()`, the field's current value
|
||||
/// (`{var}.{field}.clone()`) for `ocs.update()`, or a diverging `return Err(..)` block for
|
||||
/// a required-but-missing `ocs.add()` field. Result: a plain (non-`PyResult`)
|
||||
/// expression of the field's own Rust type (`T`, `Vec<T>`, or `Option<T>`).
|
||||
fn setter_for_field(
|
||||
registry: &TypeRegistry,
|
||||
type_id: &str,
|
||||
optional: bool,
|
||||
is_sequence: bool,
|
||||
py_name: &str,
|
||||
keep_expr: &str,
|
||||
) -> String {
|
||||
if is_sequence {
|
||||
let inner = leaf_from_py(registry, type_id, "item");
|
||||
format!(
|
||||
"match dict.get_item_opt(\"{py_name}\", vm)? {{ None => {keep_expr}, Some(v) => {{ let items: Vec<PyObjectRef> = v.try_into_value(vm)?; items.into_iter().map(|item| {inner}).collect::<PyResult<Vec<_>>>()? }} }}"
|
||||
)
|
||||
} else if optional {
|
||||
let inner = leaf_from_py(registry, type_id, "v");
|
||||
format!(
|
||||
"match dict.get_item_opt(\"{py_name}\", vm)? {{ None => {keep_expr}, Some(v) if vm.is_none(&v) => None, Some(v) => Some({inner}?) }}"
|
||||
)
|
||||
} else {
|
||||
let inner = leaf_from_py(registry, type_id, "v");
|
||||
format!(
|
||||
"match dict.get_item_opt(\"{py_name}\", vm)? {{ None => {keep_expr}, Some(v) if vm.is_none(&v) => {keep_expr}, Some(v) => {inner}? }}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn field_default_expr(f: &FieldInfo) -> String {
|
||||
if f.is_sequence {
|
||||
"Vec::new()".to_string()
|
||||
} else if f.optional {
|
||||
"None".to_string()
|
||||
} else {
|
||||
default_expr(f.type_id.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A diverging block expression: aborts `dict_to_entity` with a clear error
|
||||
/// instead of producing a value. Valid anywhere an expression of any type is
|
||||
/// expected (Rust's `!` coerces to everything), so it drops straight into a
|
||||
/// `match` arm exactly like a real value would.
|
||||
fn required_error_expr(entity_kind: &str, py_name: &str) -> String {
|
||||
format!(
|
||||
"{{ return Err(vm.new_value_error(\"ocs.add: {entity_kind} requires \\\"{py_name}\\\"\".to_owned())); }}"
|
||||
)
|
||||
}
|
||||
|
||||
/// Wraps an already-built sequence-field setter expression with a
|
||||
/// non-empty check, for a required sequence field (e.g. a polyline's
|
||||
/// `vertices`) — covers both "key missing" and "key present but `[]`" in
|
||||
/// one place, since both parse to the same empty `Vec`.
|
||||
fn wrap_required_nonempty(setter_expr: &str, entity_kind: &str, py_name: &str) -> String {
|
||||
format!(
|
||||
"{{ let parsed = {setter_expr}; if parsed.is_empty() {{ return Err(vm.new_value_error(\"ocs.add: {entity_kind} requires at least one \\\"{py_name}\\\"\".to_owned())); }} parsed }}"
|
||||
)
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Per-helper-type generation
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
fn gen_unit_enum(name: &str, info: &TypeInfo) -> String {
|
||||
let path = format!("acadrust::entities::{name}");
|
||||
let snake = snake_case(name);
|
||||
let mut name_arms = String::new();
|
||||
let mut parse_arms = String::new();
|
||||
for v in &info.variants {
|
||||
name_arms.push_str(&format!(" {path}::{v} => \"{v}\",\n", v = v.name));
|
||||
parse_arms.push_str(&format!(" \"{v}\" => Ok({path}::{v}),\n", v = v.name));
|
||||
}
|
||||
let first_variant = info
|
||||
.variants
|
||||
.first()
|
||||
.unwrap_or_else(|| panic!("{name}: unit enum has no variants"))
|
||||
.name
|
||||
.clone();
|
||||
format!(
|
||||
r#"fn {snake}_name(value: &{path}) -> &'static str {{
|
||||
match value {{
|
||||
{name_arms} }}
|
||||
}}
|
||||
|
||||
fn str_to_{snake}(value: String, vm: &VirtualMachine) -> PyResult<{path}> {{
|
||||
match value.as_str() {{
|
||||
{parse_arms} other => Err(vm.new_value_error(format!("ocs: unsupported {name} value: {{other}}"))),
|
||||
}}
|
||||
}}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn default_{snake}() -> {path} {{
|
||||
{path}::{first_variant}
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn gen_bitflags(name: &str) -> String {
|
||||
let path = format!("acadrust::entities::{name}");
|
||||
let snake = snake_case(name);
|
||||
format!(
|
||||
r#"#[allow(dead_code)]
|
||||
fn default_{snake}() -> {path} {{
|
||||
{path}::new()
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn gen_tagged_enum(name: &str, info: &TypeInfo, registry: &TypeRegistry) -> String {
|
||||
let path = format!("acadrust::entities::{name}");
|
||||
let snake = snake_case(name);
|
||||
let mut to_arms = String::new();
|
||||
let mut from_arms = String::new();
|
||||
for variant in &info.variants {
|
||||
let payload = &variant.fields[0];
|
||||
let getter = leaf_to_py(registry, payload.type_id.as_str(), "value");
|
||||
let setter = leaf_from_py(registry, payload.type_id.as_str(), "payload");
|
||||
to_arms.push_str(&format!(
|
||||
" {path}::{variant}(value) => {{ let dict = vm.ctx.new_dict(); dict.set_item(\"kind\", vm.new_pyobj(\"{variant}\"), vm)?; dict.set_item(\"value\", {getter}?, vm)?; Ok(dict.into()) }}\n",
|
||||
variant = variant.name,
|
||||
));
|
||||
from_arms.push_str(&format!(
|
||||
" \"{variant}\" => {{ let payload = dict.get_item(\"value\", vm)?; Ok({path}::{variant}({setter}?)) }}\n",
|
||||
variant = variant.name,
|
||||
));
|
||||
}
|
||||
let first = &info.variants[0];
|
||||
let default_payload = default_expr(first.fields[0].type_id.as_str());
|
||||
format!(
|
||||
r#"fn {snake}_to_dict(vm: &VirtualMachine, value: &{path}) -> PyResult<PyObjectRef> {{
|
||||
match value {{
|
||||
{to_arms} }}
|
||||
}}
|
||||
|
||||
fn dict_to_{snake}(value: PyObjectRef, vm: &VirtualMachine) -> PyResult<{path}> {{
|
||||
let dict = value.try_into_value::<rustpython_vm::builtins::PyDictRef>(vm)?;
|
||||
let kind = dict.get_item("kind", vm)?.try_into_value::<String>(vm)?;
|
||||
match kind.as_str() {{
|
||||
{from_arms} other => Err(vm.new_value_error(format!("ocs: unsupported {name} kind: {{other}}"))),
|
||||
}}
|
||||
}}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn default_{snake}() -> {path} {{
|
||||
{path}::{first_variant}({default_payload})
|
||||
}}
|
||||
|
||||
"#,
|
||||
first_variant = first.name,
|
||||
)
|
||||
}
|
||||
|
||||
fn gen_struct(name: &str, info: &TypeInfo, registry: &TypeRegistry) -> String {
|
||||
let path = format!("acadrust::entities::{name}");
|
||||
let snake = snake_case(name);
|
||||
let mut to_dict_body = String::new();
|
||||
let mut from_dict_fields = String::new();
|
||||
let mut default_fields = String::new();
|
||||
for f in &info.fields {
|
||||
let getter = getter_for_field(
|
||||
registry,
|
||||
f.type_id.as_str(),
|
||||
f.optional,
|
||||
f.is_sequence,
|
||||
&format!("value.{}", f.name),
|
||||
);
|
||||
to_dict_body.push_str(&format!(
|
||||
" dict.set_item(\"{fname}\", {getter}?, vm)?;\n",
|
||||
fname = f.name
|
||||
));
|
||||
let setter = setter_for_field(
|
||||
registry,
|
||||
f.type_id.as_str(),
|
||||
f.optional,
|
||||
f.is_sequence,
|
||||
&f.name,
|
||||
&field_default_expr(f),
|
||||
);
|
||||
from_dict_fields.push_str(&format!(" {fname}: {setter},\n", fname = f.name));
|
||||
default_fields.push_str(&format!(
|
||||
" {fname}: {def},\n",
|
||||
fname = f.name,
|
||||
def = field_default_expr(f)
|
||||
));
|
||||
}
|
||||
format!(
|
||||
r#"fn {snake}_to_dict(vm: &VirtualMachine, value: &{path}) -> PyResult<PyObjectRef> {{
|
||||
let dict = vm.ctx.new_dict();
|
||||
{to_dict_body} Ok(dict.into())
|
||||
}}
|
||||
|
||||
fn dict_to_{snake}(value: PyObjectRef, vm: &VirtualMachine) -> PyResult<{path}> {{
|
||||
let dict = value.try_into_value::<rustpython_vm::builtins::PyDictRef>(vm)?;
|
||||
Ok({path} {{
|
||||
{from_dict_fields} }})
|
||||
}}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn default_{snake}() -> {path} {{
|
||||
{path} {{
|
||||
{default_fields} }}
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Top-level entity fields (registry + manifest overrides)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
struct ResolvedEntityField {
|
||||
py_name: String,
|
||||
/// A `PyResult<PyObjectRef>` expression.
|
||||
getter: String,
|
||||
/// A full, semicolon-terminated Rust statement.
|
||||
setter: String,
|
||||
}
|
||||
|
||||
/// `ocs.add()` builds an entity from scratch (missing fields fall back to a
|
||||
/// type default, or error if `required`); `ocs.update()` merges onto an
|
||||
/// already-`.clone()`d existing entity (missing fields simply keep whatever
|
||||
/// `var` already holds — see [[project_opencad_python_generic_entity_crud_plan]]
|
||||
/// in this session's memory for why: reconstructing from a partial dict on
|
||||
/// update could silently reset properties this generator doesn't represent
|
||||
/// at all, e.g. `EntityCommon`'s color/line-weight/XDATA).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Add,
|
||||
Update,
|
||||
}
|
||||
|
||||
fn resolve_entity_fields(
|
||||
manifest: &Manifest,
|
||||
registry: &TypeRegistry,
|
||||
kind: &str,
|
||||
info: &TypeInfo,
|
||||
var: &str,
|
||||
mode: Mode,
|
||||
) -> Vec<ResolvedEntityField> {
|
||||
let override_def = manifest.overrides.get(kind).cloned().unwrap_or_default();
|
||||
let mut out = Vec::new();
|
||||
for f in &info.fields {
|
||||
if f.name == "common" {
|
||||
continue;
|
||||
}
|
||||
let field_override = override_def.fields.get(&f.name).cloned();
|
||||
if let Some(ov) = &field_override {
|
||||
if ov.exclude {
|
||||
continue;
|
||||
}
|
||||
if let (Some(getter_expr), Some(setter_stmt)) = (&ov.rust_getter, &ov.rust_setter) {
|
||||
assert!(!ov.read_only, "{kind}.{} cannot be read_only with a rust_setter", f.name);
|
||||
assert!(
|
||||
!ov.required,
|
||||
"entity_manifest.json: {kind}.{}: \"required\" only applies to the generic \
|
||||
field path, not a rust_getter/rust_setter override",
|
||||
f.name
|
||||
);
|
||||
let py_name = ov.python_name.clone().unwrap_or_else(|| f.name.clone());
|
||||
let getter = format!("Ok(vm.new_pyobj({}))", getter_expr.replace("VAR", var));
|
||||
let setter = setter_stmt.replace("VAR", var);
|
||||
out.push(ResolvedEntityField { py_name, getter, setter });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let py_name = field_override
|
||||
.as_ref()
|
||||
.and_then(|o| o.python_name.clone())
|
||||
.unwrap_or_else(|| f.name.clone());
|
||||
let required = field_override.as_ref().map(|o| o.required).unwrap_or(false);
|
||||
let getter = getter_for_field(
|
||||
registry,
|
||||
f.type_id.as_str(),
|
||||
f.optional,
|
||||
f.is_sequence,
|
||||
&format!("{var}.{}", f.name),
|
||||
);
|
||||
if field_override.as_ref().is_some_and(|ov| ov.read_only) {
|
||||
let setter = format!("if dict.get_item_opt(\"{py_name}\", vm)?.is_some() {{ return Err(vm.new_value_error(\"{kind}.{py_name} is read-only\".to_owned())); }}");
|
||||
out.push(ResolvedEntityField { py_name, getter, setter });
|
||||
continue;
|
||||
}
|
||||
let keep_expr = match mode {
|
||||
Mode::Update => format!("{var}.{}.clone()", f.name),
|
||||
Mode::Add if required && !f.is_sequence => required_error_expr(kind, &py_name),
|
||||
Mode::Add => format!("{var}.{}.clone()", f.name),
|
||||
};
|
||||
let mut setter_expr =
|
||||
setter_for_field(registry, f.type_id.as_str(), f.optional, f.is_sequence, &py_name, &keep_expr);
|
||||
if mode == Mode::Add && required && f.is_sequence {
|
||||
setter_expr = wrap_required_nonempty(&setter_expr, kind, &py_name);
|
||||
}
|
||||
let setter = format!("{var}.{} = {setter_expr};", f.name);
|
||||
out.push(ResolvedEntityField { py_name, getter, setter });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn gen_entity_to_dict(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||
let mut arms = String::new();
|
||||
for kind in &manifest.type_filter {
|
||||
let info = get(registry, kind).unwrap();
|
||||
let var = "value";
|
||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Add);
|
||||
let mut body = String::new();
|
||||
body.push_str(" let dict = vm.ctx.new_dict();\n");
|
||||
body.push_str(&format!(" dict.set_item(\"kind\", vm.new_pyobj(\"{kind}\"), vm)?;\n"));
|
||||
body.push_str(&format!(
|
||||
" dict.set_item(\"handle\", vm.new_pyobj({var}.common.handle.value()), vm)?;\n"
|
||||
));
|
||||
body.push_str(&format!(
|
||||
" dict.set_item(\"layer\", vm.new_pyobj({var}.common.layer.clone()), vm)?;\n"
|
||||
));
|
||||
if manifest.base_fields.iter().any(|field| field == "owner_handle") {
|
||||
body.push_str(&format!(
|
||||
" dict.set_item(\"owner_handle\", vm.new_pyobj({var}.common.owner_handle.value()), vm)?;\n"
|
||||
));
|
||||
}
|
||||
for f in &fields {
|
||||
body.push_str(&format!(
|
||||
" dict.set_item(\"{}\", {}?, vm)?;\n",
|
||||
f.py_name, f.getter
|
||||
));
|
||||
}
|
||||
body.push_str(" Ok(dict.into())\n");
|
||||
arms.push_str(&format!(
|
||||
" acadrust::EntityType::{kind}({var}) => {{\n{body} }}\n"
|
||||
));
|
||||
}
|
||||
format!(
|
||||
r#"/// Convert any generic-CRUD-supported entity into a plain dict with a
|
||||
/// `"kind"` key (matching the convention every other `ocs.*` dict already
|
||||
/// uses, e.g. XDATA's `{{"kind": ..., "value": ...}}`), plus `"handle"` and
|
||||
/// `"layer"` and every entity-specific field the type registry knows about.
|
||||
pub(crate) fn entity_to_dict(vm: &VirtualMachine, entity: &acadrust::EntityType) -> PyResult<PyObjectRef> {{
|
||||
match entity {{
|
||||
{arms} _ => Err(vm.new_value_error(
|
||||
"ocs: this entity kind is not supported by the generic add/update API yet".to_owned(),
|
||||
)),
|
||||
}}
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn gen_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||
let mut arms = String::new();
|
||||
for kind in &manifest.type_filter {
|
||||
if manifest.overrides.get(kind).is_some_and(|override_def| override_def.update_only) {
|
||||
continue;
|
||||
}
|
||||
let info = get(registry, kind).unwrap();
|
||||
let var = "value";
|
||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Add);
|
||||
let mut body = String::new();
|
||||
let mut allowed = vec!["kind", "handle", "layer"];
|
||||
if manifest.base_fields.iter().any(|field| field == "owner_handle") {
|
||||
allowed.push("owner_handle");
|
||||
}
|
||||
allowed.extend(fields.iter().map(|field| field.py_name.as_str()));
|
||||
body.push_str(&format!(
|
||||
" ensure_known_entity_keys(dict, \"{kind}\", &{:?}, vm)?;\n",
|
||||
allowed
|
||||
));
|
||||
let constructor = manifest.overrides.get(kind)
|
||||
.and_then(|override_def| override_def.rust_constructor.as_deref())
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| format!("acadrust::entities::{kind}::default()"));
|
||||
body.push_str(&format!(" let mut {var} = {constructor};\n"));
|
||||
body.push_str(&format!(
|
||||
" {var}.common.handle = acadrust::Handle::new(get_opt_u64(dict, \"handle\", vm)?);\n"
|
||||
));
|
||||
body.push_str(&format!(
|
||||
" {var}.common.layer = get_opt_string(dict, \"layer\", \"0\", vm)?;\n"
|
||||
));
|
||||
if manifest.base_fields.iter().any(|field| field == "owner_handle") {
|
||||
body.push_str(&format!(
|
||||
" {var}.common.owner_handle = acadrust::Handle::new(get_opt_u64(dict, \"owner_handle\", vm)?);\n"
|
||||
));
|
||||
}
|
||||
for f in &fields {
|
||||
body.push_str(&format!(" {}\n", f.setter));
|
||||
}
|
||||
body.push_str(&format!(" acadrust::EntityType::{kind}({var})\n"));
|
||||
arms.push_str(&format!(" \"{kind}\" => {{\n{body} }}\n"));
|
||||
}
|
||||
format!(
|
||||
r#"/// Build a brand-new entity from a dict with a `"kind"` key (see
|
||||
/// `entity_to_dict`) — the `ocs.add()` direction. Fields the manifest marks
|
||||
/// `"required"` (the geometry that actually defines the shape — a line's
|
||||
/// endpoints, a circle's center/radius, ...) error if missing rather than
|
||||
/// silently defaulting to zero; every other field defaults sensibly (what a
|
||||
/// fresh `acadrust::entities::X::default()` would already hold).
|
||||
pub(crate) fn dict_to_entity(dict: &rustpython_vm::builtins::PyDictRef, vm: &VirtualMachine) -> PyResult<acadrust::EntityType> {{
|
||||
let kind = dict.get_item("kind", vm)?.try_into_value::<String>(vm)?;
|
||||
Ok(match kind.as_str() {{
|
||||
{arms} other => {{
|
||||
return Err(vm.new_value_error(format!("ocs: unsupported entity kind: {{other}}")));
|
||||
}}
|
||||
}})
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn gen_apply_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||
let mut arms = String::new();
|
||||
for kind in &manifest.type_filter {
|
||||
let info = get(registry, kind).unwrap();
|
||||
let var = "value";
|
||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Update);
|
||||
let mut body = String::new();
|
||||
let mut allowed = vec!["kind", "handle", "layer"];
|
||||
if manifest.base_fields.iter().any(|field| field == "owner_handle") {
|
||||
allowed.push("owner_handle");
|
||||
}
|
||||
allowed.extend(fields.iter().map(|field| field.py_name.as_str()));
|
||||
body.push_str(&format!(
|
||||
" ensure_known_entity_keys(dict, \"{kind}\", &{:?}, vm)?;\n",
|
||||
allowed
|
||||
));
|
||||
body.push_str(&format!(" let mut {var} = existing_value.clone();\n"));
|
||||
// `handle` is never touched — an update can't rehome an entity's
|
||||
// identity. `layer` (unlike every other field) is promoted onto
|
||||
// every dict by `entity_to_dict`/read by `dict_to_entity` without a
|
||||
// manifest entry, so it's handled by hand here too, the same way
|
||||
// `dict_to_entity` hand-writes it above — merge semantics: keep the
|
||||
// existing layer unless the dict actually specifies one.
|
||||
body.push_str(&format!(
|
||||
" {var}.common.layer = match dict.get_item_opt(\"layer\", vm)? {{ \
|
||||
None => {var}.common.layer.clone(), \
|
||||
Some(v) if vm.is_none(&v) => {var}.common.layer.clone(), \
|
||||
Some(v) => v.try_into_value::<String>(vm)? }};\n"
|
||||
));
|
||||
if manifest.base_fields.iter().any(|field| field == "owner_handle") {
|
||||
body.push_str(&format!(
|
||||
" if let Some(owner) = dict.get_item_opt(\"owner_handle\", vm)? {{ \
|
||||
let owner = owner.try_into_value::<u64>(vm)?; \
|
||||
if owner != {var}.common.owner_handle.value() {{ \
|
||||
return Err(vm.new_value_error(\"owner_handle is read-only after creation\".to_owned())); }} }};\n"
|
||||
));
|
||||
}
|
||||
for f in &fields {
|
||||
body.push_str(&format!(" {}\n", f.setter));
|
||||
}
|
||||
body.push_str(&format!(" acadrust::EntityType::{kind}({var})\n"));
|
||||
arms.push_str(&format!(
|
||||
" acadrust::EntityType::{kind}(existing_value) => {{\n{body} }}\n"
|
||||
));
|
||||
}
|
||||
format!(
|
||||
r#"/// Apply a dict's keys onto a *clone* of `existing` — the `ocs.update()`
|
||||
/// direction. Unlike `dict_to_entity`, a key missing from `dict` means
|
||||
/// "leave this field as it already was", not "reset it to a type default":
|
||||
/// reconstructing a fresh entity from a necessarily-partial dict would
|
||||
/// silently drop whatever this generator can't represent at all (most of
|
||||
/// `EntityCommon` — color, line weight, XDATA, ...) the moment a script
|
||||
/// updates just one field. `existing`'s own kind is authoritative; a `dict`
|
||||
/// `"kind"` (if present at all — round-tripped from a prior `get_entity()`)
|
||||
/// is never consulted, since an update can't change an entity's shape.
|
||||
pub(crate) fn apply_dict_to_entity(
|
||||
existing: &acadrust::EntityType,
|
||||
dict: &rustpython_vm::builtins::PyDictRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<acadrust::EntityType> {{
|
||||
Ok(match existing {{
|
||||
{arms} _ => {{
|
||||
return Err(vm.new_value_error(
|
||||
"ocs: this entity kind is not supported by the generic add/update API yet"
|
||||
.to_owned(),
|
||||
));
|
||||
}}
|
||||
}})
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Entry point
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
pub fn generate_entity_crud(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(
|
||||
"// @generated by opencad-python/build.rs from entity_manifest.json + the\n\
|
||||
// ocs_plugin_api embedded type registry. Do not edit by hand — edit\n\
|
||||
// entity_manifest.json or build/generate.rs instead.\n\n",
|
||||
);
|
||||
|
||||
for name in helper_closure(manifest, registry) {
|
||||
let info = get(registry, &name).unwrap();
|
||||
match classify(&name, info) {
|
||||
Classify::UnitEnum => out.push_str(&gen_unit_enum(&name, info)),
|
||||
Classify::TaggedEnum => out.push_str(&gen_tagged_enum(&name, info, registry)),
|
||||
Classify::BitFlags => out.push_str(&gen_bitflags(&name)),
|
||||
Classify::Struct => out.push_str(&gen_struct(&name, info, registry)),
|
||||
}
|
||||
}
|
||||
|
||||
out.push_str(&gen_entity_to_dict(manifest, registry));
|
||||
out.push_str(&gen_dict_to_entity(manifest, registry));
|
||||
out.push_str(&gen_apply_dict_to_entity(manifest, registry));
|
||||
out
|
||||
}
|
||||
Reference in new issue
Block a user