cargo_rdme/
lib.rs

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