Add ocs_doc_api: object-oriented CAD entity API (v2)

New crate crates/ocs_doc_api: object-centric facade (DocApi -> Document ->
collections -> typed handles), transport-agnostic wire layer (DocApiEnvelope =
single op | read-only query batch), per-op autocommit (no write transactions),
bulk ops with upfront-validation all-or-nothing, structured ApiError, and
build-time codegen (spec/entities.toml -> ops/query enums, api_reference.md,
binding_schema.json).

ocs_plugin_api: append-only PluginRequest::DocApiRequest / PluginResponse::
DocApiResponse (serde_bytes payload) + defaulted HostApi::doc_api_dispatch.

Host: DocApiBackend impl for HostSession (src/app/doc_api.rs) mapping to scene
primitives (solid_models lift-on-miss, register_prepared_solid_model, meshes
metrics), entity conversions (doc_api_convert.rs), HostSession accessors +
doc_api_dispatch override.

Tests: 592 host lib (incl. 7 DocApi integration tests: create/boolean/query,
unknown-id, transform-many all-or-nothing, query-batch cap, 2D/geometric/DWG
roundtrips), 8 crate unit tests. Examples + README/ARCHITECTURE/IMPLEMENTATION docs.
This commit is contained in:
Sebastian committed 2026-09-06 00:15:27 +02:00
1 parent 9129c85f97
commit e41086b058
44 files changed
+4837 -12

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
//! The wire envelope (plan §7). One request = ONE write op OR a query batch
//! (read-only batching is safe: no mutation, no undo, no rollback).
use serde::{Deserialize, Serialize};
use crate::gen::{Operation, Query};
use crate::id::ObjectId;
use crate::query::QueryResult;
use crate::revision::GeometryRevision;
use crate::ENVELOPE_VERSION;
/// The single request unit crossing the transport.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DocApiEnvelope {
/// Crate protocol version (`ENVELOPE_VERSION`). Bridges must check this.
pub version: u16,
pub body: EnvelopeBody,
}
impl DocApiEnvelope {
pub fn op(operation: Operation) -> Self {
Self { version: ENVELOPE_VERSION, body: EnvelopeBody::Op(operation) }
}
pub fn queries(queries: Vec<Query>) -> Self {
Self { version: ENVELOPE_VERSION, body: EnvelopeBody::Queries(queries) }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EnvelopeBody {
/// Exactly ONE write op (write batching removed, plan §13). Atomic by itself.
Op(Operation),
/// One-or-more read-only queries answered together (safe: no mutation/undo).
Queries(Vec<Query>),
}
/// The outcome of an applied write op.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum OpOutcome {
/// A new entity was created; carries its fresh `ObjectId`.
NewId(ObjectId),
/// An existing entity was updated in place (same `ObjectId` preserved).
Updated(ObjectId),
/// Entities were deleted; carries the deleted ids (one for `Delete`, N for `DeleteMany`).
Deleted(Vec<ObjectId>),
/// Many entities were created (bulk `CreateMany`); carries all fresh ids in order.
NewIds(Vec<ObjectId>),
}
impl OpOutcome {
/// The single fresh id, for ops that create exactly one entity.
pub fn new_id(&self) -> Option<ObjectId> {
match self {
OpOutcome::NewId(id) => Some(*id),
_ => None,
}
}
/// All fresh ids, for bulk creates.
pub fn new_ids(&self) -> &[ObjectId] {
match self {
OpOutcome::NewIds(ids) => ids,
OpOutcome::NewId(id) => std::slice::from_ref(id),
_ => &[],
}
}
}
/// The per-request result (and the query-batch answer carrier).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Receipt {
/// Present for `EnvelopeBody::Op`; absent for pure queries.
pub outcome: Option<OpOutcome>,
/// One `QueryResult` per `Query` in a `Queries` batch (in order).
pub query_results: Vec<QueryResult>,
/// The geometry revision after this request (== before, for pure queries).
pub new_revision: GeometryRevision,
}