forked from 1b-agent/opencadstudio-upstream
RegionFromProfile reuses the REGION command's kernel calls; doc.solids.region exposes it with layer and delete_source options. The lifecycle test builds regions from real circle and polyline entities and covers refusals, a move, both formats and undo. create_entity now accepts coordinate tuples for point properties and every generated float setter accepts Python ints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1062 lines
46 KiB
Rust
1062 lines
46 KiB
Rust
// 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 {
|
|
/// Kinds whose payload is a nested enum: converted by hand-written
|
|
/// `<snake>_to_dict`/`<snake>_from_dict`/`<snake>_apply` functions.
|
|
#[serde(default)]
|
|
pub manual_kinds: Vec<String>,
|
|
/// 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,
|
|
/// Message returned when a script tries to create an update-only kind.
|
|
#[serde(default)]
|
|
pub update_only_message: Option<String>,
|
|
#[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,
|
|
/// Still generate this field type's converters although a
|
|
/// `rust_getter`/`rust_setter` override replaces the generic path (the
|
|
/// override's hand-written code calls them).
|
|
#[serde(default)]
|
|
pub keep_types: 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", "Color"];
|
|
|
|
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
|
|
}
|
|
}
|
|
// acadrust `bitflags!` types serialize as a newtype over their integer
|
|
// bits; they convert exactly like the hand-written `bits` structs.
|
|
TypeKind::Newtype => Classify::BitFlags,
|
|
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 {
|
|
if manifest.manual_kinds.contains(kind) {
|
|
continue;
|
|
}
|
|
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() && !o.keep_types))
|
|
.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 let Some(parts) = tuple_parts(&name) {
|
|
queue.extend(parts.into_iter().map(str::to_owned));
|
|
continue;
|
|
}
|
|
if BUILTIN.contains(&name.as_str())
|
|
|| SPECIAL.contains(&name.as_str())
|
|
|| f64_array_len(&name).is_some()
|
|
{
|
|
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 {
|
|
if f.type_id.as_str() != "EntityCommon" {
|
|
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}"),
|
|
}
|
|
}
|
|
|
|
/// Rust path of a traced type; most live in `acadrust::entities`.
|
|
fn type_path(name: &str) -> String {
|
|
match name {
|
|
"LineWeight" => "acadrust::types::LineWeight".to_owned(),
|
|
"LeaderLineBreakInfo" => "acadrust::entities::multileader::LeaderLineBreakInfo".to_owned(),
|
|
_ => format!("acadrust::entities::{name}"),
|
|
}
|
|
}
|
|
|
|
fn is_native(type_id: &str) -> bool {
|
|
BUILTIN.contains(&type_id)
|
|
}
|
|
|
|
/// Split a tuple type id such as `(u32,CellBorder)` into its element types.
|
|
fn tuple_parts(type_id: &str) -> Option<Vec<&str>> {
|
|
let inner = type_id.strip_prefix('(')?.strip_suffix(')')?;
|
|
let mut parts = Vec::new();
|
|
let (mut depth, mut start) = (0i32, 0usize);
|
|
for (i, ch) in inner.char_indices() {
|
|
match ch {
|
|
'(' | '[' | '<' => depth += 1,
|
|
')' | ']' | '>' => depth -= 1,
|
|
',' if depth == 0 => {
|
|
parts.push(inner[start..i].trim());
|
|
start = i + 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
parts.push(inner[start..].trim());
|
|
(parts.len() >= 2).then_some(parts)
|
|
}
|
|
|
|
/// `[f64; N]` fixed arrays (e.g. a 4x4 transform) convert as flat float lists.
|
|
fn f64_array_len(type_id: &str) -> Option<usize> {
|
|
type_id
|
|
.strip_prefix("[f64; ")?
|
|
.strip_suffix(']')?
|
|
.parse()
|
|
.ok()
|
|
}
|
|
|
|
fn default_expr(type_id: &str) -> String {
|
|
if let Some(n) = f64_array_len(type_id) {
|
|
return format!("[0.0f64; {n}]");
|
|
}
|
|
if let Some(parts) = tuple_parts(type_id) {
|
|
let items: Vec<String> = parts.iter().map(|part| default_expr(part)).collect();
|
|
return format!("({})", items.join(", "));
|
|
}
|
|
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(),
|
|
"Color" => "acadrust::types::Color::ByLayer".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})");
|
|
if f64_array_len(type_id).is_some() {
|
|
return format!(
|
|
"Ok(vm.ctx.new_list(({item_expr}).iter().map(|item| vm.new_pyobj(*item)).collect()).into())"
|
|
);
|
|
}
|
|
if let Some(parts) = tuple_parts(type_id) {
|
|
let items: Vec<String> = parts
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, part)| format!("{}?", leaf_to_py(registry, part, &format!("&({item_expr}).{i}"))))
|
|
.collect();
|
|
return format!("Ok(vm.ctx.new_list(vec![{}]).into())", items.join(", "));
|
|
}
|
|
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()))"),
|
|
"Color" => format!("color_to_py_dict(vm, {item_expr})"),
|
|
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 type_id == "f64" {
|
|
// RustPython's f64 conversion rejects ints; scripts write `radius=5`.
|
|
return format!("py_number_to_f64({value_expr}, vm)");
|
|
}
|
|
if type_id == "u64" {
|
|
// serde reports Rust `usize` as `u64`; the inferred cast serves both.
|
|
return format!("{value_expr}.try_into_value::<u64>(vm).map(|n| n as _)");
|
|
}
|
|
if is_native(type_id) {
|
|
return format!("{value_expr}.try_into_value::<{}>(vm)", native_rust_type(type_id));
|
|
}
|
|
if let Some(n) = f64_array_len(type_id) {
|
|
return format!("py_to_f64_array::<{n}>({value_expr}, vm)");
|
|
}
|
|
if let Some(parts) = tuple_parts(type_id) {
|
|
let n = parts.len();
|
|
let items: Vec<String> = parts
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, part)| format!("{}?", leaf_from_py(registry, part, &format!("items.remove(0 * {i})"))))
|
|
.collect();
|
|
return format!(
|
|
"(|| -> PyResult<_> {{ let mut items: Vec<PyObjectRef> = ({value_expr}).try_into_value(vm)?; if items.len() != {n} {{ return Err(vm.new_value_error(\"expected a {n}-item list\".to_owned())); }} Ok(({})) }})()",
|
|
items.join(", ")
|
|
);
|
|
}
|
|
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)"),
|
|
"Color" => format!("py_to_color_dict({value_expr}, vm)"),
|
|
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 = type_path(other);
|
|
let bits_ty = bits_field_type(info);
|
|
if matches!(info.kind, TypeKind::Newtype) {
|
|
// bitflags 2: `from_bits` is fallible and rejects unknown bits.
|
|
format!(
|
|
"{value_expr}.try_into_value::<i64>(vm).and_then(|n| <{bits_ty}>::try_from(n).ok().and_then({path}::from_bits).ok_or_else(|| vm.new_value_error(\"{other} has unknown or out-of-range bits\".to_owned())))"
|
|
)
|
|
} else {
|
|
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 {
|
|
// The registry reports a fixed array as a sequence of itself.
|
|
let is_sequence = is_sequence && f64_array_len(type_id).is_none();
|
|
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 {
|
|
let is_sequence = is_sequence && f64_array_len(type_id).is_none();
|
|
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 && f64_array_len(f.type_id.as_str()).is_none() {
|
|
"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 = type_path(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, info: &TypeInfo) -> String {
|
|
let path = type_path(name);
|
|
let snake = snake_case(name);
|
|
let ctor = if matches!(info.kind, TypeKind::Newtype) { "empty" } else { "new" };
|
|
format!(
|
|
r#"#[allow(dead_code)]
|
|
fn default_{snake}() -> {path} {{
|
|
{path}::{ctor}()
|
|
}}
|
|
|
|
"#
|
|
)
|
|
}
|
|
|
|
fn gen_tagged_enum(name: &str, info: &TypeInfo, registry: &TypeRegistry) -> String {
|
|
let path = type_path(name);
|
|
let snake = snake_case(name);
|
|
let mut to_arms = String::new();
|
|
let mut from_arms = String::new();
|
|
for variant in &info.variants {
|
|
if variant.fields.is_empty() {
|
|
to_arms.push_str(&format!(
|
|
" {path}::{variant} => {{ let dict = vm.ctx.new_dict(); dict.set_item(\"kind\", vm.new_pyobj(\"{variant}\"), vm)?; Ok(dict.into()) }}\n",
|
|
variant = variant.name,
|
|
));
|
|
from_arms.push_str(&format!(
|
|
" \"{variant}\" => Ok({path}::{variant}),\n",
|
|
variant = variant.name,
|
|
));
|
|
continue;
|
|
}
|
|
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_value = match first.fields.first() {
|
|
None => format!("{path}::{}", first.name),
|
|
Some(payload) => format!("{path}::{}({})", first.name, default_expr(payload.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} {{
|
|
{default_value}
|
|
}}
|
|
|
|
"#
|
|
)
|
|
}
|
|
|
|
fn gen_struct(name: &str, info: &TypeInfo, registry: &TypeRegistry) -> String {
|
|
let path = type_path(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 {
|
|
if f.type_id.as_str() == "EntityCommon" {
|
|
// A sub-record's own common data (layer, XDATA, handles) is not part
|
|
// of the scripted model; it defaults on the way in.
|
|
let default = "acadrust::entities::EntityCommon::default()";
|
|
from_dict_fields.push_str(&format!(" {}: {default},\n", f.name));
|
|
default_fields.push_str(&format!(" {}: {default},\n", f.name));
|
|
continue;
|
|
}
|
|
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 {
|
|
if manifest.manual_kinds.contains(kind) {
|
|
arms.push_str(&format!(
|
|
" acadrust::EntityType::{kind}(value) => {}_to_dict(vm, value),\n",
|
|
snake_case(kind)
|
|
));
|
|
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();
|
|
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 let Some(override_def) = manifest.overrides.get(kind).filter(|o| o.update_only) {
|
|
let message = override_def.update_only_message.clone().unwrap_or_else(|| {
|
|
format!("{kind} cannot be created through the generic API; only existing {kind} entities can be edited")
|
|
});
|
|
arms.push_str(&format!(
|
|
" \"{kind}\" => {{ return Err(vm.new_value_error({message:?}.to_owned())); }}\n"
|
|
));
|
|
continue;
|
|
}
|
|
if manifest.manual_kinds.contains(kind) {
|
|
arms.push_str(&format!(
|
|
" \"{kind}\" => acadrust::EntityType::{kind}({}_from_dict(dict, vm)?),\n",
|
|
snake_case(kind)
|
|
));
|
|
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 {
|
|
if manifest.manual_kinds.contains(kind) {
|
|
arms.push_str(&format!(
|
|
" acadrust::EntityType::{kind}(existing_value) => acadrust::EntityType::{kind}({}_apply(existing_value, dict, vm)?),\n",
|
|
snake_case(kind)
|
|
));
|
|
continue;
|
|
}
|
|
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, info)),
|
|
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
|
|
}
|