1#![cfg_attr(feature = "fatal-warnings", deny(warnings))]
2
3use crate::markdown::{Markdown, MarkdownError};
4use cargo_metadata::{PackageName, TargetKind};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9mod extract_doc;
10mod inject_doc;
11mod markdown;
12pub mod transform;
13pub mod utils;
14
15pub use extract_doc::{ExtractDocError, extract_doc_from_source_file};
16pub use inject_doc::{InjectDocError, MARKER_RDME, inject_doc_in_readme};
17
18#[derive(Error, Debug)]
19pub enum ProjectError {
20 #[error("failed to get cargo metadata: {0}")]
21 CargoMetadataError(cargo_metadata::Error),
22 #[error("project has no root package")]
23 ProjectHasNoRootPackage,
24 #[error("project has no package \"{0}\"")]
25 ProjectHasNoPackage(String),
26}
27
28impl From<cargo_metadata::Error> for ProjectError {
29 fn from(e: cargo_metadata::Error) -> ProjectError {
30 ProjectError::CargoMetadataError(e)
31 }
32}
33
34pub fn find_first_file_in_ancestors(dir_path: impl AsRef<Path>, filename: &str) -> Option<PathBuf> {
35 for ancestor_dir in dir_path.as_ref().ancestors() {
36 let file = ancestor_dir.join(filename);
37 if file.is_file() {
38 return Some(file);
39 }
40 }
41
42 None
43}
44
45#[derive(PartialEq, Eq, Debug)]
46pub struct Project {
47 package_name: PackageName,
48 readme_path: Option<PathBuf>,
49 lib_path: Option<PathBuf>,
50 bin_path: HashMap<String, PathBuf>,
51 directory: PathBuf,
52}
53
54impl Project {
55 pub fn from_current_dir(manifest_path: Option<&Path>) -> Result<Project, ProjectError> {
58 let metadata = Project::get_cargo_metadata(manifest_path)?;
59 let package = metadata.root_package().ok_or(ProjectError::ProjectHasNoRootPackage)?;
60
61 Ok(Project::from_package(package))
62 }
63
64 fn get_cargo_metadata(
65 manifest_path: Option<&Path>,
66 ) -> Result<cargo_metadata::Metadata, ProjectError> {
67 let mut command = cargo_metadata::MetadataCommand::new();
68
69 if let Some(manifest_path) = manifest_path {
70 command.manifest_path(manifest_path);
71 }
72
73 Ok(command.exec()?)
74 }
75
76 fn select_package<'a>(
77 metadata: &'a cargo_metadata::Metadata,
78 package_name: &str,
79 ) -> Option<&'a cargo_metadata::Package> {
80 metadata.packages.iter().find(|package| {
81 package.name == package_name && metadata.workspace_members.contains(&package.id)
82 })
83 }
84
85 pub fn from_current_dir_workspace_project(
86 manifest_path: Option<&Path>,
87 project_name: &str,
88 ) -> Result<Project, ProjectError> {
89 let metadata = Project::get_cargo_metadata(manifest_path)?;
90
91 let package = Project::select_package(&metadata, project_name)
92 .ok_or_else(|| ProjectError::ProjectHasNoPackage(project_name.to_owned()))?;
93
94 Ok(Project::from_package(package))
95 }
96
97 fn from_package(package: &cargo_metadata::Package) -> Project {
98 const LIB_CRATE_KINDS: [TargetKind; 6] = [
99 TargetKind::Lib,
100 TargetKind::DyLib,
101 TargetKind::StaticLib,
102 TargetKind::CDyLib,
103 TargetKind::RLib,
104 TargetKind::ProcMacro,
105 ];
106 let lib_packages: Vec<&cargo_metadata::Target> = package
107 .targets
108 .iter()
109 .filter(|target| target.kind.iter().any(|k| LIB_CRATE_KINDS.contains(k)))
110 .collect();
111
112 assert!(lib_packages.len() <= 1, "more than one lib target");
113
114 let lib_package = lib_packages.first();
115
116 let bin_packages =
117 package.targets.iter().filter(|target| target.kind.contains(&TargetKind::Bin));
118
119 let directory = package
120 .manifest_path
121 .clone()
122 .into_std_path_buf()
123 .parent()
124 .expect("error getting the parent path of the manifest file")
125 .to_path_buf();
126
127 Project {
128 package_name: package.name.clone(),
129 readme_path: package.readme.as_ref().map(|p| p.clone().into_std_path_buf()),
130 lib_path: lib_package.map(|t| t.src_path.clone().into_std_path_buf()),
131 bin_path: bin_packages
132 .map(|t| (t.name.clone(), t.src_path.clone().into_std_path_buf()))
133 .collect(),
134 directory,
135 }
136 }
137
138 #[must_use]
139 pub fn get_lib_entryfile_path(&self) -> Option<&Path> {
140 self.lib_path.as_ref().filter(|p| p.is_file()).map(PathBuf::as_path)
141 }
142
143 #[must_use]
144 pub fn get_bin_default_entryfile_path(&self) -> Option<&Path> {
145 match self.bin_path.len() {
146 1 => self
147 .bin_path
148 .keys()
149 .next()
150 .and_then(|bin_name| self.get_bin_entryfile_path(bin_name)),
151 _ => None,
152 }
153 }
154
155 #[must_use]
156 pub fn get_bin_entryfile_path(&self, name: &str) -> Option<&Path> {
157 self.bin_path.get(name).filter(|p| p.is_file()).map(PathBuf::as_path)
158 }
159
160 #[must_use]
161 pub fn get_readme_path(&self) -> Option<PathBuf> {
162 self.readme_path
163 .clone()
164 .or_else(|| Some(Path::new("README.md").to_path_buf()))
165 .map(|p| self.directory.join(p))
166 .filter(|p| p.is_file())
167 }
168
169 #[must_use]
170 pub fn get_package_name(&self) -> &PackageName {
171 &self.package_name
172 }
173}
174
175fn project_package_name(manifest_path: impl AsRef<Path>) -> Option<String> {
176 let str: String = std::fs::read_to_string(&manifest_path).ok()?;
177 let toml: toml::Value = toml::from_str(&str).ok()?;
178 let package_name =
179 toml.get("package").and_then(|v| v.get("name")).and_then(toml::Value::as_str)?;
180
181 Some(package_name.to_owned())
182}
183
184#[derive(Eq, PartialEq, Clone, Debug)]
185pub struct Doc {
186 pub markdown: Markdown,
187}
188
189impl Doc {
190 #[must_use]
191 pub fn from_markdown(markdown: Markdown) -> Doc {
192 Doc { markdown }
193 }
194
195 #[allow(clippy::should_implement_trait)]
197 pub fn from_str(str: impl Into<String>) -> Doc {
198 Doc { markdown: Markdown::from_str(str) }
199 }
200
201 fn is_toplevel_doc(attr: &syn::Attribute) -> bool {
202 use syn::AttrStyle;
203 use syn::token::Not;
204
205 attr.style == AttrStyle::Inner(Not::default()) && attr.path().is_ident("doc")
206 }
207
208 pub fn lines(&self) -> impl Iterator<Item = &str> {
209 self.markdown.lines()
210 }
211
212 #[must_use]
214 pub fn as_string(&self) -> &str {
215 self.markdown.as_string()
216 }
217}
218
219#[derive(Error, Debug)]
220pub enum ReadmeError {
221 #[error("failed to read README file \"{0}\"")]
222 ErrorReadingReadmeFromFile(PathBuf),
223 #[error("failed to write README file \"{0}\"")]
224 ErrorWritingMarkdownToFile(PathBuf),
225 #[error("failed to write README")]
226 ErrorWritingMarkdown,
227}
228
229impl From<MarkdownError> for ReadmeError {
230 fn from(e: MarkdownError) -> ReadmeError {
231 match e {
232 MarkdownError::ErrorReadingMarkdownFromFile(p) => {
233 ReadmeError::ErrorReadingReadmeFromFile(p)
234 }
235 MarkdownError::ErrorWritingMarkdownToFile(p) => {
236 ReadmeError::ErrorWritingMarkdownToFile(p)
237 }
238 MarkdownError::ErrorWritingMarkdown => ReadmeError::ErrorWritingMarkdown,
239 }
240 }
241}
242
243#[derive(Eq, PartialEq, Debug, Copy, Clone)]
244pub enum LineTerminator {
245 Lf,
246 CrLf,
247}
248
249pub struct Readme {
250 pub markdown: Markdown,
251}
252
253impl Readme {
254 pub fn from_file(file_path: impl AsRef<Path>) -> Result<Readme, ReadmeError> {
255 Ok(Readme { markdown: Markdown::from_file(file_path)? })
256 }
257
258 #[allow(clippy::should_implement_trait)]
260 pub fn from_str(str: impl Into<String>) -> Readme {
261 Readme { markdown: Markdown::from_str(str) }
262 }
263
264 pub fn from_lines(lines: &[impl AsRef<str>]) -> Readme {
265 Readme { markdown: Markdown::from_lines(lines) }
266 }
267
268 pub fn lines(&self) -> impl Iterator<Item = &str> {
269 self.markdown.lines()
270 }
271
272 pub fn write_to_file(
273 &self,
274 file: impl AsRef<Path>,
275 line_terminator: LineTerminator,
276 ) -> Result<(), ReadmeError> {
277 Ok(self.markdown.write_to_file(file, line_terminator)?)
278 }
279
280 pub fn write(
281 &self,
282 writer: impl std::io::Write,
283 line_terminator: LineTerminator,
284 ) -> Result<(), ReadmeError> {
285 Ok(self.markdown.write(writer, line_terminator)?)
286 }
287
288 #[must_use]
290 pub fn as_string(&self) -> &str {
291 self.markdown.as_string()
292 }
293}
294
295pub fn infer_line_terminator(file_path: impl AsRef<Path>) -> std::io::Result<LineTerminator> {
296 let content: String = std::fs::read_to_string(file_path.as_ref())?;
297
298 let crlf_lines: usize = content.matches("\r\n").count();
299 let lf_lines: usize = content.matches('\n').count() - crlf_lines;
300
301 if crlf_lines > lf_lines { Ok(LineTerminator::CrLf) } else { Ok(LineTerminator::Lf) }
302}