1//! The backend for the sphinx_rust Python package.
2//!
3//! This module provides a Python interface to the ``analyzer`` crate.
4//!
5//! ```{req} Integrate rust with sphinx
6//! :id: RUST001
7//! :tags: rust
8//!
9//! We need to integrate Sphinx with Rust so that we can use the `sphinx_rust` backend to generate documentation for Rust code.
10//! ```
11
12use pyo3::{exceptions::PyIOError, prelude::*};
13
14use analyzer::analyze;
15
16pub mod data_model;
17pub mod data_query;
18
19#[pymodule]
20/// sphinx_rust backend
21// Note: The name of this function must match the `lib.name` setting in the `Cargo.toml`,
22// else Python will not be able to import the module.
23fn sphinx_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
24 m.add("__version__", env!("CARGO_PKG_VERSION"))?;
25 m.add_function(wrap_pyfunction!(analyze_crate, m)?)?;
26 m.add_class::<data_model::Crate>()?;
27 m.add_class::<data_model::Module>()?;
28 m.add_class::<data_model::Struct>()?;
29 m.add_class::<data_model::Field>()?;
30 m.add_class::<data_model::TypeSegment>()?;
31 m.add_class::<data_model::Enum>()?;
32 m.add_class::<data_model::Variant>()?;
33 m.add_class::<data_model::Function>()?;
34 m.add_class::<AnalysisResult>()?;
35 m.add_function(wrap_pyfunction!(data_query::load_crate, m)?)?;
36 m.add_function(wrap_pyfunction!(data_query::load_module, m)?)?;
37 m.add_function(wrap_pyfunction!(data_query::load_struct, m)?)?;
38 m.add_function(wrap_pyfunction!(data_query::load_enum, m)?)?;
39 m.add_function(wrap_pyfunction!(data_query::load_function, m)?)?;
40 m.add_function(wrap_pyfunction!(data_query::load_child_modules, m)?)?;
41 m.add_function(wrap_pyfunction!(data_query::load_child_structs, m)?)?;
42 m.add_function(wrap_pyfunction!(data_query::load_child_enums, m)?)?;
43 m.add_function(wrap_pyfunction!(data_query::load_child_functions, m)?)?;
44 m.add_function(wrap_pyfunction!(data_query::load_descendant_modules, m)?)?;
45 m.add_function(wrap_pyfunction!(data_query::load_descendant_structs, m)?)?;
46 m.add_function(wrap_pyfunction!(data_query::load_descendant_enums, m)?)?;
47 Ok(())
48}
49
50#[pyfunction]
51/// analyse a crate and cache the results to disk
52pub fn analyze_crate(crate_path: &str, cache_path: &str) -> PyResult<AnalysisResult> {
53 // check that the cache path is a directory
54 let cache_path = std::path::Path::new(cache_path);
55 if !cache_path.is_dir() {
56 return Err(PyIOError::new_err(format!(
57 "cache_path is not an existing directory: {}",
58 cache_path.to_string_lossy()
59 )));
60 }
61
62 // perform the analysis
63 let result = match analyze::analyze_crate(crate_path) {
64 Ok(result) => result,
65 Err(err) => {
66 return Err(PyIOError::new_err(format!(
67 "Could not analyze crate: {}",
68 err.chain()
69 .map(|err| err.to_string())
70 .collect::<Vec<_>>()
71 .join("\n")
72 )))
73 }
74 };
75
76 let mut output = AnalysisResult::default();
77
78 // now cache the results
79 // note we don't write to disk, if the file already exists and has the same contents
80 // this is because Sphinx uses the file's mtime in determining whether to rebuild
81 // TODO should also delete files that refer to objects that no longer exist
82 let crates_path = cache_path.join("crates");
83 if !crates_path.exists() {
84 std::fs::create_dir(&crates_path)?;
85 }
86 output.crate_ = result.crate_.name.clone();
87 let crate_path = crates_path.join(format!("{}.json", result.crate_.name));
88 serialize_to_file(&crate_path, &result.crate_)?;
89
90 let modules_path = cache_path.join("modules");
91 if !modules_path.exists() {
92 std::fs::create_dir(&modules_path)?;
93 }
94 for mod_ in &result.modules {
95 output.modules.push(mod_.path_str().clone());
96 let mod_path = modules_path.join(format!("{}.json", mod_.path_str()));
97 serialize_to_file(&mod_path, &mod_)?;
98 }
99 let structs_path = cache_path.join("structs");
100 if !structs_path.exists() {
101 std::fs::create_dir(&structs_path)?;
102 }
103 for struct_ in &result.structs {
104 output.structs.push(struct_.path_str().clone());
105 let struct_path = structs_path.join(format!("{}.json", struct_.path_str()));
106 serialize_to_file(&struct_path, &struct_)?;
107 }
108 let enums_path = cache_path.join("enums");
109 if !enums_path.exists() {
110 std::fs::create_dir(&enums_path)?;
111 }
112 for enum_ in &result.enums {
113 output.enums.push(enum_.path_str().clone());
114 let enum_path = enums_path.join(format!("{}.json", enum_.path_str()));
115 serialize_to_file(&enum_path, &enum_)?;
116 }
117 let funcs_path = cache_path.join("functions");
118 if !funcs_path.exists() {
119 std::fs::create_dir(&funcs_path)?;
120 }
121 for func in &result.functions {
122 output.functions.push(func.path_str().clone());
123 let func_path = funcs_path.join(format!("{}.json", func.path_str()));
124 serialize_to_file(&func_path, &func)?;
125 }
126 Ok(output)
127}
128
129#[pyclass]
130#[derive(Debug, Clone, Default)]
131/// pyo3 representation of the result of an analysis
132pub struct AnalysisResult {
133 #[pyo3(get)]
134 pub crate_: String,
135 #[pyo3(get)]
136 pub modules: Vec<String>,
137 #[pyo3(get)]
138 pub structs: Vec<String>,
139 #[pyo3(get)]
140 pub enums: Vec<String>,
141 #[pyo3(get)]
142 pub functions: Vec<String>,
143}
144
145#[pymethods]
146impl AnalysisResult {
147 pub fn __repr__(&self) -> String {
148 format!(
149 "AnalysisResult(crate={:?},\n modules={:?},\n structs={:?},\n enums={:?}\n)",
150 self.crate_, self.modules, self.structs, self.enums
151 )
152 }
153}
154
155/// Serialize a value to a file.
156/// The file is only written if the value is different from any existing value.
157fn serialize_to_file<T>(path: &std::path::Path, value: &T) -> PyResult<()>
158where
159 T: serde::Serialize,
160{
161 let value = match serde_json::to_string(value) {
162 Ok(value) => value,
163 Err(err) => {
164 return Err(PyIOError::new_err(format!(
165 "Could not serialize value: {}",
166 err
167 )))
168 }
169 };
170 if path.exists() {
171 match std::fs::read_to_string(path) {
172 Ok(old_value) => {
173 if value == old_value {
174 return Ok(());
175 }
176 }
177 Err(_) => {}
178 };
179 }
180 match std::fs::write(path, value) {
181 Err(err) => Err(PyIOError::new_err(format!(
182 "Could not write value to file: {}",
183 err
184 ))),
185 Ok(_) => Ok(()),
186 }
187}