ready_set_rust/
manifest_edit.rs1use 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#[derive(Debug, Default, Clone)]
12pub struct WorkspacePlan {
13 pub changed: bool,
15 pub added_members: Vec<String>,
17 pub workspace_table_added: bool,
19}
20
21#[derive(Debug, Default, Clone)]
23pub struct LintsPlan {
24 pub changed: bool,
26 pub lints_updated: bool,
28 pub lints_drifted: bool,
31}
32
33pub 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
85pub 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
127pub 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}