Skip to main content

asimov_module_kit/module/
cargo_toml.rs

1// This is free and unencumbered software released into the public domain.
2
3//! Targeted `Cargo.toml` edits that preserve existing formatting and comments.
4
5use alloc::string::String;
6use thiserror::Error;
7use toml_edit::{Array, DocumentMut, Item, Table, value};
8
9#[derive(Debug, Error)]
10pub enum CargoTomlError {
11    #[error("failed to parse Cargo.toml: {0}")]
12    Parse(#[from] toml_edit::TomlError),
13
14    #[error("`[bin]` in Cargo.toml is not an array of tables")]
15    BinNotArrayOfTables,
16}
17
18/// Inserts a new `[[bin]]` entry, preserving all other formatting/comments.
19pub fn insert_bin(
20    doc: &mut DocumentMut,
21    name: &str,
22    path: &str,
23    required_features: &[String],
24) -> Result<(), CargoTomlError> {
25    let mut bin = Table::new();
26    bin.insert("name", value(name));
27    bin.insert("path", value(path));
28    if !required_features.is_empty() {
29        let features: Array = required_features.iter().map(String::as_str).collect();
30        bin.insert("required-features", value(features));
31    }
32
33    doc.entry("bin")
34        .or_insert(Item::ArrayOfTables(Default::default()))
35        .as_array_of_tables_mut()
36        .ok_or(CargoTomlError::BinNotArrayOfTables)?
37        .push(bin);
38
39    Ok(())
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use alloc::string::ToString;
46
47    #[test]
48    fn inserts_into_existing_bin_array() {
49        let toml = r#"# comment kept as-is
50[package]
51name = "asimov-widget-module"
52
53[[bin]]
54name = "asimov-widget-emitter"
55path = "src/emitter/main.rs"
56required-features = ["cli"]
57"#;
58        let mut doc: DocumentMut = toml.parse().unwrap();
59        insert_bin(
60            &mut doc,
61            "asimov-widget-fetcher",
62            "src/fetcher/main.rs",
63            &[String::from("cli")],
64        )
65        .unwrap();
66
67        let rendered = doc.to_string();
68        assert!(rendered.contains("# comment kept as-is"));
69        assert!(rendered.contains("name = \"asimov-widget-emitter\""));
70        assert!(rendered.contains("name = \"asimov-widget-fetcher\""));
71        assert!(rendered.contains("path = \"src/fetcher/main.rs\""));
72
73        let bins = doc["bin"].as_array_of_tables().unwrap();
74        assert_eq!(bins.len(), 2);
75    }
76
77    #[test]
78    fn creates_bin_array_when_absent() {
79        let toml = "[package]\nname = \"asimov-widget-module\"\n";
80        let mut doc: DocumentMut = toml.parse().unwrap();
81        insert_bin(
82            &mut doc,
83            "asimov-widget-emitter",
84            "src/emitter/main.rs",
85            &[],
86        )
87        .unwrap();
88
89        let bins = doc["bin"].as_array_of_tables().unwrap();
90        assert_eq!(bins.len(), 1);
91        assert_eq!(
92            bins.get(0).unwrap()["name"].as_str(),
93            Some("asimov-widget-emitter")
94        );
95        assert!(bins.get(0).unwrap().get("required-features").is_none());
96    }
97
98    #[test]
99    fn omits_required_features_when_empty() {
100        let mut doc: DocumentMut = "[package]\nname = \"x\"\n".parse().unwrap();
101        insert_bin(&mut doc, "x-emitter", "src/emitter/main.rs", &[]).unwrap();
102        let bins = doc["bin"].as_array_of_tables().unwrap();
103        assert!(bins.get(0).unwrap().get("required-features").is_none());
104    }
105
106    #[test]
107    fn includes_required_features_when_given() {
108        let mut doc: DocumentMut = "[package]\nname = \"x\"\n".parse().unwrap();
109        insert_bin(
110            &mut doc,
111            "x-emitter",
112            "src/emitter/main.rs",
113            &[String::from("cli"), String::from("std")],
114        )
115        .unwrap();
116        let bins = doc["bin"].as_array_of_tables().unwrap();
117        let features = bins.get(0).unwrap()["required-features"]
118            .as_array()
119            .unwrap();
120        assert_eq!(features.len(), 2);
121    }
122}