cargo_rdme/
lib.rs

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