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