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