Add nested attribute entity host support

This commit is contained in:
Felix Riestra committed 2026-09-19 13:16:40 +02:00
1 parent 5ef7000b67
commit cf2a923568
6 files changed
+2509 -541

No files matched your search

+219 -53
View File
@@ -17,7 +17,9 @@ include!("src/type_registry_types.rs");
mod entity_coverage_schema {
include!("src/entity_coverage_types.rs");
}
use entity_coverage_schema::{EntityCoverageCatalog, EntityKindCoverage, EntityScope, ModelAccess, PropertyCoverage};
use entity_coverage_schema::{
EntityCoverageCatalog, EntityKindCoverage, EntityScope, ModelAccess, PropertyCoverage,
};
#[derive(serde::Deserialize)]
struct EntityCoveragePolicy {
@@ -506,41 +508,71 @@ fn generate_type_registry(out_dir: &Path) {
fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
use std::collections::HashSet;
let policy: EntityCoveragePolicy = serde_json::from_str(include_str!("entity_coverage_policy.json"))
let policy: EntityCoveragePolicy =
serde_json::from_str(include_str!("entity_coverage_policy.json"))
.expect("valid entity coverage policy");
let variants = &registry.types[&TypeId::new("EntityType")].variants;
let variant_names: HashSet<&str> = variants.iter().map(|v| v.name.as_str()).collect();
for name in policy.internal_kinds.iter().chain(&policy.opaque_kinds)
.chain(policy.editable.keys()).chain(policy.readable.keys()) {
assert!(variant_names.contains(name.as_str()), "coverage policy names unknown kind: {name}");
for name in policy
.internal_kinds
.iter()
.chain(&policy.opaque_kinds)
.chain(policy.editable.keys())
.chain(policy.readable.keys())
{
assert!(
variant_names.contains(name.as_str()),
"coverage policy names unknown kind: {name}"
);
}
for name in &policy.internal_kinds {
assert!(!policy.opaque_kinds.contains(name) && !policy.editable.contains_key(name),
"internal kind has conflicting coverage: {name}");
assert!(
!policy.opaque_kinds.contains(name) && !policy.editable.contains_key(name),
"internal kind has conflicting coverage: {name}"
);
}
for name in &policy.opaque_kinds {
assert!(!policy.editable.contains_key(name), "opaque kind is editable: {name}");
assert!(
!policy.editable.contains_key(name),
"opaque kind is editable: {name}"
);
}
for name in policy.aliases.keys() {
assert!(policy.editable.contains_key(name), "aliases for non-editable kind: {name}");
assert!(
policy.editable.contains_key(name),
"aliases for non-editable kind: {name}"
);
}
let common = &registry.types[&TypeId::new("EntityCommon")];
let mut entity_kinds = Vec::with_capacity(variants.len());
for variant in variants {
let kind = &variant.name;
let scope = if policy.internal_kinds.contains(kind) { EntityScope::Internal }
else if policy.opaque_kinds.contains(kind) { EntityScope::Opaque }
else { EntityScope::Canvas };
let scope = if policy.internal_kinds.contains(kind) {
EntityScope::Internal
} else if policy.opaque_kinds.contains(kind) {
EntityScope::Opaque
} else {
EntityScope::Canvas
};
let editable = policy.editable.get(kind).cloned().unwrap_or_default();
let readable = policy.readable.get(kind).cloned().unwrap_or_default();
let mut unresolved: HashSet<String> = editable.iter().chain(&readable).cloned().collect();
assert_eq!(unresolved.len(), editable.len() + readable.len(), "duplicate mapped property in {kind}");
assert_eq!(
unresolved.len(),
editable.len() + readable.len(),
"duplicate mapped property in {kind}"
);
let aliases = policy.aliases.get(kind);
let mut properties = vec![PropertyCoverage {
name: "kind".into(), source_path: "<variant>".into(), type_id: "String".into(),
optional: false, is_sequence: false, snapshot_readable: true,
model_access: ModelAccess::ReadOnly, validation: "none".into(),
name: "kind".into(),
source_path: "<variant>".into(),
type_id: "String".into(),
optional: false,
is_sequence: false,
snapshot_readable: true,
model_access: ModelAccess::ReadOnly,
validation: "none".into(),
}];
for field in &common.fields {
let (name, access) = match field.name.as_str() {
@@ -549,69 +581,203 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
"layer" if scope == EntityScope::Canvas => {
unresolved.remove("layer");
("layer".to_owned(), ModelAccess::ReadWrite)
},
}
"layer" => ("layer".to_owned(), ModelAccess::ReadOnly),
other => (format!("common.{other}"), ModelAccess::Unmapped),
};
properties.push(PropertyCoverage {
name, source_path: format!("common.{}", field.name), type_id: field.type_id.as_str().to_owned(),
optional: field.optional, is_sequence: field.is_sequence, snapshot_readable: true,
name,
source_path: format!("common.{}", field.name),
type_id: field.type_id.as_str().to_owned(),
optional: field.optional,
is_sequence: field.is_sequence,
snapshot_readable: true,
validation: if access == ModelAccess::ReadWrite && field.name == "layer" {
"transaction_nonempty"
} else if access == ModelAccess::ReadWrite { "type_conversion_only" } else { "none" }.into(),
} else if access == ModelAccess::ReadWrite {
"type_conversion_only"
} else {
"none"
}
.into(),
model_access: access,
});
}
let shape_type = variant.fields.first().expect("entity variant payload").type_id.clone();
let shape_type = variant
.fields
.first()
.expect("entity variant payload")
.type_id
.clone();
let shape = &registry.types[&shape_type];
for field in &shape.fields {
if field.name == "common" { continue; }
if field.name == "common" {
continue;
}
let exposed = editable.iter().chain(&readable).find(|name| {
aliases.and_then(|map| map.get(*name)).map_or(name.as_str(), String::as_str) == field.name
aliases
.and_then(|map| map.get(*name))
.map_or(name.as_str(), String::as_str)
== field.name
});
let (name, access) = match exposed {
Some(name) => {
unresolved.remove(name);
((*name).clone(), if editable.contains(name) { ModelAccess::ReadWrite } else { ModelAccess::ReadOnly })
(
(*name).clone(),
if editable.contains(name) {
ModelAccess::ReadWrite
} else {
ModelAccess::ReadOnly
},
)
}
None => (field.name.clone(), ModelAccess::Unmapped),
};
let validation = if access != ModelAccess::ReadWrite { "none" }
else if matches!((kind.as_str(), name.as_str()),
("Point", "location") | ("Line", "start" | "end") |
("Circle" | "Arc", "center" | "radius") |
("Ray" | "XLine", "base_point" | "direction") |
("Solid", "first_corner" | "second_corner" | "third_corner" | "fourth_corner" | "normal" | "thickness") |
("Face3D", "first_corner" | "second_corner" | "third_corner" | "fourth_corner" | "invisible_edges") |
("Insert", "insert_point" | "x_scale" | "y_scale" | "z_scale" | "rotation" | "normal" |
"column_count" | "row_count" | "column_spacing" | "row_spacing") |
("Tolerance", "insertion_point" | "direction" | "normal" | "text" |
"dimension_style_name" | "text_height" | "dimension_gap") |
("Shape", "insertion_point" | "size" | "shape_name" | "shape_number" |
"rotation" | "relative_x_scale" | "oblique_angle" | "normal" |
"thickness" | "style_name") |
("AttributeDefinition", "tag" | "prompt" | "default_value" |
"insertion_point" | "alignment_point" | "height" | "rotation" |
"width_factor" | "oblique_angle" | "text_style" |
"text_generation_flags" | "horizontal_alignment" |
"vertical_alignment" | "flags" | "field_length" | "normal" |
"mtext_flag" | "is_multiline" | "line_count" | "lock_position")) {
let validation = if access != ModelAccess::ReadWrite {
"none"
} else if matches!(
(kind.as_str(), name.as_str()),
("Point", "location")
| ("Line", "start" | "end")
| ("Circle" | "Arc", "center" | "radius")
| ("Ray" | "XLine", "base_point" | "direction")
| (
"Solid",
"first_corner"
| "second_corner"
| "third_corner"
| "fourth_corner"
| "normal"
| "thickness"
)
| (
"Face3D",
"first_corner"
| "second_corner"
| "third_corner"
| "fourth_corner"
| "invisible_edges"
)
| (
"Insert",
"insert_point"
| "x_scale"
| "y_scale"
| "z_scale"
| "rotation"
| "normal"
| "column_count"
| "row_count"
| "column_spacing"
| "row_spacing"
)
| (
"Tolerance",
"insertion_point"
| "direction"
| "normal"
| "text"
| "dimension_style_name"
| "text_height"
| "dimension_gap"
)
| (
"Shape",
"insertion_point"
| "size"
| "shape_name"
| "shape_number"
| "rotation"
| "relative_x_scale"
| "oblique_angle"
| "normal"
| "thickness"
| "style_name"
)
| (
"AttributeDefinition",
"tag"
| "prompt"
| "default_value"
| "insertion_point"
| "alignment_point"
| "height"
| "rotation"
| "width_factor"
| "oblique_angle"
| "text_style"
| "text_generation_flags"
| "horizontal_alignment"
| "vertical_alignment"
| "flags"
| "field_length"
| "normal"
| "mtext_flag"
| "is_multiline"
| "line_count"
| "lock_position"
)
| (
"AttributeEntity",
"tag"
| "value"
| "insertion_point"
| "alignment_point"
| "height"
| "rotation"
| "width_factor"
| "oblique_angle"
| "text_style"
| "text_generation_flags"
| "horizontal_alignment"
| "vertical_alignment"
| "flags"
| "field_length"
| "normal"
| "mtext_flag"
| "is_multiline"
| "line_count"
| "lock_position"
)
) {
"transaction_geometry"
} else { "type_conversion_only" };
} else {
"type_conversion_only"
};
properties.push(PropertyCoverage {
name, source_path: field.name.clone(),
type_id: if exposed.is_some_and(|name| name == "closed") { "bool".into() }
else { field.type_id.as_str().to_owned() },
optional: field.optional, is_sequence: field.is_sequence, snapshot_readable: true,
name,
source_path: field.name.clone(),
type_id: if exposed.is_some_and(|name| name == "closed") {
"bool".into()
} else {
field.type_id.as_str().to_owned()
},
optional: field.optional,
is_sequence: field.is_sequence,
snapshot_readable: true,
validation: validation.into(),
model_access: access,
});
}
assert!(unresolved.is_empty(), "{kind} has coverage keys absent from registry: {unresolved:?}");
entity_kinds.push(EntityKindCoverage { kind: kind.clone(), scope, properties });
assert!(
unresolved.is_empty(),
"{kind} has coverage keys absent from registry: {unresolved:?}"
);
entity_kinds.push(EntityKindCoverage {
kind: kind.clone(),
scope,
properties,
});
}
let catalog = EntityCoverageCatalog { schema_version: 1, entity_kinds };
fs::write(out_dir.join("entity_coverage.json"), serde_json::to_string_pretty(&catalog).unwrap())
let catalog = EntityCoverageCatalog {
schema_version: 1,
entity_kinds,
};
fs::write(
out_dir.join("entity_coverage.json"),
serde_json::to_string_pretty(&catalog).unwrap(),
)
.expect("write entity coverage");
}