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}