readme_sync/
manifest.rs

1use std::collections::{HashMap, HashSet};
2use std::io;
3use std::path::{Path, PathBuf};
4use std::string::String;
5use std::vec::Vec;
6
7use serde::Deserialize;
8use thiserror::Error;
9
10/// Package manifest.
11///
12/// It includes only fields that are necessary for
13/// locating and parsing readme and library documentation.
14///
15/// See <https://doc.rust-lang.org/cargo/reference/manifest.html> for more details.
16#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
17pub struct Manifest {
18    /// Defines a package.
19    pub package: ManifestPackage,
20    /// Library target settings.
21    pub lib: Option<ManifestLibTarget>,
22    /// Binary target settings.
23    pub bin: Option<Vec<ManifestBinTarget>>,
24    /// Conditional compilation features.
25    pub features: Option<HashMap<String, HashSet<String>>>,
26    /// Package library dependencies.
27    pub dependencies: Option<HashMap<String, ManifestDependency>>,
28    /// Metadata that customize docs.rs builds.
29    #[serde(rename = "package.metadata.docs.rs")]
30    pub docs_meta: Option<ManifestDocsRsMetadata>,
31}
32
33/// Package manifest `[package]` section.
34///
35/// See <https://doc.rust-lang.org/cargo/reference/manifest.html#the-package-section> for more details.
36#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
37pub struct ManifestPackage {
38    /// The package name that is used to locate main binary,
39    /// add package title, disallow package docs links, use absolute package docs links.
40    pub name: String,
41    /// The package version that is not used by current library but defined as a required by Cargo.
42    pub version: String,
43    /// The `documentation` field specifies a URL to a website hosting the crate's documentation.
44    pub documentation: Option<String>,
45    /// The `readme` field specifies a path to a readme file in the package root (relative to this Cargo.toml).
46    pub readme: Option<ManifestReadmePath>,
47    /// The `repository` field specifies a URL to the source repository for the package.
48    pub repository: Option<String>,
49}
50
51/// Package manifest `[lib]` section.
52///
53/// See <https://doc.rust-lang.org/cargo/reference/cargo-targets.html#library> for more details.
54#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
55pub struct ManifestLibTarget {
56    /// The name of the target.
57    pub name: Option<String>,
58    /// The source file of the target.
59    pub path: Option<String>,
60    /// Is documented by default.
61    pub doc: Option<bool>,
62}
63
64/// Package manifest `[[bin]]` section.
65///
66/// See <https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries> for more details.
67#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
68pub struct ManifestBinTarget {
69    /// The name of the target.
70    pub name: String,
71    /// The source file of the target.
72    pub path: Option<String>,
73    /// Is documented by default.
74    pub doc: Option<bool>,
75}
76
77/// Package manifest dependency.
78///
79/// See <https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html> for more details.
80#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
81#[serde(untagged)]
82pub enum ManifestDependency {
83    /// Readme path.
84    Version(String),
85    /// If the field is set to true, a default value of README.md will be assumed.
86    /// If the field is set to false, a readme file is defined as absent.
87    Details(ManifestDependencyDetails),
88}
89
90/// Package manifest dependency details.
91///
92/// See <https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html> for more details.
93#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize)]
94pub struct ManifestDependencyDetails {
95    /// Is the dependency is optional and therefore adds a feature with the specified name.
96    pub optional: Option<bool>,
97}
98
99/// Manifest metadata that customize docs.rs builds.
100///
101/// See <https://docs.rs/about/metadata> for more details
102#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
103pub struct ManifestDocsRsMetadata {
104    /// Features to pass to Cargo (default: []).alloc
105    pub features: Option<HashSet<String>>,
106    /// Whether to pass `--all-features` to Cargo (default: false).
107    #[serde(rename = "all-features")]
108    pub all_features: Option<bool>,
109    /// Whether to pass `--no-default-features` to Cargo (default: false).
110    #[serde(rename = "no-default-features")]
111    pub no_default_features: Option<bool>,
112    /// Target to test build on, used as the default landing page.
113    #[serde(rename = "default-target")]
114    pub default_target: Option<String>,
115    /// Targets to build.
116    pub targets: Option<Vec<String>>,
117}
118
119/// The optional Manifest `readme` field that allows string or boolean value.
120///
121/// If `readme` field is not specified, and a file named README.md, README.txt or README
122/// exists in the package root, then the name of that file will be used.
123///
124/// See <https://doc.rust-lang.org/cargo/reference/manifest.html#the-readme-field> for more details.
125#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
126#[serde(untagged)]
127pub enum ManifestReadmePath {
128    /// Readme path.
129    Path(PathBuf),
130    /// If the field is set to true, a default value of README.md will be assumed.
131    /// If the field is set to false, a readme file is defined as absent.
132    Bool(bool),
133}
134
135impl Manifest {
136    /// Creates simple manifest from package name and version.
137    pub fn from_name_and_version(name: String, version: String) -> Self {
138        Manifest {
139            package: ManifestPackage {
140                name,
141                version,
142                repository: None,
143                documentation: None,
144                readme: None,
145            },
146            lib: None,
147            bin: None,
148            features: None,
149            dependencies: None,
150            docs_meta: None,
151        }
152    }
153
154    /// Creates manifest from `Cargo.toml` file contents.
155    pub fn from_cargo_toml_content(content: &str) -> Result<Self, TomlParseError> {
156        Ok(toml::from_str(content)?)
157    }
158
159    /// Reads manifest from a specified file path.
160    pub fn from_cargo_toml_path(path: &Path) -> Result<Self, TomlReadError> {
161        let content = std::fs::read_to_string(path).map_err(|err| TomlReadError::IoError {
162            path: path.to_path_buf(),
163            err,
164        })?;
165        Self::from_cargo_toml_content(&content).map_err(|err| TomlReadError::ParseError {
166            path: path.to_path_buf(),
167            err,
168        })
169    }
170
171    /// Reads manifest from the `Cargo.toml` file in the specified package path.
172    pub fn from_package_path(path: &Path) -> Result<Self, TomlReadError> {
173        Self::from_cargo_toml_path(&path.join("Cargo.toml"))
174    }
175
176    /// Returns package relative readme path.
177    pub fn relative_readme_path(&self, root: &Path) -> Option<&Path> {
178        match &self.package.readme {
179            Some(value) => match value {
180                ManifestReadmePath::Bool(false) => None,
181                ManifestReadmePath::Bool(true) => Some(Path::new("README.md")),
182                ManifestReadmePath::Path(value) => Some(value),
183            },
184            None => Manifest::default_readme_filename(root),
185        }
186    }
187
188    /// Returns package relative default readme path.
189    pub fn default_readme_filename(root: &Path) -> Option<&'static Path> {
190        const DEFAULT_FILES: [&str; 3] = ["README.md", "README.txt", "README"];
191
192        for &filename in DEFAULT_FILES.iter() {
193            if root.join(filename).is_file() {
194                return Some(Path::new(filename));
195            }
196        }
197
198        None
199    }
200
201    /// Returns `true` if the package's library is documented by default.
202    ///
203    /// See <https://doc.rust-lang.org/cargo/commands/cargo-doc.html> for more details.
204    pub fn is_lib_documented_by_default(&self) -> bool {
205        self.lib.as_ref().and_then(|lib| lib.doc).unwrap_or(true)
206    }
207
208    /// Returns package relative library file path.
209    ///
210    /// See <https://doc.rust-lang.org/cargo/commands/cargo-doc.html> for more details.
211    pub fn relative_lib_path(&self) -> &Path {
212        Path::new(
213            self.lib
214                .as_ref()
215                .and_then(|lib| lib.path.as_deref())
216                .unwrap_or("src/lib.rs"),
217        )
218    }
219
220    /// Returns package relative default binary file path.
221    ///
222    /// See <https://doc.rust-lang.org/cargo/commands/cargo-doc.html> for more details.
223    pub fn default_relative_bin_path(&self) -> &'static Path {
224        Path::new("src/main.rs")
225    }
226
227    /// Returns package relative binary file path by the specified binary target name.
228    ///
229    /// See <https://doc.rust-lang.org/cargo/commands/cargo-doc.html> for more details.
230    pub fn relative_bin_path(&self, name: &str) -> Result<PathBuf, BinPathError> {
231        use std::string::ToString;
232
233        let mut bins = self.bin.iter().flatten().filter(|bin| bin.name == name);
234        match (bins.next(), bins.next()) {
235            (Some(_), Some(_)) => Err(BinPathError::SpecifiedMoreThanOnce(name.to_string())),
236            (Some(bin), None) => Ok(bin.path.as_ref().map_or_else(
237                || PathBuf::from("src/bin").join(Path::new(&bin.name)),
238                PathBuf::from,
239            )),
240            (None, None) => {
241                if name == self.package.name {
242                    Ok(PathBuf::from("src/main.rs"))
243                } else {
244                    Err(BinPathError::NotFound(name.to_string()))
245                }
246            }
247            (None, Some(_)) => unreachable!(),
248        }
249    }
250
251    /// Returns package default library or binary target.
252    ///
253    /// See <https://doc.rust-lang.org/cargo/commands/cargo-doc.html> for more details.
254    pub fn default_relative_target_path(&self) -> &Path {
255        if self.is_lib_documented_by_default() {
256            self.relative_lib_path()
257        } else {
258            self.default_relative_bin_path()
259        }
260    }
261
262    /// Returns package target used for docs.rs builds.
263    ///
264    /// See <https://docs.rs/about/metadata> for more details.
265    pub fn docs_rs_default_target(&self) -> &str {
266        const DEFAULT_TARGET: &str = "x86_64-unknown-linux-gnu";
267
268        if let Some(docs_meta) = &self.docs_meta {
269            if let Some(default_target) = &docs_meta.default_target {
270                return default_target;
271            }
272            if let Some(targets) = &docs_meta.targets {
273                if let Some(first_target) = targets.first() {
274                    return first_target;
275                }
276            }
277        }
278        DEFAULT_TARGET
279    }
280
281    /// Returns a default package features.
282    pub fn default_features(&self) -> HashSet<&str> {
283        use core::ops::Deref;
284
285        if let Some(features) = self.features.as_ref() {
286            if let Some(default_features) = features.get("default") {
287                return default_features.iter().map(Deref::deref).collect();
288            }
289        }
290        HashSet::new()
291    }
292
293    /// Returns all package features.
294    pub fn all_features(&self) -> HashSet<&str> {
295        use core::ops::Deref;
296
297        let mut all_features = HashSet::new();
298        if let Some(features) = self.features.as_ref() {
299            all_features.extend(features.keys().map(Deref::deref));
300        }
301        if let Some(dependencies) = self.dependencies.as_ref() {
302            all_features.extend(dependencies.iter().filter_map(|(key, dep)| match dep {
303                ManifestDependency::Details(ManifestDependencyDetails {
304                    optional: Some(true),
305                }) => Some(key.deref()),
306                _ => None,
307            }));
308        }
309        all_features
310    }
311
312    /// Returns package features used for docs.rs builds.
313    ///
314    /// See <https://docs.rs/about/metadata> for more details.
315    pub fn docs_rs_features(&self) -> HashSet<&str> {
316        use core::ops::Deref;
317
318        let all_features = self
319            .docs_meta
320            .as_ref()
321            .and_then(|docs_meta| docs_meta.all_features)
322            .unwrap_or(false);
323        if all_features {
324            return self.all_features();
325        }
326
327        let no_default_features = self
328            .docs_meta
329            .as_ref()
330            .and_then(|docs_meta| docs_meta.no_default_features)
331            .unwrap_or(false);
332        let features = self
333            .docs_meta
334            .as_ref()
335            .and_then(|docs_meta| docs_meta.features.as_ref())
336            .map(|features| features.iter().map(Deref::deref).collect());
337
338        match (no_default_features, features) {
339            (true, Some(features)) => features,
340            (true, None) => HashSet::new(),
341            (false, Some(features)) => features.union(&self.default_features()).copied().collect(),
342            (false, None) => self.default_features(),
343        }
344    }
345}
346
347/// An error which can occur when parsing manifest from toml file.
348#[derive(Clone, Debug, Eq, Error, PartialEq)]
349pub enum TomlParseError {
350    /// Toml parse error
351    #[error(transparent)]
352    ParseError(#[from] toml::de::Error),
353}
354
355/// An error which can occur when reading manifest from the specified file path.
356#[derive(Debug, Error)]
357pub enum TomlReadError {
358    /// File reading failed.
359    #[error("Failed to read toml at `{path}`: {err}")]
360    IoError {
361        /// File path.
362        path: PathBuf,
363        /// Rust `io::Error`.
364        err: io::Error,
365    },
366    /// File parsing failed.
367    #[error("Failed to parse toml at `{path}`: {err}")]
368    ParseError {
369        /// File path.
370        path: PathBuf,
371        /// The corresponding parse error.
372        err: TomlParseError,
373    },
374}
375
376/// An error which can occur when locating the binary file path by the specified target name.
377#[derive(Clone, Debug, Eq, Error, PartialEq)]
378pub enum BinPathError {
379    /// The binary specified by the target name is not found.
380    #[error("Binary `{0}` not found.")]
381    NotFound(String),
382    /// The binary specified by the target name is specified more than once.
383    #[error("Binary `{0}` specified more than once.")]
384    SpecifiedMoreThanOnce(String),
385}