gdnative_doc/
builder.rs

1use crate::{
2    backend::{self, BuiltinBackend, Callbacks, Resolver},
3    documentation::Documentation,
4    ConfigFile, Error, GodotVersion,
5};
6use std::{fs, path::PathBuf};
7
8/// Used to specify a crate in [`Builder::package`].
9#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub enum Package {
11    /// Specify the crate by name
12    Name(String),
13    /// Specify the crate by the path of its root file
14    Root(PathBuf),
15}
16
17#[derive(Debug)]
18/// A builder for generating godot documentation in various formats.
19///
20/// For each format you want to generate, you must add a backend via [`add_backend`]
21/// or [`add_backend_with_callbacks`].
22///
23/// [`add_backend`]: Builder::add_backend
24/// [`add_backend_with_callbacks`]: Builder::add_backend_with_callbacks
25pub struct Builder {
26    /// List of backends with their output directory
27    backends: Vec<(Box<dyn Callbacks>, PathBuf)>,
28    /// Configuration file
29    user_config: ConfigFile,
30    /// Used to disambiguate which crate to use.
31    package: Option<Package>,
32}
33
34impl Default for Builder {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl Builder {
41    /// Create a default `Builder` with no backends.
42    pub fn new() -> Self {
43        Self {
44            backends: Vec::new(),
45            user_config: ConfigFile::default(),
46            package: None,
47        }
48    }
49
50    /// Set user configuration options.
51    ///
52    /// See the `ConfigFile` documentation for information about the configuration file format.
53    pub fn user_config(mut self, config: ConfigFile) -> Self {
54        self.user_config = config;
55        self
56    }
57
58    /// Specify the crate to document.
59    ///
60    /// The `Builder` will try to automatically determine which crate you want to document. If this fails or you are not satisfied with its guess, you can use this function to manually specify the crate you want to refer to.
61    ///
62    /// This can be either the [name](Package::Name) of the crate, or directly the
63    /// [path of the root file](Package::Root).
64    ///
65    /// Only one crate can be documented at a time: if this function is called
66    /// multiple times, the last call will prevail.
67    ///
68    /// # Example
69    /// ```
70    /// # use gdnative_doc::{Builder, Package};
71    /// let builder = Builder::new().package(Package::Name("my-gdnative-crate".to_string()));
72    /// ```
73    pub fn package(mut self, package: Package) -> Self {
74        self.package = Some(package);
75        self
76    }
77
78    /// Add a new builtin backend to the builder.
79    ///
80    /// # Example
81    /// ```
82    /// # use gdnative_doc::{Builder, backend::BuiltinBackend};
83    /// # use std::path::PathBuf;
84    /// let builder = Builder::new().add_backend(BuiltinBackend::Markdown, PathBuf::from("doc"));
85    /// ```
86    pub fn add_backend(mut self, backend: BuiltinBackend, output_dir: PathBuf) -> Self {
87        let callbacks: Box<dyn Callbacks> = match &backend {
88            BuiltinBackend::Markdown => Box::new(backend::MarkdownCallbacks::default()),
89            BuiltinBackend::Html => Box::new(backend::HtmlCallbacks::default()),
90            BuiltinBackend::Gut => Box::new(backend::GutCallbacks::default()),
91        };
92        self.backends.push((callbacks, output_dir));
93        self
94    }
95
96    /// Add a new backend to the builder, with custom callbacks encoding functions.
97    ///
98    /// See the [`backend`](crate::backend) module for how to implement your own
99    /// backend.
100    pub fn add_backend_with_callbacks(
101        mut self,
102        callbacks: Box<dyn Callbacks>,
103        output_dir: PathBuf,
104    ) -> Self {
105        self.backends.push((callbacks, output_dir));
106        self
107    }
108
109    /// Build the documentation.
110    ///
111    /// This will generate the documentation for each
112    /// [specified backend](Self::add_backend), creating the ouput directories if
113    /// needed.
114    #[allow(clippy::or_fun_call)]
115    pub fn build(mut self) -> Result<(), Error> {
116        let mut resolver = Resolver::new(match &self.user_config.godot_version {
117            Some(s) => GodotVersion::try_from(s.as_str())?,
118            None => GodotVersion::Version35,
119        });
120
121        let (markdown_options, opening_comment) = {
122            let opening_comment = self.user_config.opening_comment.unwrap_or(true);
123            let markdown_options = self
124                .user_config
125                .markdown_options()
126                .unwrap_or(pulldown_cmark::Options::empty());
127            resolver.apply_user_config(&self.user_config);
128            (markdown_options, opening_comment)
129        };
130
131        let documentation = self.build_documentation(&resolver)?;
132        for (mut callbacks, output_dir) in self.backends {
133            let generator = backend::Generator::new(
134                &resolver,
135                &documentation,
136                markdown_options,
137                opening_comment,
138            );
139
140            let files = callbacks.generate_files(generator);
141
142            if let Err(err) = fs::create_dir_all(&output_dir) {
143                return Err(Error::Io(output_dir, err));
144            }
145            for (file_name, content) in files {
146                let out_file = output_dir.join(file_name);
147                if let Err(err) = fs::write(&out_file, content) {
148                    return Err(Error::Io(out_file, err));
149                }
150            }
151        }
152
153        Ok(())
154    }
155
156    /// Build documentation from a root file.
157    ///
158    /// The root file is either stored in `self`, or automatically discovered using
159    /// [`find_root_file`].
160    fn build_documentation(&mut self, resolver: &Resolver) -> Result<Documentation, Error> {
161        log::debug!("building documentation");
162        let (name, root_file) = match self.package.take() {
163            Some(Package::Root(root_file)) => ("_".to_string(), root_file),
164            Some(Package::Name(name)) => find_root_file(Some(&name))?,
165            None => find_root_file(None)?,
166        };
167
168        let mut documentation = Documentation::from_root_file(name, root_file)?;
169        resolver.rename_classes(&mut documentation);
170        Ok(documentation)
171    }
172}
173
174/// Returns the name of the crate and the root file.
175fn find_root_file(package_name: Option<&str>) -> Result<(String, PathBuf), Error> {
176    let metadata = cargo_metadata::MetadataCommand::new().exec()?;
177    let mut root_files = Vec::new();
178    for package in metadata.packages {
179        if metadata.workspace_members.contains(&package.id) {
180            if let Some(target) = package
181                .targets
182                .into_iter()
183                .find(|target| target.kind.iter().any(|kind| kind == "cdylib"))
184            {
185                root_files.push((package.name, target.src_path.into()))
186            }
187        }
188    }
189
190    if let Some(package_name) = package_name {
191        match root_files
192            .into_iter()
193            .find(|(name, _)| name == package_name)
194        {
195            Some((_, root_file)) => Ok((package_name.to_string(), root_file)),
196            None => Err(Error::NoMatchingCrate(package_name.to_string())),
197        }
198    } else {
199        if root_files.len() > 1 {
200            return Err(Error::MultipleCandidateCrate(
201                root_files.into_iter().map(|(name, _)| name).collect(),
202            ));
203        }
204        if let Some((name, root_file)) = root_files.pop() {
205            Ok((name, root_file))
206        } else {
207            Err(Error::NoCandidateCrate)
208        }
209    }
210}