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