codama_stores/
crate_store.rs

1use cargo_toml::Manifest;
2use codama_errors::CodamaResult;
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7
8use crate::FileModuleStore;
9
10#[derive(Debug, PartialEq)]
11pub struct CrateStore {
12    pub file: syn::File,
13    pub manifest: Option<Manifest>,
14    pub file_modules: Vec<FileModuleStore>,
15    pub path: PathBuf,
16}
17
18impl CrateStore {
19    pub fn load<P: AsRef<Path>>(path: P) -> CodamaResult<Self> {
20        // Find and load the closest Cargo.toml file — a.k.a. the crate's manifest.
21        let manifest_path = get_closest_manifest_path(path.as_ref())?;
22        let mut manifest = Manifest::from_path(&manifest_path)?;
23        manifest.complete_from_path(path.as_ref())?;
24
25        // Find the crate's content from the manifest.
26        let relative_product_path = get_product_path(&manifest)?;
27        let product_path = manifest_path.parent().unwrap().join(relative_product_path);
28
29        // Load the crate's content and parse it.
30        let content = fs::read_to_string(&product_path)?;
31        let file = syn::parse_file(&content)?;
32
33        // Load all external modules from the crate's content.
34        let modules = FileModuleStore::load_all(&product_path, &file.items)?;
35
36        Ok(Self {
37            file,
38            manifest: Some(manifest),
39            file_modules: modules,
40            path: product_path.to_path_buf(),
41        })
42    }
43
44    pub fn hydrate(tt: proc_macro2::TokenStream) -> CodamaResult<Self> {
45        Ok(Self {
46            file: syn::parse2::<syn::File>(tt)?,
47            manifest: None,
48            file_modules: Vec::new(),
49            path: PathBuf::new(),
50        })
51    }
52}
53
54/// Given a path, get the closest available path to a Cargo.toml file.
55/// E.g. "my/crate/Cargo.toml" returns "my/crate/Cargo.toml"
56/// E.g. "my/crate" may return "my/crate/Cargo.toml"
57/// E.g. "my/workspace/crate" may return "my/workspace/Cargo.toml"
58pub fn get_closest_manifest_path<P: AsRef<Path>>(path: P) -> CodamaResult<PathBuf> {
59    let mut current_path = path.as_ref().canonicalize()?;
60
61    // If the initial path is a valid Cargo.toml file, return it.
62    if current_path.ends_with("Cargo.toml") && current_path.is_file() {
63        return Ok(current_path);
64    }
65
66    // Otherwise, search for the closest Cargo.toml file by moving up the directory tree.
67    loop {
68        let cargo_toml = current_path.join("Cargo.toml");
69        if cargo_toml.is_file() {
70            return Ok(cargo_toml);
71        }
72
73        // Move up one directory
74        match current_path.parent() {
75            Some(parent) => current_path = parent.to_path_buf(),
76            None => break, // Reached the root directory.
77        }
78    }
79
80    // If no Cargo.toml file was found, return an error.
81    Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Cargo.toml not found").into())
82}
83
84fn get_product_path(manifest: &Manifest) -> CodamaResult<PathBuf> {
85    let product = get_product_candidates(manifest)
86        .iter()
87        .filter_map(|product| product.path.as_ref())
88        .next();
89
90    match product {
91        Some(path) => Ok(PathBuf::from(path)),
92        None => Err(cargo_toml::Error::Other("No crate path found in Cargo.toml").into()),
93    }
94}
95
96fn get_product_candidates(manifest: &Manifest) -> Vec<&cargo_toml::Product> {
97    let mut candidates = Vec::new();
98    if let Some(product) = &manifest.lib {
99        candidates.push(product);
100    }
101    for product in &manifest.bin {
102        candidates.push(product);
103    }
104    candidates
105}