Skip to main content

cargo_rdme/
lib.rs

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    /// Creates a [`Project`] the current directory.  It will search ancestor paths until it finds
54    /// the root of the project.
55    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    // TODO implement FromStr when ! type is stable.
194    #[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    // Return the markdown as a string.  Note that the line terminator will always be a line feed.
211    #[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    // TODO implement FromStr when ! type is stable.
257    #[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    // Return the markdown as a string.  Note that the line terminator will always be a line feed.
287    #[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}