forked from 1b-agent/opencadstudio-upstream
General improvements on API
This commit is contained in:
1 parent
56e09cd3f5
commit
55f61a4327
30 files changed
+1820
-2565
No files matched your search
+62
-48
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user