From cdddaf8963920d8f49790723ee63e04768f44311 Mon Sep 17 00:00:00 2001 From: dani-giron Date: Mon, 17 Aug 2026 12:30:43 +0200 Subject: [PATCH 1/2] Return Result from draw() and save_gexf() instead of panicking or discarding errors (closes #33) --- src/controllers/apotheosis.rs | 17 ++++++++++++----- tests/api_contract.rs | 3 ++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/controllers/apotheosis.rs b/src/controllers/apotheosis.rs index 70f7ca6..ff1cd50 100644 --- a/src/controllers/apotheosis.rs +++ b/src/controllers/apotheosis.rs @@ -213,7 +213,7 @@ where /// /// # Parameters /// * `path` - Base filename for output (e.g., "model" creates "model_layer0.gexf", "model_layer1.gexf", etc.) - pub fn draw>(&self, path: P) { + pub fn draw>(&self, path: P) -> Result<(), Box> { let base_path = path.as_ref(); let layer_gexfs = self.hnsw.draw_model(); @@ -240,8 +240,10 @@ where ); } - let _ = self.save_gexf(base_path, layer_idx, &gexf); + self.save_gexf(base_path, layer_idx, &gexf)?; } + + Ok(()) } // Once draw is built, we need to add the attribute schema to the GEXF XML @@ -272,7 +274,12 @@ where xml } - fn save_gexf(&self, base_path: &Path, layer_idx: usize, gexf: &Gexf) -> std::io::Result<()> { + fn save_gexf( + &self, + base_path: &Path, + layer_idx: usize, + gexf: &Gexf, + ) -> Result<(), Box> { let mut file_path = PathBuf::from(base_path); if let Some(stem) = file_path.file_stem().and_then(|s| s.to_str()) { @@ -285,9 +292,9 @@ where } let fixed_xml = if !self.records.is_empty() { - self.add_attribute_schema(gexf.to_string().unwrap(), self.records[0].get_attributes()) + self.add_attribute_schema(gexf.to_string()?, self.records[0].get_attributes()) } else { - gexf.to_string().unwrap() + gexf.to_string()? }; fs::write(file_path, fixed_xml)?; diff --git a/tests/api_contract.rs b/tests/api_contract.rs index 3b4a0a3..d7730bb 100644 --- a/tests/api_contract.rs +++ b/tests/api_contract.rs @@ -76,7 +76,8 @@ fn draw_produces_one_gexf_file_per_layer() { } let layer_count = idx.draw_model().len(); - idx.draw(&base); + idx.draw(&base) + .expect("draw() must succeed for this dataset"); for n in 0..layer_count { let f = PathBuf::from(format!("{}/model_layer{}.gexf", dir, n)); From bae1f94f774ef54ff79788e1da78d070749770cd Mon Sep 17 00:00:00 2001 From: danielhuici Date: Tue, 1 Sep 2026 17:18:06 +0200 Subject: [PATCH 2/2] docs: export::gexf::draw returns Result Update the interface document, the C&C connector table and the README example for the error propagation introduced by #34. --- README.md | 4 ++-- docs/architecture/interfaz-apotheosis.md | 13 ++++++++----- docs/architecture/views/cc/vista-cc.md | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7c440af..107df9a 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ A built model can be saved to disk and loaded back. `load()` validates the M, M0 index.dump("model.bin")?; let loaded: Apotheosis = Apotheosis::load("model.bin")?; -// export::gexf::draw() writes model_layer0.gexf, model_layer1.gexf, ... next to the given base path. -apotheosis2::export::gexf::draw(&index, "model"); +// export::gexf::draw() also returns Result and writes model_layer0.gexf, model_layer1.gexf, ... +apotheosis2::export::gexf::draw(&index, "model")?; ``` diff --git a/docs/architecture/interfaz-apotheosis.md b/docs/architecture/interfaz-apotheosis.md index 434c49d..f6bfb65 100644 --- a/docs/architecture/interfaz-apotheosis.md +++ b/docs/architecture/interfaz-apotheosis.md @@ -207,7 +207,10 @@ The bound `where Self: serde::de::DeserializeOwned` imposes the same restriction ```rust // free function in the export::gexf module, not a facade method -pub fn draw>(model: &Apotheosis<...>, path: P) +pub fn draw>( + model: &Apotheosis<...>, + path: P, +) -> Result<(), Box> ``` **Semantics** @@ -218,7 +221,7 @@ Each GEXF file contains nodes (one per entry in that HNSW layer, identified by i **Error Handling** -Does not return `Result`. I/O errors when writing each GEXF file are silently discarded: the code uses `let _ = save_gexf(...)`. GEXF XML serialization errors (`gexf.to_string().unwrap()`) produce a panic. +Returns `Err` on I/O errors when writing a GEXF file and on GEXF XML serialization errors. The first failing layer aborts the export; files already written remain on disk. @@ -545,15 +548,15 @@ The crate does not adopt a uniform error-handling strategy: different methods us #### `Result<_, Box>` -Used by `dump`, `load`, and the `create` record constructors. The error type is a dynamic trait object that can wrap any I/O or serialization error. The caller must match on or propagate the error with `?`. There are no structured error types that would allow programmatic distinction between the possible causes (I/O failure, parameter mismatch, malformed file). +Used by `dump`, `load`, `export::gexf::draw`, and the `create` record constructors. The error type is a dynamic trait object that can wrap any I/O or serialization error. The caller must match on or propagate the error with `?`. There are no structured error types that would allow programmatic distinction between the possible causes (I/O failure, parameter mismatch, malformed file). #### Panics -`export::gexf::draw` can panic if the internal GEXF XML serialization fails (`gexf.to_string().unwrap()` in `save_gexf`). No public facade method has identifiable panics in the normal flow; the `create` record constructors return `Err` on malformed input instead of panicking. +No public method has identifiable panics in the normal flow: `export::gexf::draw` returns `Err` instead of panicking on serialization failures, and the `create` record constructors return `Err` on malformed input. #### Absence of error return -`insert` returns `bool` rather than `Result`: duplicate keys are signaled with `false` and a warning, without propagating an error to the caller. `export::gexf::draw` has no explicit error paths: I/O errors when writing each GEXF file are silently discarded via `let _ = save_gexf(...)`. +`insert` returns `bool` rather than `Result`: duplicate keys are signaled with `false` and a warning, without propagating an error to the caller. --- diff --git a/docs/architecture/views/cc/vista-cc.md b/docs/architecture/views/cc/vista-cc.md index 9c127c2..535397a 100644 --- a/docs/architecture/views/cc/vista-cc.md +++ b/docs/architecture/views/cc/vista-cc.md @@ -93,7 +93,7 @@ The course notes define *attachment* as the association between component ports | `apo↔radix-lookup` | `<>` | `Apotheosis::index-radix` | `RadixTree::lookup` | Input: `key: &[u8]` / Output: `Option<&RadixNode>` (Apotheosis extracts `Option` from `node.data`). If `Some(index)`: fast-path; if `None`: ANN path | First operation in every `search()` call. Its result determines which path is taken | | `apo→radix-insert` | `<>` | `Apotheosis::index-radix` | `RadixTree::insert` | Input: `(key: Vec, index: usize)` / no return | Executed on every new insertion, always alongside `hnsw-insert` | | `apo↔fs-io` | `<>` | `Apotheosis::persistence` | `FileSystem::io` | `dump(path)` writes to FS; `load(path)` reads from FS into Apotheosis | Bidirectional. `load` reconstructs the full model in memory | -| `apo→fs-gexf` | `<>` | `Apotheosis::persistence` | `FileSystem::gexf-out` | `export::gexf::draw(&model, path)` writes one `.gexf` file per HNSW layer with pattern `_layer.gexf` | Write only. No return value | +| `apo→fs-gexf` | `<>` | `Apotheosis::persistence` | `FileSystem::gexf-out` | `export::gexf::draw(&model, path)` writes one `.gexf` file per HNSW layer with pattern `_layer.gexf` | Write only. Returns `Result`: the first failed write or serialization aborts the export | | `fs→gephi` | `<>` | `FileSystem::gexf-out` | `Gephi::gexf-in` | `.gexf` file | Asynchronous with respect to APOTHEOSIS 2. Gephi opens the file independently; no runtime coupling | ### 2.3 Element Interfaces