odgi_ffi/graph.rs
1// src/graph.rs
2
3//! Provides the main [`Graph`] struct for in-memory graph operations.
4//!
5//! This module defines the central [`Graph`] object, which is the primary
6//! entry point for querying a loaded ODGI graph. It also defines the
7//! associated [`Error`] type for handling failures.
8
9use cxx::UniquePtr;
10use std::error::Error as StdError;
11use std::fmt;
12use super::ffi;
13
14// Re-export the FFI data structures so they are part of the public API
15// and can be used as return types from the Graph methods.
16pub use super::ffi::{Edge, PathPosition};
17
18/// A custom error type for operations within the `odgi-ffi` crate.
19///
20/// This error is returned by functions that might fail, such as [`Graph::load`].
21#[derive(Debug)]
22pub struct Error(pub String);
23
24impl fmt::Display for Error {
25 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26 write!(f, "{}", self.0)
27 }
28}
29
30impl StdError for Error {}
31
32/// A safe, idiomatic Rust wrapper around a C++ `odgi::graph_t` object.
33///
34/// A `Graph` instance represents a pangenome graph loaded into memory.
35/// Once loaded, you can use its methods to perform various queries, such as
36/// retrieving node sequences, finding paths, and traversing the graph structure.
37///
38/// The only way to create a `Graph` is by calling [`Graph::load`].
39pub struct Graph {
40 // This field will only exist in real builds.
41 #[cfg(not(feature = "docs-only"))]
42 inner: UniquePtr<ffi::OpaqueGraph>,
43
44 // For docs builds, add a dummy field to make the struct valid.
45 #[cfg(feature = "docs-only")]
46 _inner: (),
47}
48
49// --- REAL IMPLEMENTATION (for normal builds) ---
50#[cfg(not(feature = "docs-only"))]
51impl Graph {
52 /// Loads an ODGI graph from a file into memory.
53 ///
54 /// # Arguments
55 ///
56 /// * `path` - A string slice that holds the path to the ODGI file.
57 ///
58 /// # Errors
59 ///
60 /// Returns an [`Error`] if the file does not exist or if the file format is invalid.
61 ///
62 /// # Examples
63 ///
64 /// ```rust,no_run
65 /// use odgi_ffi::Graph;
66 ///
67 /// match Graph::load("my_graph.odgi") {
68 /// Ok(graph) => println!("Graph loaded successfully!"),
69 /// Err(e) => eprintln!("Failed to load graph: {}", e),
70 /// }
71 /// ```
72 pub fn load(path: &str) -> Result<Self, Error> {
73 let graph_ptr = ffi::load_graph(path);
74 if graph_ptr.is_null() {
75 Err(Error(format!("Failed to load ODGI graph from '{}'", path)))
76 } else {
77 Ok(Graph { inner: graph_ptr })
78 }
79 }
80
81 /// Returns the total number of nodes in the graph.
82 ///
83 /// # Examples
84 ///
85 /// ```rust,no_run
86 /// # use odgi_ffi::Graph;
87 /// # let graph = Graph::load("my_graph.odgi").unwrap();
88 /// let count = graph.node_count();
89 /// println!("The graph has {} nodes.", count);
90 /// ```
91 pub fn node_count(&self) -> u64 {
92 let graph_t_ref = ffi::get_graph_t(&self.inner);
93 ffi::get_node_count(graph_t_ref)
94 }
95
96 /// Returns a list of all path names in the graph.
97 ///
98 /// # Examples
99 ///
100 /// ```rust,no_run
101 /// # use odgi_ffi::Graph;
102 /// # let graph = Graph::load("my_graph.odgi").unwrap();
103 /// let paths = graph.get_path_names();
104 /// for path_name in paths {
105 /// println!("Found path: {}", path_name);
106 /// }
107 /// ```
108 pub fn get_path_names(&self) -> Vec<String> {
109 let graph_t_ref = ffi::get_graph_t(&self.inner);
110 ffi::graph_get_path_names(graph_t_ref)
111 }
112
113 /// Projects a 0-based linear coordinate on a path to graph coordinates.
114 ///
115 /// This is useful for finding which node and offset corresponds to a
116 /// specific position along a named path.
117 ///
118 /// # Arguments
119 ///
120 /// * `path_name` - The name of the path to project onto.
121 /// * `pos` - The 0-based nucleotide position along the path.
122 ///
123 /// # Returns
124 ///
125 /// Returns `Some(PathPosition)` if the path exists and the position is
126 /// within its bounds. Returns `None` otherwise.
127 ///
128 /// # Examples
129 ///
130 /// ```rust,no_run
131 /// # use odgi_ffi::Graph;
132 /// # let graph = Graph::load("my_graph.odgi").unwrap();
133 /// if let Some(position) = graph.project("human_chr1", 1_000_000) {
134 /// println!("Position 1M on chr1 is at node {} offset {}",
135 /// position.node_id, position.offset);
136 /// } else {
137 /// println!("Position not found on path.");
138 /// }
139 /// ```
140 pub fn project(&self, path_name: &str, pos: u64) -> Option<PathPosition> {
141 let graph_t_ref = ffi::get_graph_t(&self.inner);
142 let result_ptr = ffi::graph_project(graph_t_ref, path_name, pos);
143
144 if result_ptr.is_null() {
145 None
146 } else {
147 Some(result_ptr.as_ref().unwrap().clone())
148 }
149 }
150
151 /// Gets the DNA sequence for a given node ID.
152 ///
153 /// # Arguments
154 ///
155 /// * `node_id` - The ID of the node to query.
156 ///
157 /// # Returns
158 ///
159 /// Returns the sequence as a `String`. If the `node_id` is invalid,
160 /// an empty string is returned.
161 pub fn get_node_sequence(&self, node_id: u64) -> String {
162 let graph_t_ref = ffi::get_graph_t(&self.inner);
163 ffi::graph_get_node_sequence(graph_t_ref, node_id)
164 }
165
166 /// Gets the length of the sequence for a given node ID.
167 ///
168 /// # Arguments
169 ///
170 /// * `node_id` - The ID of the node to query.
171 ///
172 /// # Returns
173 ///
174 /// Returns the sequence length. If the `node_id` is invalid, `0` is returned.
175 pub fn get_node_len(&self, node_id: u64) -> u64 {
176 let graph_t_ref = ffi::get_graph_t(&self.inner);
177 ffi::graph_get_node_len(graph_t_ref, node_id)
178 }
179
180 /// Gets all successor edges for a given node ID.
181 ///
182 /// Successors are the nodes immediately following this one in the graph topology.
183 pub fn get_successors(&self, node_id: u64) -> Vec<Edge> {
184 let graph_t_ref = ffi::get_graph_t(&self.inner);
185 ffi::graph_get_successors(graph_t_ref, node_id)
186 }
187
188 /// Gets all predecessor edges for a given node ID.
189 ///
190 /// Predecessors are the nodes immediately preceding this one in the graph topology.
191 pub fn get_predecessors(&self, node_id: u64) -> Vec<Edge> {
192 let graph_t_ref = ffi::get_graph_t(&self.inner);
193 ffi::graph_get_predecessors(graph_t_ref, node_id)
194 }
195
196 /// Gets the names of all paths that step on a given node ID.
197 pub fn get_paths_on_node(&self, node_id: u64) -> Vec<String> {
198 let graph_t_ref = ffi::get_graph_t(&self.inner);
199 ffi::graph_get_paths_on_node(graph_t_ref, node_id)
200 }
201
202 /// Gets the total length of a path in base pairs.
203 ///
204 /// # Arguments
205 ///
206 /// * `path_name` - The name of the path to measure.
207 ///
208 /// # Returns
209 ///
210 /// Returns `Some(u64)` with the path length if the path exists.
211 /// Returns `None` if no path with that name is found in the graph.
212 pub fn get_path_length(&self, path_name: &str) -> Option<u64> {
213 let graph_t_ref = ffi::get_graph_t(&self.inner);
214 // We can use the existing get_path_names to check for existence first,
215 // making our Rust API safer and more idiomatic than the C++ one.
216 if !self.get_path_names().iter().any(|p| p == path_name) {
217 return None;
218 }
219 let length = ffi::graph_get_path_length(graph_t_ref, path_name);
220 Some(length)
221 }
222 // ADD THIS NEW PUBLIC METHOD
223 /// Gets the next node ID on a given path from a specified node.
224 ///
225 /// # Returns
226 ///
227 /// Returns `Some(u64)` with the next node ID if the current node is on the
228 /// path and is not the last node. Returns `None` otherwise.
229 pub fn get_next_node_on_path(&self, node_id: u64, path_name: &str) -> Option<u64> {
230 let graph_t_ref = ffi::get_graph_t(&self.inner);
231 let next_node_id = ffi::graph_get_next_node_on_path(graph_t_ref, path_name, node_id);
232 if next_node_id >= 0 {
233 Some(next_node_id as u64)
234 } else {
235 None
236 }
237 }
238
239 /// Gets the names of all paths that traverse a specific directed edge.
240 ///
241 /// An edge is defined by a source node and a destination node, including the
242 /// orientation of each node. This function will return a unique list of
243 /// path names that step from `from_node` to `to_node` with the specified
244 /// orientations.
245 ///
246 /// # Arguments
247 ///
248 /// * `from_node` - The ID of the node where the edge begins.
249 /// * `from_orientation` - The orientation of the `from_node`. `true` for forward, `false` for reverse.
250 /// * `to_node` - The ID of the node where the edge ends.
251 /// * `to_orientation` - The orientation of the `to_node`. `true` for forward, `false` for reverse.
252 ///
253 /// # Returns
254 ///
255 /// Returns a `Vec<String>` containing the names of all paths on the given edge.
256 /// If the edge does not exist on any path, an empty vector is returned.
257 ///
258 /// # Examples
259 ///
260 /// ```rust,no_run
261 /// # use odgi_ffi::Graph;
262 /// # let graph = Graph::load("my_graph.odgi").unwrap();
263 /// // Find paths going from the forward orientation of node 1 to the forward orientation of node 2.
264 /// let paths = graph.get_paths_on_edge(1, true, 2, true);
265 /// for path_name in paths {
266 /// println!("Found path on edge 1+ -> 2+: {}", path_name);
267 /// }
268 /// ```
269 pub fn get_paths_on_edge(
270 &self,
271 from_node: u64,
272 from_orientation: bool,
273 to_node: u64,
274 to_orientation: bool,
275 ) -> Vec<String> {
276 let graph_t_ref = ffi::get_graph_t(&self.inner);
277 ffi::graph_get_paths_on_edge(
278 graph_t_ref,
279 from_node,
280 from_orientation,
281 to_node,
282 to_orientation
283 )
284 }
285}
286
287// --- MOCK IMPLEMENTATION (for docs.rs) ---
288#[cfg(feature = "docs-only")]
289impl Graph {
290 /// Loads an ODGI graph from a file into memory.
291 pub fn load(_path: &str) -> Result<Self, Error> { Ok(Graph { _inner: () }) }
292
293 /// Returns the total number of nodes in the graph.
294 pub fn node_count(&self) -> u64 { 0 }
295
296 /// Returns a list of all path names in the graph.
297 pub fn get_path_names(&self) -> Vec<String> { vec![] }
298
299 /// Projects a 0-based linear coordinate on a path to graph coordinates.
300 pub fn project(&self, _path_name: &str, _pos: u64) -> Option<PathPosition> { None }
301
302 /// Gets the DNA sequence for a given node ID.
303 pub fn get_node_sequence(&self, _node_id: u64) -> String { String::new() }
304
305 /// Gets the length of the sequence for a given node ID.
306 pub fn get_node_len(&self, _node_id: u64) -> u64 { 0 }
307
308 /// Gets all successor edges for a given node ID.
309 pub fn get_successors(&self, _node_id: u64) -> Vec<Edge> { vec![] }
310
311 /// Gets all predecessor edges for a given node ID.
312 pub fn get_predecessors(&self, _node_id: u64) -> Vec<Edge> { vec![] }
313
314 /// Gets the names of all paths that step on a given node ID.
315 pub fn get_paths_on_node(&self, _node_id: u64) -> Vec<String> { vec![] }
316
317 /// Gets the total length of a path in base pairs.
318 pub fn get_path_length(&self, _path_name: &str) -> Option<u64> { None }
319
320 /// Gets the names of all paths that traverse a specific directed edge.
321 pub fn get_paths_on_edge(
322 &self,
323 _from_node: u64,
324 _from_orientation: bool,
325 _to_node: u64,
326 _to_orientation: bool,
327 ) -> Vec<String> {
328 vec![]
329 }
330}
331
332
333/// Marks the `Graph` struct as safe to send between threads.
334// The `unsafe` keyword is our guarantee to the compiler that we've ensured
335// the underlying C++ object is safe to be sent and accessed across threads,
336// which is true in our read-only use case.
337unsafe impl Send for Graph {}
338
339/// Marks the `Graph` struct as safe to share between threads.
340// The `unsafe` keyword is our guarantee to the compiler that we've ensured
341// the underlying C++ object is safe to be sent and accessed across threads,
342// which is true in our read-only use case.
343unsafe impl Sync for Graph {}