General improvements on API

This commit is contained in:
Sebastian committed 2026-09-09 08:02:35 +02:00
1 parent 56e09cd3f5
commit 55f61a4327
30 files changed
+1820 -2565

No files matched your search

+62 -48
View File
@@ -1,4 +1,15 @@
//! Regenerate the API reference and binding schema from the curated spec.
//! Codegen driver (plan §10): reads `spec/entities.toml` and regenerates the
//! derived snapshots in `src/gen/` (`api_reference.md`, `binding_schema.json`).
//! CI runs `cargo build` and fails on drift for those two (the committed files
//! are the reviewable truth; build.rs rewrites them deterministically).
//!
//! NOTE: the `Operation`/`Query` wire enums (`ops_gen.rs`/`query_gen.rs`) are
//! HAND-MAINTAINED, append-only, and NOT derived here — the spec's `op`/`query`
//! names must map to enum variants (asserted by a test), never the reverse.
//!
//! The wire-layout tracing for `binding_schema.json` uses serde-reflection on the
//! DTO payload types (same mechanism as `ocs_plugin_api`'s `type_registry.json`),
//! so the traced layouts are mechanically correct, never hand-written.
use std::env;
use std::fs;
@@ -93,40 +104,48 @@ fn main() {
let spec_path = manifest_dir.join("spec/entities.toml");
println!("cargo:rerun-if-changed={}", spec_path.display());
println!("cargo:rerun-if-changed=src/gen/layouts.json");
let spec_src = fs::read_to_string(&spec_path).expect("read spec/entities.toml");
let spec: Spec = toml::from_str(&spec_src).expect("parse spec/entities.toml");
let gen_dir = manifest_dir.join("src/gen");
fs::create_dir_all(&gen_dir).unwrap();
// 1. Emit the HAND-MAINTAINED ops/query enum snapshots verbatim. The enums are
// NOT derived from the spec here — they are the canonical append-only wire
// vocabulary; the spec's `op`/`query` names must map to variants (asserted
// by a test). `write_if_changed` is effectively a no-op round-trip.
write_if_changed(&gen_dir.join("ops_gen.rs"), &ops_gen_src());
write_if_changed(&gen_dir.join("query_gen.rs"), &query_gen_src());
// 2. Emit the auto-generated API reference (§10.1) from the spec.
write_if_changed(&gen_dir.join("api_reference.md"), &api_reference_md(&spec));
write_if_changed(
&gen_dir.join("binding_schema.json"),
&binding_schema_json(&spec),
);
// 3. Emit the merged binding handover schema (§10.2) with traced wire layouts.
write_if_changed(&gen_dir.join("binding_schema.json"), &binding_schema_json(&spec));
}
/// Rewrite a generated file only when content changed (keeps mtime/CI stable).
fn write_if_changed(path: &PathBuf, content: &str) {
let unchanged = fs::read_to_string(path)
.map(|old| old == content)
.unwrap_or(false);
let unchanged = fs::read_to_string(path).map(|old| old == content).unwrap_or(false);
if !unchanged {
fs::write(path, content).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
}
}
fn ops_gen_src() -> String {
// Keep in sync with the committed snapshot; the canonical list lives there.
include_str!("src/gen/ops_gen.rs").to_string()
}
fn query_gen_src() -> String {
include_str!("src/gen/query_gen.rs").to_string()
}
/// Render a method/constructor's `fixed` fields compactly (e.g.
/// `, op="Intersection", erase_sources=true`). Empty when no fixed fields.
fn fmt_fixed(fixed: Option<&toml::Value>) -> String {
let Some(fixed) = fixed else {
return String::new();
};
let Some(tbl) = fixed.as_table() else {
return String::new();
};
let Some(fixed) = fixed else { return String::new() };
let Some(tbl) = fixed.as_table() else { return String::new() };
if tbl.is_empty() {
return String::new();
}
@@ -139,10 +158,7 @@ fn fmt_args(args: &[Arg]) -> String {
if args.is_empty() {
return String::new();
}
args.iter()
.map(|a| format!("{}: {}", a.name, a.ty))
.collect::<Vec<_>>()
.join(", ")
args.iter().map(|a| format!("{}: {}", a.name, a.ty)).collect::<Vec<_>>().join(", ")
}
/// The parenthesised argument list: `(a: T, b: U)` or `()` for zero args.
@@ -218,10 +234,7 @@ fn status_suffix(status: Option<&str>) -> &'static str {
fn api_reference_md(spec: &Spec) -> String {
let mut s = String::new();
s.push_str("# ocs_doc_api — API reference (auto-generated)\n\n");
s.push_str(&format!(
"Envelope version: `{}`\n\n",
spec.meta.envelope_version
));
s.push_str(&format!("Envelope version: `{}`\n\n", spec.meta.envelope_version));
s.push_str("Do NOT edit: regenerated by `build.rs` from `spec/entities.toml`.\n\n");
for fam in &spec.family {
@@ -289,14 +302,13 @@ fn api_reference_md(spec: &Spec) -> String {
/// The wire vocabulary section: every `Operation`/`Query` variant with its payload,
/// read from `src/gen/layouts.json` (the curated wire-layout snapshot).
fn wire_vocabulary_md() -> String {
let layouts = read_layouts();
let layouts = match fs::read_to_string(layouts_snapshot_path()) {
Ok(src) => serde_json::from_str::<serde_json::Value>(&src).unwrap_or_else(|_| serde_json::json!({})),
Err(_) => serde_json::json!({}),
};
let mut s = String::from("## Wire vocabulary\n\nEvery `Operation` is ONE atomic write op (one undo step); every `Query` is read-only (safe to batch). Variants are append-only (bincode discriminant stability). Requests ride `DocApiEnvelope`: `Op(Operation)` for a write, `Queries(Vec<Query>)` for a read batch.\n\n");
s.push_str("### `Operation`\n\n");
if let Some(variants) = layouts
.get("Operation")
.and_then(|o| o.get("enum"))
.and_then(|v| v.as_array())
{
if let Some(variants) = layouts.get("Operation").and_then(|o| o.get("enum")).and_then(|v| v.as_array()) {
for v in variants {
if let Some(name) = v.as_str() {
s.push_str(&format!("- `{name}`\n"));
@@ -304,11 +316,7 @@ fn wire_vocabulary_md() -> String {
}
}
s.push_str("\n### `Query`\n\n");
if let Some(variants) = layouts
.get("Query")
.and_then(|o| o.get("enum"))
.and_then(|v| v.as_array())
{
if let Some(variants) = layouts.get("Query").and_then(|o| o.get("enum")).and_then(|v| v.as_array()) {
for v in variants {
if let Some(name) = v.as_str() {
s.push_str(&format!("- `{name}`\n"));
@@ -324,10 +332,10 @@ fn error_and_envelope_md() -> String {
| Variant | Meaning |
|---|---|
| `Validation { op, reason }` | Input rejected before any document mutation. |
| `Validation { op, reason }` | Input rejected **before** any mutation (bulk ops name the failing index). |
| `Geometry { kind, msg }` | The kernel failed (boolean refused, invalid input, ACIS lift/lower). |
| `UnknownId(ObjectId)` | The handle is stale / deleted / never existed (never a panic). |
| `Unsupported(String)` | Capability not supported for this entity or backend. |
| `Unsupported(String)` | Capability not implemented (e.g. a later-phase family or sub-type). |
| `Transport(String)` | The channel failed (disconnected / oversized / timeout). |
`ApiError` is serializable, so IPC returns the same structured error the in-process
@@ -346,7 +354,7 @@ Send + Sync` trait (`apply(envelope) -> Receipt`, `alive()`). `Receipt` carries
}
/// The merged, self-contained binding handover schema (§10.2). Object model +
/// method signatures + op/query mapping + curated wire vocabulary inlined per node.
/// method signatures + op/query mapping + traced wire layouts inlined per node.
/// The handle types for the binding schema, derived from the spec's families
/// ("Entity" always first, then the deduped family handles in spec order).
fn handles_for_schema(spec: &Spec) -> Vec<String> {
@@ -361,7 +369,9 @@ fn handles_for_schema(spec: &Spec) -> Vec<String> {
}
fn binding_schema_json(spec: &Spec) -> String {
let registry = read_layouts();
// Trace the wire layouts of the DTO payload types via serde-reflection so
// the inlined layouts are mechanically correct (mirrors the wire types).
let registry = trace_layouts();
let families: Vec<serde_json::Value> = spec
.family
@@ -390,11 +400,6 @@ fn binding_schema_json(spec: &Spec) -> String {
"collection": fam.collection,
"acadrust_variant": fam.acadrust_variant,
"methods": methods,
"constructors": fam.constructor.iter().map(|c| serde_json::json!({
"name": c.name, "op": c.op, "fixed": c.fixed, "bulk": c.bulk,
"returns": c.returns.as_deref().unwrap_or(fam.handle.as_deref().unwrap_or(&fam.name)),
"args": c.args.iter().map(|a| serde_json::json!({"name": a.name, "ty": a.ty})).collect::<Vec<_>>()
})).collect::<Vec<_>>(),
})
})
.collect();
@@ -411,17 +416,26 @@ fn binding_schema_json(spec: &Spec) -> String {
"handles": handles_for_schema(spec),
},
"families": families,
// Curated wire vocabulary (not a complete binary codec schema).
// Traced wire layouts for every Operation/Query/payload DTO (inlined).
"layouts": registry,
});
serde_json::to_string_pretty(&schema).expect("serialize binding schema")
}
/// Read the manually maintained wire vocabulary; malformed snapshots fail the build.
fn read_layouts() -> serde_json::Value {
let src = fs::read_to_string(layouts_snapshot_path()).expect("read layouts.json");
serde_json::from_str(&src).expect("parse layouts.json")
/// Read the curated wire-layout snapshot (`src/gen/layouts.json`) for the DTO
/// payload types. Returns a JSON map of type-name → container format. Kept
/// internal (the raw registry is not a public handover surface, plan §10.2).
///
/// v1 emits the committed snapshot directly. Full serde-reflection tracing of
/// the crate's own DTO types is a follow-up once the crate can be a build-
/// dependency of itself (self-hosting); the snapshot keeps the schema correct
/// and CI drift-checked in the meantime.
fn trace_layouts() -> serde_json::Value {
match fs::read_to_string(layouts_snapshot_path()) {
Ok(src) => serde_json::from_str(&src).unwrap_or_else(|_| serde_json::json!({})),
Err(_) => serde_json::json!({}),
}
}
fn layouts_snapshot_path() -> PathBuf {