cargo_manifest/
lib.rs

1#![allow(clippy::large_enum_variant)]
2#![doc = include_str!("../README.md")]
3
4use serde::Deserializer;
5use serde::{Deserialize, Serialize, Serializer};
6use std::collections::BTreeMap;
7use std::fs;
8use std::io;
9use std::path::Path;
10
11pub use toml::Value;
12
13pub type DepsSet = BTreeMap<String, Dependency>;
14pub type TargetDepsSet = BTreeMap<String, Target>;
15pub type FeatureSet = BTreeMap<String, Vec<String>>;
16pub type PatchSet = BTreeMap<String, DepsSet>;
17pub type ToolLintsSet = BTreeMap<String, Lint>;
18pub type LintsSet = BTreeMap<String, ToolLintsSet>;
19
20mod afs;
21mod error;
22pub use crate::afs::*;
23pub use crate::error::Error;
24use serde::de::{Error as _, Unexpected};
25use std::str::FromStr;
26
27/// The top-level `Cargo.toml` structure
28///
29/// The `Metadata` is a type for `[package.metadata]` table. You can replace it with
30/// your own struct type if you use the metadata and don't want to use the catch-all `Value` type.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "kebab-case")]
33pub struct Manifest<PackageMetadata = Value, WorkspaceMetadata = Value> {
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub package: Option<Package<PackageMetadata>>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub cargo_features: Option<Vec<String>>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub workspace: Option<Workspace<WorkspaceMetadata>>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub dependencies: Option<DepsSet>,
42    #[serde(skip_serializing_if = "Option::is_none", alias = "dev_dependencies")]
43    pub dev_dependencies: Option<DepsSet>,
44    #[serde(skip_serializing_if = "Option::is_none", alias = "build_dependencies")]
45    pub build_dependencies: Option<DepsSet>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub target: Option<TargetDepsSet>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub features: Option<FeatureSet>,
50    /// Note that due to autobins feature this is not the complete list
51    /// unless you run `complete_from_path`
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub bin: Vec<Product>,
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub bench: Vec<Product>,
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub test: Vec<Product>,
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub example: Vec<Product>,
60
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub patch: Option<PatchSet>,
63
64    /// Note that due to autolibs feature this is not the complete list
65    /// unless you run `complete_from_path`
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub lib: Option<Product>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub profile: Option<Profiles>,
70
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub badges: Option<Badges>,
73
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub lints: Option<MaybeInheritedLintsSet>,
76}
77
78impl<PackageMetadata, WorkspaceMetadata> Default for Manifest<PackageMetadata, WorkspaceMetadata> {
79    #[allow(deprecated)]
80    fn default() -> Self {
81        Self {
82            package: None,
83            cargo_features: None,
84            workspace: None,
85            dependencies: None,
86            dev_dependencies: None,
87            build_dependencies: None,
88            target: None,
89            features: None,
90            patch: None,
91            lib: None,
92            profile: None,
93            lints: None,
94            badges: None,
95            bin: Default::default(),
96            bench: Default::default(),
97            test: Default::default(),
98            example: Default::default(),
99        }
100    }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
104#[serde(rename_all = "kebab-case")]
105pub struct Workspace<Metadata = Value> {
106    #[serde(default)]
107    pub members: Vec<String>,
108
109    #[serde(skip_serializing_if = "Option::is_none", alias = "default_members")]
110    pub default_members: Option<Vec<String>>,
111
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub exclude: Option<Vec<String>>,
114
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub resolver: Option<Resolver>,
117
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub dependencies: Option<DepsSet>,
120
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub package: Option<WorkspacePackage>,
123
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub metadata: Option<Metadata>,
126
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub lints: Option<LintsSet>,
129}
130
131/// The workspace.package table is where you define keys that can be inherited by members of a
132/// workspace. These keys can be inherited by defining them in the member package with
133/// `{key}.workspace = true`.
134///
135/// See <https://doc.rust-lang.org/nightly/cargo/reference/workspaces.html#the-package-table>
136/// for more details.
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
138#[serde(rename_all = "kebab-case")]
139pub struct WorkspacePackage {
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub edition: Option<Edition>,
142    /// e.g. "1.9.0"
143    pub version: Option<String>,
144    /// e.g. ["Author <e@mail>", "etc"]
145    pub authors: Option<Vec<String>>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    /// A short blurb about the package. This is not rendered in any format when
148    /// uploaded to crates.io (aka this is not markdown).
149    pub description: Option<String>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub homepage: Option<String>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub documentation: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    /// This points to a file under the package root (relative to this `Cargo.toml`).
156    pub readme: Option<StringOrBool>,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub keywords: Option<Vec<String>>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    /// This is a list of up to five categories where this crate would fit.
161    /// e.g. ["command-line-utilities", "development-tools::cargo-plugins"]
162    pub categories: Option<Vec<String>>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    /// e.g. "MIT"
165    pub license: Option<String>,
166    #[serde(rename = "license-file")]
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub license_file: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub publish: Option<Publish>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub exclude: Option<Vec<String>>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub include: Option<Vec<String>>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub repository: Option<String>,
177    /// e.g. "1.63.0"
178    #[serde(rename = "rust-version")]
179    pub rust_version: Option<String>,
180}
181
182fn default_true() -> bool {
183    true
184}
185
186fn is_true(value: &bool) -> bool {
187    *value
188}
189
190fn is_false(value: &bool) -> bool {
191    !value
192}
193
194impl Manifest<Value> {
195    /// Parse contents of a `Cargo.toml` file already loaded as a byte slice.
196    ///
197    /// It does not call `complete_from_path`, so may be missing implicit data.
198    pub fn from_slice(cargo_toml_content: &[u8]) -> Result<Self, Error> {
199        Self::from_slice_with_metadata(cargo_toml_content)
200    }
201
202    /// Parse contents from a `Cargo.toml` file on disk.
203    ///
204    /// Calls `complete_from_path`.
205    pub fn from_path(cargo_toml_path: impl AsRef<Path>) -> Result<Self, Error> {
206        Self::from_path_with_metadata(cargo_toml_path)
207    }
208}
209
210impl FromStr for Manifest<Value> {
211    type Err = Error;
212
213    /// Parse contents of a `Cargo.toml` file loaded as a string
214    ///
215    /// Note: this is **not** a file name, but file's content. See `from_path`.
216    ///
217    /// It does not call `complete_from_path`, so may be missing implicit data.
218    fn from_str(cargo_toml_content: &str) -> Result<Self, Self::Err> {
219        Self::from_slice_with_metadata(cargo_toml_content.as_bytes())
220    }
221}
222
223impl<Metadata: for<'a> Deserialize<'a>> Manifest<Metadata> {
224    /// Parse `Cargo.toml`, and parse its `[package.metadata]` into a custom Serde-compatible type.
225    ///
226    /// It does not call `complete_from_path`, so may be missing implicit data.
227    pub fn from_slice_with_metadata(cargo_toml_content: &[u8]) -> Result<Self, Error> {
228        let mut manifest: Self = toml_from_slice(cargo_toml_content)?;
229        if manifest.package.is_none() && manifest.workspace.is_none() {
230            // Some old crates lack the `[package]` header
231
232            let val: Value = toml_from_slice(cargo_toml_content)?;
233            if let Some(project) = val.get("project") {
234                manifest.package = Some(project.clone().try_into()?);
235            } else {
236                manifest.package = Some(val.try_into()?);
237            }
238        }
239        Ok(manifest)
240    }
241
242    /// Parse contents from `Cargo.toml` file on disk, with custom Serde-compatible metadata type.
243    ///
244    /// Calls `complete_from_path`
245    pub fn from_path_with_metadata(cargo_toml_path: impl AsRef<Path>) -> Result<Self, Error> {
246        let cargo_toml_path = cargo_toml_path.as_ref();
247        let cargo_toml_content = fs::read(cargo_toml_path)?;
248        let mut manifest = Self::from_slice_with_metadata(&cargo_toml_content)?;
249        manifest.complete_from_path(cargo_toml_path)?;
250        Ok(manifest)
251    }
252
253    /// `Cargo.toml` may not contain explicit information about `[lib]`, `[[bin]]` and
254    /// `[package].build`, which are inferred based on files on disk.
255    ///
256    /// This scans the disk to make the data in the manifest as complete as possible.
257    pub fn complete_from_path(&mut self, path: &Path) -> Result<(), Error> {
258        let manifest_dir = path.parent().ok_or_else(|| io::Error::other("bad path"))?;
259        self.complete_from_abstract_filesystem(&Filesystem::new(manifest_dir))
260    }
261
262    /// `Cargo.toml` may not contain explicit information about `[lib]`, `[[bin]]` and
263    /// `[package].build`, which are inferred based on files on disk.
264    ///
265    /// You can provide any implementation of directory scan, which doesn't have to
266    /// be reading straight from disk (might scan a tarball or a git repo, for example).
267    pub fn complete_from_abstract_filesystem<FS: AbstractFilesystem>(
268        &mut self,
269        fs: &FS,
270    ) -> Result<(), Error> {
271        enum ProductType {
272            #[allow(dead_code)]
273            Lib,
274            Bin,
275            Example,
276            Test,
277            Bench,
278        }
279
280        let autobins = self.autobins();
281        let autotests = self.autotests();
282        let autoexamples = self.autoexamples();
283        let autobenches = self.autobenches();
284
285        if let Some(ref mut package) = self.package {
286            let src = match fs.file_names_in("src") {
287                Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Default::default()),
288                result => result,
289            }?;
290
291            let edition = match package.edition {
292                Some(MaybeInherited::Local(edition)) => Some(edition),
293                _ => None,
294            };
295
296            if let Some(ref mut lib) = self.lib {
297                // Use `lib.path` if it's set, otherwise use `src/lib.rs` if it exists, otherwise return an error
298                // (see https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-path-field).
299                if lib.path.is_none() {
300                    let fallback_name = lib.name.as_deref().unwrap_or(&package.name);
301
302                    if src.contains("lib.rs") {
303                        lib.path = Some("src/lib.rs".to_string());
304                    } else if package.uses_legacy_auto_discovery()
305                        && src.contains(format!("{fallback_name}.rs").as_str())
306                    {
307                        lib.path = Some(format!("src/{fallback_name}.rs"));
308                    } else {
309                        let msg =
310                            "can't find library, rename file to `src/lib.rs` or specify lib.path";
311                        return Err(Error::Other(msg.to_string()));
312                    }
313                }
314
315                // Use `lib.name` if it's set, otherwise use `package.name` with dashes replaced by underscores
316                // (see <https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-name-field>).
317                if lib.name.is_none() {
318                    lib.name = Some(package.name.replace('-', "_"));
319                }
320
321                // Use `lib.edition` if it's set, otherwise use `package.edition` unless it's inherited
322                // (see https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-edition-field).
323                if lib.edition.is_none() {
324                    lib.edition = edition;
325                }
326
327                // Use `lib.crate_type` if it's set, otherwise use `["lib"]`
328                // (see https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field).
329                if lib.crate_type.is_none() {
330                    lib.crate_type = Some(vec!["lib".to_string()]);
331                }
332
333                // `lib.required-features` has no effect on `[lib]`
334                // (see https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-required-features-field).
335                lib.required_features.clear();
336            } else if !matches!(package.autolib, Some(false)) && src.contains("lib.rs") {
337                self.lib = Some(Product {
338                    name: Some(package.name.replace('-', "_")),
339                    path: Some("src/lib.rs".to_string()),
340                    edition,
341                    crate_type: Some(vec!["lib".to_string()]),
342                    ..Product::default()
343                })
344            }
345
346            let fill_target_defaults = |targets: &mut Vec<Product>, kind: ProductType| {
347                for target in targets {
348                    if target.edition.is_none() {
349                        target.edition = edition;
350                    }
351
352                    if matches!(kind, ProductType::Example) && target.crate_type.is_none() {
353                        target.crate_type = Some(vec!["bin".to_string()]);
354                    }
355                }
356            };
357
358            let mut discovered_targets = discover_targets(fs, "src/bin")?;
359
360            let has_main_rs = src.contains("main.rs");
361            if has_main_rs {
362                let target = DiscoveredTarget {
363                    name: package.name.clone(),
364                    path: "src/main.rs".to_string(),
365                };
366                discovered_targets.push(target);
367            }
368
369            process_discovered_targets(&mut self.bin, discovered_targets, autobins)?;
370            fill_target_defaults(&mut self.bin, ProductType::Bin);
371
372            // For the 2015 edition, cargo defaults to using `src/main.rs` as
373            // the `path`, if it exists, unless it is explicitly set or there
374            // is a corresponding file in the `src/bin` directory.
375            if package.uses_legacy_auto_discovery() && has_main_rs {
376                for target in self.bin.iter_mut().filter(|t| t.path.is_none()) {
377                    target.path = Some("src/main.rs".to_string());
378                }
379            }
380
381            let discovered_targets = discover_targets(fs, "examples")?;
382            process_discovered_targets(&mut self.example, discovered_targets, autoexamples)?;
383            fill_target_defaults(&mut self.example, ProductType::Example);
384
385            let discovered_targets = discover_targets(fs, "tests")?;
386            process_discovered_targets(&mut self.test, discovered_targets, autotests)?;
387            fill_target_defaults(&mut self.test, ProductType::Test);
388
389            let discovered_targets = discover_targets(fs, "benches")?;
390            process_discovered_targets(&mut self.bench, discovered_targets, autobenches)?;
391            fill_target_defaults(&mut self.bench, ProductType::Bench);
392
393            if matches!(package.build, None | Some(StringOrBool::Bool(true)))
394                && fs.file_names_in(".")?.contains("build.rs")
395            {
396                package.build = Some(StringOrBool::String("build.rs".to_string()));
397            }
398        }
399        Ok(())
400    }
401
402    pub fn autobins(&self) -> bool {
403        let Some(pkg) = &self.package else {
404            return false;
405        };
406
407        let default_value = !pkg.uses_legacy_auto_discovery() || self.bin.is_empty();
408        pkg.autobins.unwrap_or(default_value)
409    }
410
411    pub fn autoexamples(&self) -> bool {
412        let Some(pkg) = &self.package else {
413            return false;
414        };
415
416        let default_value = !pkg.uses_legacy_auto_discovery() || self.example.is_empty();
417        pkg.autoexamples.unwrap_or(default_value)
418    }
419
420    pub fn autotests(&self) -> bool {
421        let Some(pkg) = &self.package else {
422            return false;
423        };
424
425        let default_value = !pkg.uses_legacy_auto_discovery() || self.test.is_empty();
426        pkg.autotests.unwrap_or(default_value)
427    }
428
429    pub fn autobenches(&self) -> bool {
430        let Some(pkg) = &self.package else {
431            return false;
432        };
433
434        let default_value = !pkg.uses_legacy_auto_discovery() || self.bench.is_empty();
435        pkg.autobenches.unwrap_or(default_value)
436    }
437}
438
439#[derive(Debug)]
440struct DiscoveredTarget {
441    name: String,
442    path: String,
443}
444
445/// Fills in missing [Product::path] fields from the auto-discovered targets
446/// and optionally adds the additional auto-discovered targets to the list
447/// of targets.
448fn process_discovered_targets(
449    targets: &mut Vec<Product>,
450    discovered_targets: Vec<DiscoveredTarget>,
451    add_discovered_targets: bool,
452) -> Result<(), Error> {
453    for target in targets.iter_mut() {
454        // `name` is always required, if it's missing we skip the target
455        // (see https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-name-field).
456        let Some(ref name) = target.name else {
457            continue;
458        };
459
460        // Use `path` if it's set, otherwise try to find a matching auto-discovered target
461        // (see https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-path-field).
462        if target.path.is_none() {
463            let discovered_target = discovered_targets.iter().find(|t| t.name == *name);
464            if let Some(discovered_target) = discovered_target {
465                target.path = Some(discovered_target.path.clone());
466            }
467
468            // If no matching auto-discovered target was found the
469            // `path` field is kept as `None` to let the user decide
470            // how to handle the situation.
471            //
472            // `cargo`, for example, will show an error if the `path`
473            // field is not set and no auto-discovered target was found.
474        }
475    }
476
477    if add_discovered_targets {
478        for discovered_target in discovered_targets {
479            if targets
480                .iter()
481                .any(|b| b.path.as_deref() == Some(&discovered_target.path))
482            {
483                continue;
484            }
485
486            targets.push(Product {
487                name: Some(discovered_target.name),
488                path: Some(discovered_target.path),
489                edition: None,
490                ..Product::default()
491            });
492        }
493    }
494
495    Ok(())
496}
497
498/// Discover targets in a specific directory
499/// (see <https://doc.rust-lang.org/cargo/guide/project-layout.html>).
500///
501/// This function can be used to discover e.g. binary targets in the `src/bin`
502/// directory, or tests in the `tests` directory.
503///
504/// It will look for files matching `{path}/{name}.rs` and
505/// `{path}/{name}/main.rs`, and return a list of name/path pairs.
506fn discover_targets<FS: AbstractFilesystem>(
507    fs: &FS,
508    path: &str,
509) -> Result<Vec<DiscoveredTarget>, Error> {
510    let Ok(file_names) = fs.file_names_in(path) else {
511        // Ideally we'd use proper error handling here, but since
512        // `std::io::ErrorKind::NotADirectory` is not stable yet, we can't
513        // match on the error kind and handle that case correctly.
514        return Ok(Default::default());
515    };
516
517    // Sort the file names to ensure a consistent order.
518    let mut file_names = file_names.into_iter().collect::<Vec<_>>();
519    file_names.sort_unstable();
520
521    let mut out = Vec::new();
522    for file_name in file_names {
523        let rel_path = format!("{}/{}", path, file_name);
524
525        if let Some(name) = file_name.strip_suffix(".rs") {
526            out.push(DiscoveredTarget {
527                name: name.into(),
528                path: rel_path.clone(),
529            });
530        }
531
532        let Ok(subfolder_file_names) = fs.file_names_in(&rel_path) else {
533            continue;
534        };
535
536        if subfolder_file_names.contains("main.rs") {
537            out.push(DiscoveredTarget {
538                name: file_name.into(),
539                path: rel_path + "/main.rs",
540            });
541        }
542    }
543
544    Ok(out)
545}
546
547#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
548pub struct Profiles {
549    pub release: Option<Profile>,
550    pub dev: Option<Profile>,
551    pub test: Option<Profile>,
552    pub bench: Option<Profile>,
553    pub doc: Option<Profile>,
554
555    #[serde(flatten)]
556    pub custom: BTreeMap<String, Profile>,
557}
558
559#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
560#[serde(try_from = "toml::Value")]
561pub enum StripSetting {
562    /// false
563    None,
564    Debuginfo,
565    /// true
566    Symbols,
567}
568
569impl Serialize for StripSetting {
570    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
571        match self {
572            Self::None => serializer.serialize_bool(false),
573            Self::Debuginfo => serializer.serialize_str("debuginfo"),
574            Self::Symbols => serializer.serialize_bool(true),
575        }
576    }
577}
578
579impl TryFrom<Value> for StripSetting {
580    type Error = Error;
581
582    fn try_from(v: Value) -> Result<Self, Error> {
583        Ok(match v {
584            Value::Boolean(b) => {
585                if b {
586                    Self::Symbols
587                } else {
588                    Self::None
589                }
590            }
591            Value::String(s) => match s.as_str() {
592                "none" => Self::None,
593                "debuginfo" => Self::Debuginfo,
594                "symbols" => Self::Symbols,
595                other => {
596                    return Err(Error::Other(format!(
597                        "'{other}' is not a valid value for 'strip'"
598                    )))
599                }
600            },
601            _ => {
602                return Err(Error::Other(
603                    "wrong data type for strip setting".to_string(),
604                ))
605            }
606        })
607    }
608}
609
610#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611#[serde(rename_all = "kebab-case")]
612pub struct Profile {
613    #[serde(alias = "opt_level")]
614    pub opt_level: Option<Value>,
615    pub debug: Option<Value>,
616    pub rpath: Option<bool>,
617    pub inherits: Option<String>,
618    pub lto: Option<Value>,
619    #[serde(alias = "debug_assertions")]
620    pub debug_assertions: Option<bool>,
621    #[serde(alias = "codegen_units")]
622    pub codegen_units: Option<u16>,
623    pub panic: Option<String>,
624    pub incremental: Option<bool>,
625    #[serde(alias = "overflow_checks")]
626    pub overflow_checks: Option<bool>,
627    pub strip: Option<StripSetting>,
628    #[serde(default)]
629    pub package: BTreeMap<String, Value>,
630    pub split_debuginfo: Option<String>,
631    /// profile overrides
632    pub build_override: Option<Value>,
633}
634
635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(rename_all = "kebab-case")]
637/// Cargo uses the term "target" for both "target platform" and "build target" (the thing to build),
638/// which makes it ambigous.
639/// Here Cargo's bin/lib **target** is renamed to **product**.
640pub struct Product {
641    /// This field points at where the crate is located, relative to the `Cargo.toml`.
642    pub path: Option<String>,
643
644    /// The name of a product is the name of the library or binary that will be generated.
645    /// This is defaulted to the name of the package, with any dashes replaced
646    /// with underscores. (Rust `extern crate` declarations reference this name;
647    /// therefore the value must be a valid Rust identifier to be usable.)
648    pub name: Option<String>,
649
650    /// A flag for enabling unit tests for this product. This is used by `cargo test`.
651    #[serde(default = "default_true", skip_serializing_if = "is_true")]
652    pub test: bool,
653
654    /// A flag for enabling documentation tests for this product. This is only relevant
655    /// for libraries, it has no effect on other sections. This is used by
656    /// `cargo test`.
657    #[serde(default = "default_true", skip_serializing_if = "is_true")]
658    pub doctest: bool,
659
660    /// A flag for enabling benchmarks for this product. This is used by `cargo bench`.
661    #[serde(default = "default_true", skip_serializing_if = "is_true")]
662    pub bench: bool,
663
664    /// A flag for enabling documentation of this product. This is used by `cargo doc`.
665    #[serde(default = "default_true", skip_serializing_if = "is_true")]
666    pub doc: bool,
667
668    /// If the product is meant to be a compiler plugin, this field must be set to true
669    /// for Cargo to correctly compile it and make it available for all dependencies.
670    #[serde(default, skip_serializing_if = "is_false")]
671    pub plugin: bool,
672
673    /// If the product is meant to be a "macros 1.1" procedural macro, this field must
674    /// be set to true.
675    #[serde(default, alias = "proc_macro", skip_serializing_if = "is_false")]
676    pub proc_macro: bool,
677
678    /// If set to false, `cargo test` will omit the `--test` flag to rustc, which
679    /// stops it from generating a test harness. This is useful when the binary being
680    /// built manages the test runner itself.
681    #[serde(default = "default_true", skip_serializing_if = "is_true")]
682    pub harness: bool,
683
684    /// If set then a product can be configured to use a different edition than the
685    /// `[package]` is configured to use, perhaps only compiling a library with the
686    /// 2018 edition or only compiling one unit test with the 2015 edition. By default
687    /// all products are compiled with the edition specified in `[package]`.
688    #[serde(default)]
689    pub edition: Option<Edition>,
690
691    /// The required-features field specifies which features the product needs in order to be built.
692    /// If any of the required features are not selected, the product will be skipped.
693    /// This is only relevant for the `[[bin]]`, `[[bench]]`, `[[test]]`, and `[[example]]` sections,
694    /// it has no effect on `[lib]`.
695    #[serde(default, alias = "required_features")]
696    pub required_features: Vec<String>,
697
698    /// The available options are "dylib", "rlib", "staticlib", "cdylib", and "proc-macro".
699    #[serde(skip_serializing_if = "Option::is_none", alias = "crate_type")]
700    pub crate_type: Option<Vec<String>>,
701}
702
703impl Default for Product {
704    fn default() -> Self {
705        Self {
706            path: None,
707            name: None,
708            test: true,
709            doctest: true,
710            bench: true,
711            doc: true,
712            harness: true,
713            plugin: false,
714            proc_macro: false,
715            required_features: Vec::new(),
716            crate_type: None,
717            edition: Some(Edition::default()),
718        }
719    }
720}
721
722#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723#[serde(rename_all = "kebab-case")]
724pub struct Target {
725    #[serde(default)]
726    pub dependencies: DepsSet,
727    #[serde(default, alias = "dev_dependencies")]
728    pub dev_dependencies: DepsSet,
729    #[serde(default, alias = "build_dependencies")]
730    pub build_dependencies: DepsSet,
731}
732
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734#[serde(untagged)]
735pub enum Dependency {
736    Simple(String),
737    Inherited(InheritedDependencyDetail), // order is important for serde
738    Detailed(DependencyDetail),
739}
740
741impl Dependency {
742    pub fn detail(&self) -> Option<&DependencyDetail> {
743        match *self {
744            Dependency::Detailed(ref d) => Some(d),
745            _ => None,
746        }
747    }
748
749    /// Simplifies `Dependency::Detailed` to `Dependency::Simple` if only the
750    /// `version` field inside the `DependencyDetail` struct is set.
751    pub fn simplify(self) -> Self {
752        match self {
753            Dependency::Detailed(details) => details.simplify(),
754            dep => dep,
755        }
756    }
757
758    pub fn req(&self) -> &str {
759        match *self {
760            Dependency::Simple(ref v) => v,
761            Dependency::Detailed(ref d) => d.version.as_deref().unwrap_or("*"),
762            Dependency::Inherited(_) => "*",
763        }
764    }
765
766    pub fn req_features(&self) -> &[String] {
767        match *self {
768            Dependency::Simple(_) => &[],
769            Dependency::Detailed(ref d) => d.features.as_deref().unwrap_or(&[]),
770            Dependency::Inherited(ref i) => i.features.as_deref().unwrap_or(&[]),
771        }
772    }
773
774    pub fn optional(&self) -> bool {
775        match *self {
776            Dependency::Simple(_) => false,
777            Dependency::Detailed(ref d) => d.optional.unwrap_or(false),
778            Dependency::Inherited(ref i) => i.optional.unwrap_or(false),
779        }
780    }
781
782    // `Some` if it overrides the package name.
783    // If `None`, use the dependency name as the package name.
784    pub fn package(&self) -> Option<&str> {
785        match *self {
786            Dependency::Detailed(ref d) => d.package.as_deref(),
787            _ => None,
788        }
789    }
790
791    // Git URL of this dependency, if any
792    pub fn git(&self) -> Option<&str> {
793        self.detail().and_then(|d| d.git.as_deref())
794    }
795
796    // `true` if it's an usual crates.io dependency,
797    // `false` if git/path/alternative registry
798    pub fn is_crates_io(&self) -> bool {
799        match *self {
800            Dependency::Simple(_) => true,
801            Dependency::Detailed(ref d) => {
802                // TODO: allow registry to be set to crates.io explicitly?
803                d.path.is_none()
804                    && d.registry.is_none()
805                    && d.registry_index.is_none()
806                    && d.git.is_none()
807                    && d.tag.is_none()
808                    && d.branch.is_none()
809                    && d.rev.is_none()
810            }
811            Dependency::Inherited(_) => false,
812        }
813    }
814}
815
816#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
817#[serde(rename_all = "kebab-case")]
818pub struct DependencyDetail {
819    #[serde(skip_serializing_if = "Option::is_none")]
820    pub version: Option<String>,
821    #[serde(skip_serializing_if = "Option::is_none")]
822    pub registry: Option<String>,
823    #[serde(alias = "registry_index")]
824    pub registry_index: Option<String>,
825    #[serde(skip_serializing_if = "Option::is_none")]
826    pub path: Option<String>,
827    #[serde(skip_serializing_if = "Option::is_none")]
828    pub git: Option<String>,
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub branch: Option<String>,
831    #[serde(skip_serializing_if = "Option::is_none")]
832    pub tag: Option<String>,
833    #[serde(skip_serializing_if = "Option::is_none")]
834    pub rev: Option<String>,
835    #[serde(default)]
836    #[serde(skip_serializing_if = "Option::is_none")]
837    pub features: Option<Vec<String>>,
838    #[serde(default)]
839    #[serde(skip_serializing_if = "Option::is_none")]
840    pub optional: Option<bool>,
841    #[serde(default, alias = "default_features")]
842    #[serde(skip_serializing_if = "Option::is_none")]
843    pub default_features: Option<bool>,
844    #[serde(skip_serializing_if = "Option::is_none")]
845    pub package: Option<String>,
846}
847
848impl DependencyDetail {
849    fn simplify(self) -> Dependency {
850        let Self {
851            version: _,
852            registry,
853            registry_index,
854            path,
855            git,
856            branch,
857            tag,
858            rev,
859            features,
860            optional,
861            default_features,
862            package,
863        } = &self;
864
865        if registry.is_some()
866            || registry_index.is_some()
867            || path.is_some()
868            || git.is_some()
869            || branch.is_some()
870            || tag.is_some()
871            || rev.is_some()
872            || features.is_some()
873            || optional.is_some()
874            || default_features.is_some()
875            || package.is_some()
876        {
877            return Dependency::Detailed(self);
878        }
879
880        match self.version {
881            None => Dependency::Detailed(self),
882            Some(version) => Dependency::Simple(version),
883        }
884    }
885}
886
887/// When a dependency is defined as `{ workspace = true }`,
888/// and workspace data hasn't been applied yet.
889#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
890#[serde(rename_all = "kebab-case")]
891pub struct InheritedDependencyDetail {
892    pub workspace: True,
893
894    #[serde(default, skip_serializing_if = "Option::is_none")]
895    pub features: Option<Vec<String>>,
896
897    #[serde(default, skip_serializing_if = "Option::is_none")]
898    pub optional: Option<bool>,
899}
900
901/// Used as a wrapper for properties that may be inherited by workspace-level settings.
902/// It currently does not support more complex interactions (e.g. specifying part of the property
903/// in the local manifest while inheriting another part of it from the workspace manifest, as it
904/// happens for dependency features).
905///
906/// See [`cargo`'s documentation](https://doc.rust-lang.org/nightly/cargo/reference/workspaces.html#workspaces)
907/// for more details.
908#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq)]
909#[serde(untagged)]
910pub enum MaybeInherited<T> {
911    Inherited { workspace: True },
912    Local(T),
913}
914
915impl<T> MaybeInherited<T> {
916    pub fn inherited() -> Self {
917        Self::Inherited { workspace: True }
918    }
919
920    pub fn as_local(self) -> Option<T> {
921        match self {
922            Self::Local(x) => Some(x),
923            Self::Inherited { .. } => None,
924        }
925    }
926
927    pub const fn as_ref(&self) -> MaybeInherited<&T> {
928        match self {
929            Self::Local(ref x) => MaybeInherited::Local(x),
930            Self::Inherited { .. } => MaybeInherited::Inherited { workspace: True },
931        }
932    }
933}
934
935/// A type-level representation of a `true` boolean value.
936#[derive(Debug, Default, Clone, PartialEq, Eq)]
937#[doc(hidden)]
938pub struct True;
939
940impl Serialize for True {
941    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
942    where
943        S: Serializer,
944    {
945        serializer.serialize_bool(true)
946    }
947}
948
949impl<'de> Deserialize<'de> for True {
950    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
951    where
952        D: Deserializer<'de>,
953    {
954        if bool::deserialize(deserializer)? {
955            Ok(Self)
956        } else {
957            Err(D::Error::invalid_value(
958                Unexpected::Bool(false),
959                &"a `true` boolean value",
960            ))
961        }
962    }
963}
964
965/// You can replace `Metadata` type with your own
966/// to parse into something more useful than a generic toml `Value`
967#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
968pub struct Package<Metadata = Value> {
969    /// Careful: some names are uppercase
970    pub name: String,
971    #[serde(skip_serializing_if = "Option::is_none")]
972    pub edition: Option<MaybeInherited<Edition>>,
973    /// The version of the package (e.g. "1.9.0").
974    ///
975    /// Use [Package::version()] to get the effective value, with the default
976    /// value of "0.0.0" applied.
977    #[serde(skip_serializing_if = "Option::is_none")]
978    pub version: Option<MaybeInherited<String>>,
979    #[serde(skip_serializing_if = "Option::is_none")]
980    pub build: Option<StringOrBool>,
981    #[serde(skip_serializing_if = "Option::is_none")]
982    pub workspace: Option<String>,
983    #[serde(skip_serializing_if = "Option::is_none")]
984    /// e.g. ["Author <e@mail>", "etc"]
985    pub authors: Option<MaybeInherited<Vec<String>>>,
986    #[serde(skip_serializing_if = "Option::is_none")]
987    pub links: Option<String>,
988    #[serde(skip_serializing_if = "Option::is_none")]
989    /// A short blurb about the package. This is not rendered in any format when
990    /// uploaded to crates.io (aka this is not markdown).
991    pub description: Option<MaybeInherited<String>>,
992    #[serde(skip_serializing_if = "Option::is_none")]
993    pub homepage: Option<MaybeInherited<String>>,
994    #[serde(skip_serializing_if = "Option::is_none")]
995    pub documentation: Option<MaybeInherited<String>>,
996    #[serde(skip_serializing_if = "Option::is_none")]
997    /// This points to a file under the package root (relative to this `Cargo.toml`).
998    pub readme: Option<MaybeInherited<StringOrBool>>,
999    #[serde(skip_serializing_if = "Option::is_none")]
1000    pub keywords: Option<MaybeInherited<Vec<String>>>,
1001    #[serde(skip_serializing_if = "Option::is_none")]
1002    /// This is a list of up to five categories where this crate would fit.
1003    /// e.g. ["command-line-utilities", "development-tools::cargo-plugins"]
1004    pub categories: Option<MaybeInherited<Vec<String>>>,
1005    #[serde(skip_serializing_if = "Option::is_none")]
1006    /// e.g. "MIT"
1007    pub license: Option<MaybeInherited<String>>,
1008    #[serde(rename = "license-file")]
1009    #[serde(skip_serializing_if = "Option::is_none")]
1010    pub license_file: Option<MaybeInherited<String>>,
1011    #[serde(skip_serializing_if = "Option::is_none")]
1012    pub repository: Option<MaybeInherited<String>>,
1013    #[serde(skip_serializing_if = "Option::is_none")]
1014    pub metadata: Option<Metadata>,
1015    /// e.g. "1.63.0"
1016    #[serde(rename = "rust-version")]
1017    pub rust_version: Option<MaybeInherited<String>>,
1018    #[serde(skip_serializing_if = "Option::is_none")]
1019    pub exclude: Option<MaybeInherited<Vec<String>>>,
1020    #[serde(skip_serializing_if = "Option::is_none")]
1021    pub include: Option<MaybeInherited<Vec<String>>>,
1022
1023    #[serde(rename = "default-run")]
1024    #[serde(skip_serializing_if = "Option::is_none")]
1025    /// The default binary to run by cargo run.
1026    pub default_run: Option<String>,
1027
1028    /// Disables library auto discovery.
1029    #[serde(skip_serializing_if = "Option::is_none")]
1030    pub autolib: Option<bool>,
1031    /// Disables binary auto discovery.
1032    ///
1033    /// Use [Manifest::autobins()] to get the effective value.
1034    #[serde(skip_serializing_if = "Option::is_none")]
1035    pub autobins: Option<bool>,
1036    /// Disables example auto discovery.
1037    ///
1038    /// Use [Manifest::autoexamples()] to get the effective value.
1039    #[serde(skip_serializing_if = "Option::is_none")]
1040    pub autoexamples: Option<bool>,
1041    /// Disables test auto discovery.
1042    ///
1043    /// Use [Manifest::autotests()] to get the effective value.
1044    #[serde(skip_serializing_if = "Option::is_none")]
1045    pub autotests: Option<bool>,
1046    /// Disables bench auto discovery.
1047    ///
1048    /// Use [Manifest::autobenches()] to get the effective value.
1049    #[serde(skip_serializing_if = "Option::is_none")]
1050    pub autobenches: Option<bool>,
1051    #[serde(skip_serializing_if = "Option::is_none")]
1052    pub publish: Option<MaybeInherited<Publish>>,
1053    #[serde(skip_serializing_if = "Option::is_none")]
1054    pub resolver: Option<Resolver>,
1055}
1056
1057impl<Metadata> Package<Metadata> {
1058    pub fn new(name: String, version: String) -> Self {
1059        Self {
1060            name,
1061            edition: None,
1062            version: Some(MaybeInherited::Local(version)),
1063            build: None,
1064            workspace: None,
1065            authors: None,
1066            links: None,
1067            description: None,
1068            homepage: None,
1069            documentation: None,
1070            readme: None,
1071            keywords: None,
1072            categories: None,
1073            license: None,
1074            license_file: None,
1075            repository: None,
1076            metadata: None,
1077            rust_version: None,
1078            exclude: None,
1079            include: None,
1080            default_run: None,
1081            autolib: None,
1082            autobins: None,
1083            autoexamples: None,
1084            autotests: None,
1085            autobenches: None,
1086            publish: None,
1087            resolver: None,
1088        }
1089    }
1090
1091    /// Returns the effective version of the package.
1092    ///
1093    /// If the version is not set, it defaults to "0.0.0"
1094    /// (see <https://doc.rust-lang.org/cargo/reference/manifest.html#the-version-field>).
1095    pub fn version(&self) -> MaybeInherited<&str> {
1096        self.version
1097            .as_ref()
1098            .map(|v| match v {
1099                MaybeInherited::Local(v) => MaybeInherited::Local(v.as_str()),
1100                MaybeInherited::Inherited { .. } => MaybeInherited::Inherited { workspace: True },
1101            })
1102            .unwrap_or_else(|| MaybeInherited::Local("0.0.0"))
1103    }
1104
1105    /// Returns whether to use the legacy behavior for target auto-discovery
1106    /// from the 2015 Rust edition.
1107    ///
1108    /// The default value for target auto-discovery changed in the 2018 edition
1109    /// (see https://doc.rust-lang.org/cargo/reference/cargo-targets.html#target-auto-discovery).
1110    ///
1111    /// - If the edition is not set or is set to 2015, we use the legacy behavior.
1112    /// - If the edition is set to 2018 or later, we use the new behavior.
1113    /// - If the edition is inherited, we assume that the edition is 2018
1114    ///   or later, since inheritance is a newer feature.
1115    fn uses_legacy_auto_discovery(&self) -> bool {
1116        matches!(
1117            self.edition,
1118            None | Some(MaybeInherited::Local(Edition::E2015))
1119        )
1120    }
1121}
1122
1123#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
1124#[serde(untagged)]
1125pub enum StringOrBool {
1126    String(String),
1127    Bool(bool),
1128}
1129
1130#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1131#[serde(untagged)]
1132pub enum Publish {
1133    Flag(bool),
1134    Registry(Vec<String>),
1135}
1136
1137impl Default for Publish {
1138    fn default() -> Self {
1139        Publish::Flag(true)
1140    }
1141}
1142
1143impl PartialEq<Publish> for bool {
1144    fn eq(&self, p: &Publish) -> bool {
1145        match *p {
1146            Publish::Flag(flag) => flag == *self,
1147            Publish::Registry(ref reg) => reg.is_empty() != *self,
1148        }
1149    }
1150}
1151
1152impl PartialEq<bool> for Publish {
1153    fn eq(&self, b: &bool) -> bool {
1154        b.eq(self)
1155    }
1156}
1157
1158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1159#[serde(rename_all = "kebab-case")]
1160pub struct Badge {
1161    pub repository: String,
1162    #[serde(default = "default_master")]
1163    pub branch: String,
1164    pub service: Option<String>,
1165    pub id: Option<String>,
1166    #[serde(alias = "project_name")]
1167    pub project_name: Option<String>,
1168}
1169
1170fn default_master() -> String {
1171    "master".to_string()
1172}
1173
1174#[allow(clippy::unnecessary_wraps)]
1175fn ok_or_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
1176where
1177    T: Deserialize<'de> + Default,
1178    D: Deserializer<'de>,
1179{
1180    Ok(Deserialize::deserialize(deserializer).unwrap_or_default())
1181}
1182
1183fn toml_from_slice<T>(s: &'_ [u8]) -> Result<T, Error>
1184where
1185    T: serde::de::DeserializeOwned,
1186{
1187    Ok(toml::from_str(std::str::from_utf8(s)?)?)
1188}
1189
1190#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1191#[serde(rename_all = "kebab-case")]
1192pub struct Badges {
1193    /// Appveyor: `repository` is required. `branch` is optional; default is `master`
1194    /// `service` is optional; valid values are `github` (default), `bitbucket`, and
1195    /// `gitlab`; `id` is optional; you can specify the appveyor project id if you
1196    /// want to use that instead. `project_name` is optional; use when the repository
1197    /// name differs from the appveyor project name.
1198    #[serde(default, deserialize_with = "ok_or_default")]
1199    pub appveyor: Option<Badge>,
1200
1201    /// Circle CI: `repository` is required. `branch` is optional; default is `master`
1202    #[serde(default, deserialize_with = "ok_or_default")]
1203    pub circle_ci: Option<Badge>,
1204
1205    /// GitLab: `repository` is required. `branch` is optional; default is `master`
1206    #[serde(default, deserialize_with = "ok_or_default")]
1207    pub gitlab: Option<Badge>,
1208
1209    /// Travis CI: `repository` in format "\<user>/\<project>" is required.
1210    /// `branch` is optional; default is `master`
1211    #[serde(default, deserialize_with = "ok_or_default")]
1212    pub travis_ci: Option<Badge>,
1213
1214    /// Codecov: `repository` is required. `branch` is optional; default is `master`
1215    /// `service` is optional; valid values are `github` (default), `bitbucket`, and
1216    /// `gitlab`.
1217    #[serde(default, deserialize_with = "ok_or_default")]
1218    pub codecov: Option<Badge>,
1219
1220    /// Coveralls: `repository` is required. `branch` is optional; default is `master`
1221    /// `service` is optional; valid values are `github` (default) and `bitbucket`.
1222    #[serde(default, deserialize_with = "ok_or_default")]
1223    pub coveralls: Option<Badge>,
1224
1225    /// Is it maintained resolution time: `repository` is required.
1226    #[serde(default, deserialize_with = "ok_or_default")]
1227    pub is_it_maintained_issue_resolution: Option<Badge>,
1228
1229    /// Is it maintained percentage of open issues: `repository` is required.
1230    #[serde(default, deserialize_with = "ok_or_default")]
1231    pub is_it_maintained_open_issues: Option<Badge>,
1232
1233    /// Maintenance: `status` is required. Available options are `actively-developed`,
1234    /// `passively-maintained`, `as-is`, `experimental`, `looking-for-maintainer`,
1235    /// `deprecated`, and the default `none`, which displays no badge on crates.io.
1236    #[serde(default, deserialize_with = "ok_or_default")]
1237    pub maintenance: Maintenance,
1238}
1239
1240#[derive(Debug, PartialEq, Eq, Copy, Clone, Default, Serialize, Deserialize)]
1241pub struct Maintenance {
1242    pub status: MaintenanceStatus,
1243}
1244
1245#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, Serialize, Deserialize)]
1246#[serde(rename_all = "kebab-case")]
1247#[derive(Default)]
1248pub enum MaintenanceStatus {
1249    #[default]
1250    None,
1251    ActivelyDeveloped,
1252    PassivelyMaintained,
1253    AsIs,
1254    Experimental,
1255    LookingForMaintainer,
1256    Deprecated,
1257}
1258
1259#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, Serialize, Deserialize, Default)]
1260pub enum Edition {
1261    #[serde(rename = "2015")]
1262    #[default]
1263    E2015,
1264    #[serde(rename = "2018")]
1265    E2018,
1266    #[serde(rename = "2021")]
1267    E2021,
1268    #[serde(rename = "2024")]
1269    E2024,
1270}
1271
1272impl Edition {
1273    pub fn as_str(&self) -> &'static str {
1274        match self {
1275            Self::E2015 => "2015",
1276            Self::E2018 => "2018",
1277            Self::E2021 => "2021",
1278            Self::E2024 => "2024",
1279        }
1280    }
1281}
1282
1283#[derive(Debug, PartialEq, Eq, Default, Copy, Clone, Hash, Serialize, Deserialize)]
1284pub enum Resolver {
1285    #[serde(rename = "1")]
1286    #[default]
1287    V1,
1288    #[serde(rename = "2")]
1289    V2,
1290    #[serde(rename = "3")]
1291    V3,
1292}
1293
1294#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, Serialize, Deserialize)]
1295#[serde(rename_all = "kebab-case")]
1296pub enum LintLevel {
1297    Forbid,
1298    Deny,
1299    Warn,
1300    Allow,
1301}
1302
1303impl LintLevel {
1304    pub fn as_str(&self) -> &'static str {
1305        match self {
1306            Self::Forbid => "forbid",
1307            Self::Deny => "deny",
1308            Self::Warn => "warn",
1309            Self::Allow => "allow",
1310        }
1311    }
1312}
1313
1314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1315#[serde(rename_all = "kebab-case")]
1316pub struct LintConfig {
1317    pub level: LintLevel,
1318    #[serde(default)]
1319    pub priority: i8,
1320}
1321
1322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1323#[serde(untagged)]
1324pub enum Lint {
1325    Level(LintLevel),
1326    Config(LintConfig),
1327}
1328
1329impl Lint {
1330    pub fn level(&self) -> LintLevel {
1331        match self {
1332            Self::Level(level) => *level,
1333            Self::Config(config) => config.level,
1334        }
1335    }
1336
1337    pub fn priority(&self) -> i8 {
1338        match self {
1339            Self::Level(_) => 0,
1340            Self::Config(config) => config.priority,
1341        }
1342    }
1343}
1344
1345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1346pub struct MaybeInheritedLintsSet {
1347    #[serde(skip_serializing_if = "Option::is_none")]
1348    pub workspace: Option<True>,
1349    #[serde(flatten)]
1350    pub lints: LintsSet,
1351}
1352
1353impl MaybeInheritedLintsSet {
1354    pub fn is_inherited(&self) -> bool {
1355        matches!(&self.workspace, &Some(True))
1356    }
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361    use super::*;
1362
1363    #[test]
1364    fn test_auto_discovery_defaults() {
1365        let mut manifest = Manifest {
1366            package: Some(Package::<()>::new("foo".into(), "1.0.0".into())),
1367            ..Default::default()
1368        };
1369        assert!(manifest.autobins());
1370        assert!(manifest.autotests());
1371        assert!(manifest.autoexamples());
1372        assert!(manifest.autobenches());
1373
1374        manifest.bin = vec![Product::default()];
1375        assert!(!manifest.autobins());
1376        assert!(manifest.autotests());
1377        assert!(manifest.autoexamples());
1378        assert!(manifest.autobenches());
1379
1380        manifest.package.as_mut().unwrap().autotests = Some(false);
1381        assert!(!manifest.autobins());
1382        assert!(!manifest.autotests());
1383        assert!(manifest.autoexamples());
1384        assert!(manifest.autobenches());
1385
1386        manifest.package.as_mut().unwrap().autobins = Some(true);
1387        assert!(manifest.autobins());
1388        assert!(!manifest.autotests());
1389        assert!(manifest.autoexamples());
1390        assert!(manifest.autobenches());
1391
1392        manifest.package.as_mut().unwrap().autobins = None;
1393        manifest.package.as_mut().unwrap().edition = Some(MaybeInherited::Local(Edition::E2018));
1394        assert!(manifest.autobins());
1395        assert!(!manifest.autotests());
1396        assert!(manifest.autoexamples());
1397        assert!(manifest.autobenches());
1398    }
1399
1400    #[test]
1401    fn test_legacy_auto_discovery_flag() {
1402        let mut package = Package::<()>::new("foo".into(), "1.0.0".into());
1403        assert!(package.uses_legacy_auto_discovery());
1404
1405        package.edition = Some(MaybeInherited::Local(Edition::E2015));
1406        assert!(package.uses_legacy_auto_discovery());
1407
1408        package.edition = Some(MaybeInherited::Local(Edition::E2018));
1409        assert!(!package.uses_legacy_auto_discovery());
1410
1411        package.edition = Some(MaybeInherited::Local(Edition::E2021));
1412        assert!(!package.uses_legacy_auto_discovery());
1413
1414        package.edition = Some(MaybeInherited::Inherited { workspace: True });
1415        assert!(!package.uses_legacy_auto_discovery());
1416    }
1417}