forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: Phase 3 attributes + nested block traversal
- SetAttribute op + GetAttributes/GetBlockEntities queries (append-only). - backend set_attribute (add/update Insert attribute), attributes (tag,value pairs), block_entities (read-only traversal via BlockRecord.entity_handles -> id+kind+bounds). - facade Entity::set_attribute/attributes, Document::block_entities. - spec Insert methods; api_reference + binding_schema regenerated; mocks + baseline. - Query enum: drop Copy (GetBlockEntities carries String). - Test phase3_attributes_and_block_traversal -> 608 host lib + 3 consistency green.
This commit is contained in:
1 parent
2dba0158ca
commit
67655add72
15 files changed
+267
-14
No files matched your search
@@ -79,8 +79,13 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal
|
||||
- **Done:** `create_insert(block_name, point, scale, rotation)` → `Operation::CreateInsert`
|
||||
(validates the BlockRecord exists; unknown block → `Validation`, no mutation).
|
||||
Insert `transform` moves its `insert_point`. Covered by `phase3_*` test.
|
||||
- **Remaining:** `AttributeDefinition`/`AttributeEntity` typed create + attribute
|
||||
get/set; read-only nested block-content traversal.
|
||||
- **Done (attributes + traversal):** `set_attribute(tag, value)` (adds/updates an
|
||||
attribute on an insert, `SetAttribute` op), `attributes()` → (tag, value) pairs
|
||||
(`GetAttributes`), and `block_entities(block_name)` → read-only nested traversal
|
||||
of a block definition's entities (`GetBlockEntities`). Covered by
|
||||
`phase3_attributes_and_block_traversal` test.
|
||||
- **Remaining:** typed `AttributeDefinition` (in-block attribute-definition)
|
||||
create.
|
||||
|
||||
### Phase 4 — paper-space & viewports
|
||||
- **Done:** `create_viewport(center, width, height, view_target, view_height)` →
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct MockBackend {
|
||||
hatch_boundaries: HashMap<ObjectId, Vec<Vec<[f64; 2]>>>,
|
||||
/// Stored dimension measurements.
|
||||
dimension_measurements: HashMap<ObjectId, f64>,
|
||||
/// Stored insert attributes.
|
||||
attributes_store: HashMap<ObjectId, Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl MockBackend {
|
||||
@@ -180,6 +182,25 @@ impl DocApiBackend for MockBackend {
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
||||
self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
let attrs = self.attributes_store.entry(id).or_default();
|
||||
if let Some(a) = attrs.iter_mut().find(|(t, _)| t == tag) {
|
||||
a.1 = value.to_string();
|
||||
} else {
|
||||
attrs.push((tag.to_string(), value.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn attributes(&self, id: ObjectId) -> ApiResult<Vec<(String, String)>> {
|
||||
self.attributes_store.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn block_entities(&self, _block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
Ok(())
|
||||
|
||||
@@ -347,6 +347,19 @@ name = "create_insert"
|
||||
args = [{ name = "block_name", ty = "&str" }, { name = "insert_point", ty = "[f64; 3]" }, { name = "scale", ty = "f64" }, { name = "rotation", ty = "f64" }]
|
||||
op = "CreateInsert"
|
||||
|
||||
[[family.method]]
|
||||
name = "attributes"
|
||||
kind = "query"
|
||||
query = "GetAttributes"
|
||||
returns = "Vec<(String, String)>"
|
||||
|
||||
[[family.method]]
|
||||
name = "set_attribute"
|
||||
kind = "op"
|
||||
op = "SetAttribute"
|
||||
args = [{ name = "tag", ty = "&str" }, { name = "value", ty = "&str" }]
|
||||
returns = "()"
|
||||
|
||||
# ============================================================================
|
||||
# PAPER-SPACE FAMILY (phase 4) — viewports
|
||||
# ============================================================================
|
||||
|
||||
@@ -66,6 +66,15 @@ pub trait DocApiBackend {
|
||||
/// A dimension's measured value (distance for linear/radius, degrees for angular).
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64>;
|
||||
|
||||
/// Set an attribute `value` for `tag` on an insert (adds if absent). Insert-only.
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()>;
|
||||
|
||||
/// An insert's attributes as (tag, value) pairs. Insert-only.
|
||||
fn attributes(&self, id: ObjectId) -> ApiResult<Vec<(String, String)>>;
|
||||
|
||||
/// The entities inside a block definition (read-only traversal), as views.
|
||||
fn block_entities(&self, block_name: &str) -> ApiResult<Vec<EntityView>>;
|
||||
|
||||
/// Can `id` be modified in place right now (exists, is the expected family,
|
||||
/// not on a locked layer)? Read-only pre-check used before mutations.
|
||||
/// Default: existence + not-locked (backends narrow the family check).
|
||||
|
||||
@@ -227,6 +227,16 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::SetAttribute { id, tag, value } => {
|
||||
b.can_modify(*id).map_err(|e| match e {
|
||||
ApiError::UnknownId(_) => ApiError::validation(name, format!("unknown ObjectId {id:?}")),
|
||||
other => other,
|
||||
})?;
|
||||
b.push_undo(name);
|
||||
b.set_attribute(*id, tag, value)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
};
|
||||
Ok(Receipt {
|
||||
outcome: Some(outcome),
|
||||
@@ -253,20 +263,22 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
|
||||
}
|
||||
let mut results = Vec::with_capacity(queries.len());
|
||||
for q in &queries {
|
||||
let r = match *q {
|
||||
Query::GetEntity { id } => QueryResult::Entity(b.get_entity(id)?),
|
||||
Query::GetBounds { id } => QueryResult::Bounds(b.bounds(id)?),
|
||||
Query::GetCentroid { id } => QueryResult::Centroid(b.centroid(id)?),
|
||||
Query::GetVolume { id } => QueryResult::Volume(b.volume(id)?),
|
||||
let r = match q {
|
||||
Query::GetEntity { id } => QueryResult::Entity(b.get_entity(*id)?),
|
||||
Query::GetBounds { id } => QueryResult::Bounds(b.bounds(*id)?),
|
||||
Query::GetCentroid { id } => QueryResult::Centroid(b.centroid(*id)?),
|
||||
Query::GetVolume { id } => QueryResult::Volume(b.volume(*id)?),
|
||||
Query::GetIntersects { a, b: bid } => {
|
||||
let ba = b.bounds(a)?;
|
||||
let bb = b.bounds(bid)?;
|
||||
let ba = b.bounds(*a)?;
|
||||
let bb = b.bounds(*bid)?;
|
||||
QueryResult::Intersects(ba.overlaps(&bb))
|
||||
}
|
||||
Query::GetGeometryRevision => QueryResult::Revision(b.revision()),
|
||||
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(id)?),
|
||||
Query::GetHatchBoundary { id } => QueryResult::HatchBoundary(b.hatch_boundary(id)?),
|
||||
Query::GetDimensionMeasurement { id } => QueryResult::DimensionMeasurement(b.dimension_measurement(id)?),
|
||||
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(*id)?),
|
||||
Query::GetHatchBoundary { id } => QueryResult::HatchBoundary(b.hatch_boundary(*id)?),
|
||||
Query::GetDimensionMeasurement { id } => QueryResult::DimensionMeasurement(b.dimension_measurement(*id)?),
|
||||
Query::GetAttributes { id } => QueryResult::Attributes(b.attributes(*id)?),
|
||||
Query::GetBlockEntities { block_name } => QueryResult::BlockEntities(b.block_entities(block_name)?),
|
||||
};
|
||||
results.push(r);
|
||||
}
|
||||
|
||||
@@ -128,6 +128,14 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only traversal of a block definition's entities (ids + kinds + bounds).
|
||||
pub fn block_entities(&self, block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
match self.session.one_query(Query::GetBlockEntities { block_name: block_name.to_string() })? {
|
||||
QueryResult::BlockEntities(v) => Ok(v),
|
||||
_ => Err(ApiError::Transport("unexpected block-entities result".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch of read-only queries in ONE round-trip (safe: no mutation/undo).
|
||||
/// The closure records queries on a [`QueryBatch`]; the results are returned
|
||||
/// in the same order as a [`QueryResults`] view the caller destructures.
|
||||
@@ -289,6 +297,18 @@ impl Entity {
|
||||
_ => Err(ApiError::Transport("unexpected entity result".into())),
|
||||
}
|
||||
}
|
||||
/// This insert's attributes as (tag, value) pairs. Insert-only.
|
||||
pub fn attributes(&self) -> ApiResult<Vec<(String, String)>> {
|
||||
match self.session.one_query(Query::GetAttributes { id: self.id })? {
|
||||
QueryResult::Attributes(v) => Ok(v),
|
||||
_ => Err(ApiError::Transport("unexpected attributes result".into())),
|
||||
}
|
||||
}
|
||||
/// Set an attribute `value` for `tag` on this insert (adds if absent). One undo step.
|
||||
pub fn set_attribute(&self, tag: &str, value: &str) -> ApiResult<()> {
|
||||
self.session.apply_op(Operation::SetAttribute { id: self.id, tag: tag.to_string(), value: value.to_string() })?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn as_solid(&self) -> Option<Solid> {
|
||||
// Typed downcast is validated by the view's kind.
|
||||
matches!(self.view().ok()?.kind.as_str(), "Solid3D").then(|| Solid::new(self.session.clone(), self.id))
|
||||
|
||||
@@ -145,6 +145,11 @@ Block reference. Place an existing BlockRecord by name; transform moves its inse
|
||||
|
||||
- **create_insert**(`block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64`) -> `Entity`
|
||||
|
||||
### Methods
|
||||
|
||||
- **attributes**(``) -> `Vec<(String, String)>` — query `GetAttributes`
|
||||
- **set_attribute**(`tag: &str, value: &str`) -> `()` — op `SetAttribute`
|
||||
|
||||
## `Entity` (acadrust `Viewport`, collection `entities`)
|
||||
|
||||
Paper-space viewport: a width×height window at center looking at view_target with view_height zoom. bounds = center ± w/2,h/2; transform moves center.
|
||||
|
||||
@@ -300,7 +300,39 @@
|
||||
"acadrust_variant": "Insert",
|
||||
"collection": "entities",
|
||||
"handle": "Entity",
|
||||
"methods": [],
|
||||
"methods": [
|
||||
{
|
||||
"args": [],
|
||||
"bulk": false,
|
||||
"fixed": null,
|
||||
"kind": "query",
|
||||
"name": "attributes",
|
||||
"op": null,
|
||||
"query": "GetAttributes",
|
||||
"read_only": true,
|
||||
"returns": "Vec<(String, String)>"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"name": "tag",
|
||||
"ty": "&str"
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"ty": "&str"
|
||||
}
|
||||
],
|
||||
"bulk": false,
|
||||
"fixed": null,
|
||||
"kind": "op",
|
||||
"name": "set_attribute",
|
||||
"op": "SetAttribute",
|
||||
"query": null,
|
||||
"read_only": false,
|
||||
"returns": "()"
|
||||
}
|
||||
],
|
||||
"name": "Insert"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -40,4 +40,6 @@ pub enum Operation {
|
||||
CreateHatch(HatchSpec),
|
||||
// dimensions
|
||||
CreateDimensionLinear(DimensionSpec),
|
||||
// attributes
|
||||
SetAttribute { id: ObjectId, tag: String, value: String },
|
||||
}
|
||||
@@ -9,7 +9,8 @@ use crate::id::ObjectId;
|
||||
|
||||
/// A typed read query (plan §5). Read-only: no mutation, no undo, no revision
|
||||
/// bump. Safe to batch (`EnvelopeBody::Queries`). Append new variants at the END only.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
/// (Not `Copy`: some variants carry owned data like a block name.)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub enum Query {
|
||||
GetEntity { id: ObjectId },
|
||||
@@ -25,4 +26,8 @@ pub enum Query {
|
||||
GetHatchBoundary { id: ObjectId },
|
||||
/// The measured value of a Dimension (distance or angle).
|
||||
GetDimensionMeasurement { id: ObjectId },
|
||||
/// An insert's attributes as (tag, value) pairs.
|
||||
GetAttributes { id: ObjectId },
|
||||
/// The entity ids + kinds inside a block definition (read-only traversal).
|
||||
GetBlockEntities { block_name: String },
|
||||
}
|
||||
@@ -169,6 +169,7 @@ impl crate::gen::Operation {
|
||||
SetTextContent { .. } => "SetTextContent",
|
||||
CreateHatch(_) => "CreateHatch",
|
||||
CreateDimensionLinear(_) => "CreateDimensionLinear",
|
||||
SetAttribute { .. } => "SetAttribute",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,10 @@ pub enum QueryResult {
|
||||
HatchBoundary(Vec<Vec<[f64; 2]>>),
|
||||
/// A dimension's measured value (distance for linear/radius, degrees for angular).
|
||||
DimensionMeasurement(f64),
|
||||
/// An insert's attributes as (tag, value) pairs.
|
||||
Attributes(Vec<(String, String)>),
|
||||
/// The entities inside a block definition (read-only traversal).
|
||||
BlockEntities(Vec<EntityView>),
|
||||
}
|
||||
|
||||
/// Convenience: the query name for diagnostics.
|
||||
@@ -65,6 +69,8 @@ impl crate::gen::Query {
|
||||
GetTextContent { .. } => "GetTextContent",
|
||||
GetHatchBoundary { .. } => "GetHatchBoundary",
|
||||
GetDimensionMeasurement { .. } => "GetDimensionMeasurement",
|
||||
GetAttributes { .. } => "GetAttributes",
|
||||
GetBlockEntities { .. } => "GetBlockEntities",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ struct MockBackend {
|
||||
hatch_boundaries: HashMap<ObjectId, Vec<Vec<[f64; 2]>>>,
|
||||
/// Stored dimension measurements.
|
||||
dimension_measurements: HashMap<ObjectId, f64>,
|
||||
/// Stored insert attributes.
|
||||
attributes_store: HashMap<ObjectId, Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl MockBackend {
|
||||
@@ -120,6 +122,25 @@ impl DocApiBackend for MockBackend {
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
||||
self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
let attrs = self.attributes_store.entry(id).or_default();
|
||||
if let Some(a) = attrs.iter_mut().find(|(t, _)| t == tag) {
|
||||
a.1 = value.to_string();
|
||||
} else {
|
||||
attrs.push((tag.to_string(), value.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn attributes(&self, id: ObjectId) -> ApiResult<Vec<(String, String)>> {
|
||||
self.attributes_store.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn block_entities(&self, _block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
Ok(())
|
||||
|
||||
@@ -58,6 +58,7 @@ fn operation_variants() -> Vec<String> {
|
||||
Operation::SetTextContent { id: ObjectId::from_u64(0), value: String::new() },
|
||||
Operation::CreateHatch(ocs_doc_api::ops::HatchSpec { boundary: vec![], solid: true }),
|
||||
Operation::CreateDimensionLinear(ocs_doc_api::ops::DimensionSpec { first_point: [0.0; 3], second_point: [0.0; 3], definition_point: [0.0; 3] }),
|
||||
Operation::SetAttribute { id: ObjectId::from_u64(0), tag: String::new(), value: String::new() },
|
||||
];
|
||||
// Derive the variant names by serializing a probe of each and reading the
|
||||
// bincode variant index -> name map is not available; instead match by
|
||||
@@ -85,6 +86,8 @@ fn query_variants() -> Vec<String> {
|
||||
Query::GetTextContent { id: ObjectId::from_u64(0) },
|
||||
Query::GetHatchBoundary { id: ObjectId::from_u64(0) },
|
||||
Query::GetDimensionMeasurement { id: ObjectId::from_u64(0) },
|
||||
Query::GetAttributes { id: ObjectId::from_u64(0) },
|
||||
Query::GetBlockEntities { block_name: String::new() },
|
||||
];
|
||||
marker
|
||||
.iter()
|
||||
@@ -140,6 +143,7 @@ const OPERATION_BASELINE: &[&str] = &[
|
||||
"SetTextContent",
|
||||
"CreateHatch",
|
||||
"CreateDimensionLinear",
|
||||
"SetAttribute",
|
||||
];
|
||||
|
||||
/// Recorded baseline of the `Query` variant order.
|
||||
@@ -153,6 +157,8 @@ const QUERY_BASELINE: &[&str] = &[
|
||||
"GetTextContent",
|
||||
"GetHatchBoundary",
|
||||
"GetDimensionMeasurement",
|
||||
"GetAttributes",
|
||||
"GetBlockEntities",
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -262,6 +262,55 @@ impl DocApiBackend for HostSession<'_> {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||
let handle = obj_to_handle(id);
|
||||
let entity = self.document().get_entity(handle).cloned().ok_or(ApiError::UnknownId(id))?;
|
||||
let EntityType::Insert(mut ins) = entity else {
|
||||
return Err(ApiError::Unsupported("SetAttribute is only for Insert entities".into()));
|
||||
};
|
||||
if let Some(attr) = ins.attributes.iter_mut().find(|a| a.tag == tag) {
|
||||
attr.value = value.to_string();
|
||||
} else {
|
||||
let mut attr = acadrust::entities::AttributeEntity::default();
|
||||
attr.tag = tag.to_string();
|
||||
attr.value = value.to_string();
|
||||
attr.insertion_point = ins.insert_point;
|
||||
ins.attributes.push(attr);
|
||||
}
|
||||
if !self.scene_mut().update_entity(EntityType::Insert(ins)) {
|
||||
return Err(ApiError::Unsupported(format!("entity {id:?} is on a locked layer")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn attributes(&self, id: ObjectId) -> ApiResult<Vec<(String, String)>> {
|
||||
let entity = self.document().get_entity(obj_to_handle(id)).ok_or(ApiError::UnknownId(id))?;
|
||||
let EntityType::Insert(ins) = entity else {
|
||||
return Err(ApiError::Unsupported("GetAttributes is only for Insert entities".into()));
|
||||
};
|
||||
Ok(ins.attributes.iter().map(|a| (a.tag.clone(), a.value.clone())).collect())
|
||||
}
|
||||
|
||||
fn block_entities(&self, block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
let br = self
|
||||
.document()
|
||||
.block_records
|
||||
.get(block_name)
|
||||
.ok_or_else(|| ApiError::validation("GetBlockEntities", format!("unknown block {block_name:?}")))?;
|
||||
// Read-only traversal of the block definition's entities (handle lookups).
|
||||
let mut out = Vec::with_capacity(br.entity_handles.len());
|
||||
for h in &br.entity_handles {
|
||||
let id = handle_to_obj(*h);
|
||||
let (kind, bounds) = match self.document().get_entity(*h) {
|
||||
Some(e) => (crate::app::doc_api_convert::entity_kind_name(e).to_string(),
|
||||
crate::app::doc_api_convert::entity_bounds(Some(e), id).ok()),
|
||||
None => ("Missing".to_string(), None),
|
||||
};
|
||||
out.push(EntityView { id, kind, bounds });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn add_vertex(&mut self, id: ObjectId, at: usize, point: [f64; 3]) -> ApiResult<()> {
|
||||
let handle = obj_to_handle(id);
|
||||
let Some(entity) = self.document().get_entity(handle).cloned() else {
|
||||
@@ -929,6 +978,52 @@ mod tests {
|
||||
assert!((t.insertion_point.x - 15.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
// ── Phase 3 remaining: attributes + nested block traversal ──────────────
|
||||
|
||||
#[test]
|
||||
fn phase3_attributes_and_block_traversal() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
// A block "Door" containing a line; insert it, then set/read attributes.
|
||||
host.document_mut().block_records.add(acadrust::tables::BlockRecord::new("Door")).unwrap();
|
||||
let line = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateCurve(
|
||||
Curve2Spec::Line { start: [0.0; 3], end: [10.0; 3] }))).unwrap());
|
||||
host.document_mut().block_records.get_mut("Door").unwrap().entity_handles.push(obj_to_handle(line));
|
||||
|
||||
let ins = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateInsert(
|
||||
ocs_doc_api::ops::InsertSpec { block_name: "Door".into(), insert_point: [0.0; 3], scale: 1.0, rotation: 0.0 }))).unwrap());
|
||||
|
||||
// set_attribute adds a new attribute; get_attributes reads it back.
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::SetAttribute { id: ins, tag: "HANDLE".into(), value: "A-1".into() })).unwrap();
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetAttributes { id: ins }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Attributes(attrs) => {
|
||||
assert_eq!(attrs.len(), 1);
|
||||
assert_eq!(attrs[0], ("HANDLE".to_string(), "A-1".to_string()));
|
||||
}
|
||||
other => panic!("expected attributes, got {other:?}"),
|
||||
}
|
||||
// set_attribute on an existing tag updates it.
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::SetAttribute { id: ins, tag: "HANDLE".into(), value: "A-2".into() })).unwrap();
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetAttributes { id: ins }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Attributes(attrs) => assert_eq!(attrs[0].1, "A-2"),
|
||||
other => panic!("expected attributes, got {other:?}"),
|
||||
}
|
||||
|
||||
// Nested traversal: GetBlockEntities("Door") returns the line.
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetBlockEntities { block_name: "Door".into() }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::BlockEntities(v) => {
|
||||
assert_eq!(v.len(), 1);
|
||||
assert_eq!(v[0].kind, "Line");
|
||||
}
|
||||
other => panic!("expected block entities, got {other:?}"),
|
||||
}
|
||||
// Unknown block -> validation error.
|
||||
assert!(dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetBlockEntities { block_name: "Nope".into() }])).is_err());
|
||||
}
|
||||
|
||||
// ── Phase 2b-c: dimension ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user