1use crate::{
2 backend::{self, BuiltinBackend, Callbacks, Resolver},
3 documentation::Documentation,
4 ConfigFile, Error, GodotVersion,
5};
6use std::{fs, path::PathBuf};
7
8#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub enum Package {
11 Name(String),
13 Root(PathBuf),
15}
16
17#[derive(Debug)]
18pub struct Builder {
26 backends: Vec<(Box<dyn Callbacks>, PathBuf)>,
28 user_config: ConfigFile,
30 package: Option<Package>,
32}
33
34impl Default for Builder {
35 fn default() -> Self {
36 Self::new()
37 }
38}
39
40impl Builder {
41 pub fn new() -> Self {
43 Self {
44 backends: Vec::new(),
45 user_config: ConfigFile::default(),
46 package: None,
47 }
48 }
49
50 pub fn user_config(mut self, config: ConfigFile) -> Self {
54 self.user_config = config;
55 self
56 }
57
58 pub fn package(mut self, package: Package) -> Self {
74 self.package = Some(package);
75 self
76 }
77
78 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 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 #[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 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
174fn 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}