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(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    /// 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(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    // TODO implement FromStr when ! type is stable.
205    #[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    // Return the markdown as a string.  Note that the line terminator will always be a line feed.
222    #[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    // TODO implement FromStr when ! type is stable.
268    #[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    // Return the markdown as a string.  Note that the line terminator will always be a line feed.
298    #[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}