1use serde::{Deserialize, Serialize};
2use std::borrow::Borrow;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::ops::Deref;
6use std::path::{Path, PathBuf};
7
8macro_rules! string_newtype {
9 ($(#[$meta:meta])* $name:ident) => {
10 $(#[$meta])*
11 #[derive(
12 Serialize, Deserialize, Hash, Eq, PartialEq, Clone, Debug, Ord, PartialOrd,
13 )]
14 #[serde(transparent)]
15 pub struct $name(String);
16
17 impl $name {
18 pub fn new(value: impl Into<String>) -> Self {
19 Self(value.into())
20 }
21
22 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25
26 pub fn into_inner(self) -> String {
27 self.0
28 }
29 }
30
31 impl From<String> for $name {
32 fn from(value: String) -> Self {
33 Self(value)
34 }
35 }
36
37 impl From<&str> for $name {
38 fn from(value: &str) -> Self {
39 Self(value.to_owned())
40 }
41 }
42
43 impl AsRef<str> for $name {
44 fn as_ref(&self) -> &str {
45 &self.0
46 }
47 }
48
49 impl Borrow<str> for $name {
50 fn borrow(&self) -> &str {
51 &self.0
52 }
53 }
54
55 impl Deref for $name {
56 type Target = str;
57
58 fn deref(&self) -> &Self::Target {
59 &self.0
60 }
61 }
62
63 impl From<$name> for String {
64 fn from(value: $name) -> Self {
65 value.0
66 }
67 }
68
69 impl fmt::Display for $name {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str(&self.0)
72 }
73 }
74
75 impl PartialEq<str> for $name {
76 fn eq(&self, other: &str) -> bool {
77 self.0 == other
78 }
79 }
80
81 impl PartialEq<&str> for $name {
82 fn eq(&self, other: &&str) -> bool {
83 self.0 == *other
84 }
85 }
86
87 impl PartialEq<String> for $name {
88 fn eq(&self, other: &String) -> bool {
89 self.0 == *other
90 }
91 }
92
93 impl PartialEq<$name> for String {
94 fn eq(&self, other: &$name) -> bool {
95 *self == other.0
96 }
97 }
98 };
99}
100
101string_newtype!(SourceName);
102string_newtype!(ItemName);
103string_newtype!(SourceUrl);
104string_newtype!(CommitHash);
105string_newtype!(ContentHash);
106
107#[derive(Eq, PartialEq, Clone, Debug, Ord, PartialOrd)]
109pub struct DestPath(PathBuf);
110
111impl DestPath {
112 pub fn new(value: impl Into<PathBuf>) -> Self {
113 Self(value.into())
114 }
115
116 pub fn as_path(&self) -> &Path {
117 &self.0
118 }
119
120 pub fn into_inner(self) -> PathBuf {
121 self.0
122 }
123
124 pub fn resolve(&self, root: &Path) -> PathBuf {
126 root.join(&self.0)
127 }
128}
129
130impl From<PathBuf> for DestPath {
131 fn from(value: PathBuf) -> Self {
132 Self(value)
133 }
134}
135
136impl From<&Path> for DestPath {
137 fn from(value: &Path) -> Self {
138 Self(value.to_path_buf())
139 }
140}
141
142impl From<&str> for DestPath {
143 fn from(value: &str) -> Self {
144 Self(PathBuf::from(value))
145 }
146}
147
148impl From<String> for DestPath {
149 fn from(value: String) -> Self {
150 Self(PathBuf::from(value))
151 }
152}
153
154impl AsRef<Path> for DestPath {
155 fn as_ref(&self) -> &Path {
156 &self.0
157 }
158}
159
160impl Borrow<Path> for DestPath {
161 fn borrow(&self) -> &Path {
162 &self.0
163 }
164}
165
166impl Borrow<str> for DestPath {
167 fn borrow(&self) -> &str {
168 self.0.to_str().expect("DestPath must be valid UTF-8")
169 }
170}
171
172impl Hash for DestPath {
173 fn hash<H: Hasher>(&self, state: &mut H) {
174 self.0.to_string_lossy().hash(state);
175 }
176}
177
178impl Deref for DestPath {
179 type Target = Path;
180
181 fn deref(&self) -> &Self::Target {
182 &self.0
183 }
184}
185
186impl fmt::Display for DestPath {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 write!(f, "{}", self.0.display())
189 }
190}
191
192impl Serialize for DestPath {
193 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
194 self.0.to_string_lossy().serialize(serializer)
195 }
196}
197
198impl<'de> Deserialize<'de> for DestPath {
199 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
200 String::deserialize(deserializer).map(|s| Self(PathBuf::from(s)))
201 }
202}
203
204#[derive(Debug, Clone)]
208pub struct MarsContext {
209 pub project_root: PathBuf,
211 pub managed_root: PathBuf,
213}
214
215#[cfg(test)]
216impl MarsContext {
217 pub fn for_test(project_root: PathBuf, managed_root: PathBuf) -> Self {
219 MarsContext {
220 project_root,
221 managed_root,
222 }
223 }
224}
225
226#[derive(Hash, Eq, PartialEq, Clone, Debug, Ord, PartialOrd)]
228pub enum SourceId {
229 Git { url: SourceUrl },
230 Path { canonical: PathBuf },
231}
232
233impl SourceId {
234 pub fn git(url: SourceUrl) -> Self {
235 Self::Git { url }
236 }
237
238 pub fn path(base: &Path, relative_or_absolute: &Path) -> std::io::Result<Self> {
239 let candidate = if relative_or_absolute.is_absolute() {
240 relative_or_absolute.to_path_buf()
241 } else {
242 base.join(relative_or_absolute)
243 };
244 let canonical = candidate.canonicalize()?;
245 Ok(Self::Path { canonical })
246 }
247}
248
249impl fmt::Display for SourceId {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 match self {
252 Self::Git { url } => write!(f, "git:{url}"),
253 Self::Path { canonical } => write!(f, "path:{}", canonical.display()),
254 }
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct RenameRule {
260 pub from: ItemName,
261 pub to: ItemName,
262}
263
264#[derive(Debug, Clone, Default, PartialEq, Eq)]
266pub struct RenameMap(Vec<RenameRule>);
267
268impl RenameMap {
269 pub fn new() -> Self {
270 Self(Vec::new())
271 }
272
273 pub fn insert(&mut self, from: ItemName, to: ItemName) {
274 if let Some(existing) = self.0.iter_mut().find(|r| r.from == from) {
275 existing.to = to;
276 return;
277 }
278 self.0.push(RenameRule { from, to });
279 }
280
281 pub fn push(&mut self, rule: RenameRule) {
282 self.insert(rule.from, rule.to);
283 }
284
285 pub fn get(&self, from: &str) -> Option<&ItemName> {
286 self.0.iter().find(|r| r.from == from).map(|r| &r.to)
287 }
288
289 pub fn iter(&self) -> impl Iterator<Item = &RenameRule> {
290 self.0.iter()
291 }
292
293 pub fn is_empty(&self) -> bool {
294 self.0.is_empty()
295 }
296
297 pub fn len(&self) -> usize {
298 self.0.len()
299 }
300}
301
302impl Serialize for RenameMap {
303 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
304 use serde::ser::SerializeMap;
305 let mut map = serializer.serialize_map(Some(self.0.len()))?;
306 for rule in &self.0 {
307 map.serialize_entry(rule.from.as_str(), rule.to.as_str())?;
308 }
309 map.end()
310 }
311}
312
313impl<'de> Deserialize<'de> for RenameMap {
314 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
315 let map = indexmap::IndexMap::<String, String>::deserialize(deserializer)?;
316 Ok(Self(
317 map.into_iter()
318 .map(|(from, to)| RenameRule {
319 from: ItemName::from(from),
320 to: ItemName::from(to),
321 })
322 .collect(),
323 ))
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use serde::{Deserialize, Serialize};
331
332 #[derive(Debug, Serialize, Deserialize, PartialEq)]
333 struct Wrapper<T> {
334 value: T,
335 }
336
337 #[test]
338 fn dest_path_roundtrip() {
339 let v = Wrapper {
340 value: DestPath::from("agents/coder.md"),
341 };
342 let s = toml::to_string(&v).unwrap();
343 let out: Wrapper<DestPath> = toml::from_str(&s).unwrap();
344 assert_eq!(v, out);
345 }
346
347 #[test]
348 fn rename_map_toml_roundtrip_compat() {
349 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
350 struct RenameWrapper {
351 rename: RenameMap,
352 }
353
354 let input = r#"rename = { "coder" = "cool-coder" }"#;
355 let parsed: RenameWrapper = toml::from_str(input).unwrap();
356 assert_eq!(
357 parsed.rename.get("coder").map(|v| v.as_str()),
358 Some("cool-coder")
359 );
360
361 let serialized = toml::to_string(&parsed).unwrap();
362 let reparsed: RenameWrapper = toml::from_str(&serialized).unwrap();
363 assert_eq!(parsed, reparsed);
364 }
365}