Skip to main content

ready_set_rust/
manifest_edit.rs

1//! Edit the root `Cargo.toml` to register workspace state and lints.
2
3use std::collections::BTreeSet;
4
5use ready_set_sdk::{Error, Result};
6use toml_edit::{Array, DocumentMut, Item, Table};
7
8use crate::templates::WORKSPACE_LINTS;
9
10/// Plan of workspace edits.
11#[derive(Debug, Default, Clone)]
12pub struct WorkspacePlan {
13    /// Whether the document was changed.
14    pub changed: bool,
15    /// Members added to `[workspace.members]`.
16    pub added_members: Vec<String>,
17    /// Whether the `[workspace]` table was added.
18    pub workspace_table_added: bool,
19}
20
21/// Plan of lint edits.
22#[derive(Debug, Default, Clone)]
23pub struct LintsPlan {
24    /// Whether the document was changed.
25    pub changed: bool,
26    /// Whether `[workspace.lints.*]` was rewritten.
27    pub lints_updated: bool,
28    /// Whether existing lints differ from the template but `--force` was not
29    /// requested.
30    pub lints_drifted: bool,
31}
32
33/// Apply workspace edits to `doc`.
34///
35/// # Errors
36///
37/// Returns [`Error::Other`] if `[workspace]` exists but is not a table.
38pub fn apply_workspace(doc: &mut DocumentMut, desired_members: &[String]) -> Result<WorkspacePlan> {
39    let mut plan = WorkspacePlan::default();
40
41    if doc.get("workspace").is_none() {
42        doc["workspace"] = Item::Table(Table::new());
43        plan.workspace_table_added = true;
44        plan.changed = true;
45    }
46    let workspace = doc["workspace"]
47        .as_table_mut()
48        .ok_or_else(|| Error::Other("[workspace] is not a table".into()))?;
49
50    if workspace.get("resolver").and_then(Item::as_str) != Some("3") {
51        workspace["resolver"] = toml_edit::value("3");
52        plan.changed = true;
53    }
54
55    let mut existing: BTreeSet<String> = BTreeSet::new();
56    if let Some(arr) = workspace.get("members").and_then(Item::as_array) {
57        for value in arr {
58            if let Some(member) = value.as_str() {
59                existing.insert(member.to_string());
60            }
61        }
62    }
63    let mut merged = existing.clone();
64    for member in desired_members {
65        merged.insert(member.clone());
66    }
67    if merged != existing {
68        let mut arr = Array::new();
69        for member in &merged {
70            arr.push(member.as_str());
71            if !existing.contains(member) {
72                plan.added_members.push(member.clone());
73            }
74        }
75        workspace["members"] = toml_edit::value(arr);
76        plan.changed = true;
77    } else if workspace.get("members").is_none() {
78        workspace["members"] = toml_edit::value(Array::new());
79        plan.changed = true;
80    }
81
82    Ok(plan)
83}
84
85/// Apply workspace lint edits to `doc`.
86///
87/// # Errors
88///
89/// Returns [`Error::TomlParse`] if the lints template fails to parse or
90/// [`Error::Other`] if `[workspace]` exists but is not a table.
91pub fn apply_lints(doc: &mut DocumentMut, force: bool) -> Result<LintsPlan> {
92    let mut plan = LintsPlan::default();
93    if doc.get("workspace").is_none() {
94        doc["workspace"] = Item::Table(Table::new());
95        plan.changed = true;
96    }
97    let workspace = doc["workspace"]
98        .as_table_mut()
99        .ok_or_else(|| Error::Other("[workspace] is not a table".into()))?;
100
101    let template_doc: DocumentMut =
102        WORKSPACE_LINTS.parse().map_err(|e: toml_edit::TomlError| {
103            Error::TomlParse(format!("workspace-lints template: {e}"))
104        })?;
105    let template_lints = template_doc["workspace"]["lints"].clone();
106
107    match workspace.get("lints") {
108        None => {
109            workspace["lints"] = template_lints;
110            plan.lints_updated = true;
111            plan.changed = true;
112        },
113        Some(existing) if items_structurally_equal(existing, &template_lints) => {},
114        Some(_) if force => {
115            workspace["lints"] = template_lints;
116            plan.lints_updated = true;
117            plan.changed = true;
118        },
119        Some(_) => {
120            plan.lints_drifted = true;
121        },
122    }
123
124    Ok(plan)
125}
126
127/// Compare an existing document's `[workspace.lints]` table to the built-in
128/// lints template.
129///
130/// # Errors
131///
132/// Returns [`Error::TomlParse`] if the canonical lints template fails to
133/// parse.
134pub fn workspace_lints_match(doc: &DocumentMut) -> Result<Option<bool>> {
135    let Some(workspace) = doc.get("workspace").and_then(Item::as_table) else {
136        return Ok(None);
137    };
138    let Some(existing) = workspace.get("lints") else {
139        return Ok(None);
140    };
141    let template_doc: DocumentMut =
142        WORKSPACE_LINTS.parse().map_err(|e: toml_edit::TomlError| {
143            Error::TomlParse(format!("workspace-lints template: {e}"))
144        })?;
145    let template_lints = &template_doc["workspace"]["lints"];
146
147    Ok(Some(items_structurally_equal(existing, template_lints)))
148}
149
150fn items_structurally_equal(a: &Item, b: &Item) -> bool {
151    let to_value = |item: &Item| -> Option<toml::Value> {
152        let raw = format!("k = {item}");
153        match item {
154            Item::Table(_) | Item::ArrayOfTables(_) => {
155                let mut wrapper = String::from("[wrapper]\n");
156                let body = item.to_string();
157                if body.is_empty() {
158                    return None;
159                }
160                wrapper.push_str(&body);
161                toml::from_str::<toml::Value>(&wrapper).ok()
162            },
163            _ => match toml::from_str::<toml::Value>(&raw).ok()? {
164                toml::Value::Table(t) => t.into_iter().next().map(|(_, v)| v),
165                _ => None,
166            },
167        }
168    };
169
170    match (to_value(a), to_value(b)) {
171        (Some(x), Some(y)) => x == y,
172        _ => tables_equal(a, b),
173    }
174}
175
176fn tables_equal(a: &Item, b: &Item) -> bool {
177    match (a, b) {
178        (Item::Table(ta), Item::Table(tb)) => {
179            let keys_a: BTreeSet<&str> = ta.iter().map(|(k, _)| k).collect();
180            let keys_b: BTreeSet<&str> = tb.iter().map(|(k, _)| k).collect();
181            if keys_a != keys_b {
182                return false;
183            }
184            for key in &keys_a {
185                let Some(va) = ta.get(key) else { return false };
186                let Some(vb) = tb.get(key) else { return false };
187                if !tables_equal(va, vb) {
188                    return false;
189                }
190            }
191            true
192        },
193        (Item::Value(va), Item::Value(vb)) => va.to_string().trim() == vb.to_string().trim(),
194        _ => false,
195    }
196}