Skip to main content

mars_agents/
types.rs

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/// Relative path under the install root (`.agents/` / project root).
108#[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    /// Resolve this relative path under a root path.
125    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/// Resolved context for a mars command — project root + managed output root.
205///
206/// Named fields prevent argument-order bugs that plague `(project_root, managed_root)` pairs.
207#[derive(Debug, Clone)]
208pub struct MarsContext {
209    /// Project root containing mars.toml and mars.lock.
210    pub project_root: PathBuf,
211    /// Managed output directory (e.g. /project/.agents).
212    pub managed_root: PathBuf,
213}
214
215#[cfg(test)]
216impl MarsContext {
217    /// Create a MarsContext for tests without any validation.
218    pub fn for_test(project_root: PathBuf, managed_root: PathBuf) -> Self {
219        MarsContext {
220            project_root,
221            managed_root,
222        }
223    }
224}
225
226/// Stable source identity used for resolver deduplication.
227#[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/// Ordered rename rules, serialized as TOML inline table/map for compatibility.
265#[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}