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(Debug, Clone, PartialEq, Eq)]
109pub enum SourceOrigin {
110 Dependency(SourceName),
112 LocalPackage,
114}
115
116impl fmt::Display for SourceOrigin {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 match self {
119 Self::Dependency(name) => write!(f, "{name}"),
120 Self::LocalPackage => write!(f, "_self"),
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
127#[serde(rename_all = "lowercase")]
128pub enum ItemKind {
129 Agent,
130 Skill,
131}
132
133impl fmt::Display for ItemKind {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 ItemKind::Agent => write!(f, "agent"),
137 ItemKind::Skill => write!(f, "skill"),
138 }
139 }
140}
141
142#[derive(Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
147pub struct ItemId {
148 pub kind: ItemKind,
149 pub name: ItemName,
150}
151
152impl fmt::Display for ItemId {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 write!(f, "{}/{}", self.kind, self.name)
155 }
156}
157
158#[derive(Eq, PartialEq, Clone, Debug, Ord, PartialOrd)]
160pub struct DestPath(PathBuf);
161
162impl DestPath {
163 pub fn new(value: impl Into<PathBuf>) -> Self {
164 Self(value.into())
165 }
166
167 pub fn as_path(&self) -> &Path {
168 &self.0
169 }
170
171 pub fn into_inner(self) -> PathBuf {
172 self.0
173 }
174
175 pub fn resolve(&self, root: &Path) -> PathBuf {
177 root.join(&self.0)
178 }
179}
180
181impl From<PathBuf> for DestPath {
182 fn from(value: PathBuf) -> Self {
183 Self(value)
184 }
185}
186
187impl From<&Path> for DestPath {
188 fn from(value: &Path) -> Self {
189 Self(value.to_path_buf())
190 }
191}
192
193impl From<&str> for DestPath {
194 fn from(value: &str) -> Self {
195 Self(PathBuf::from(value))
196 }
197}
198
199impl From<String> for DestPath {
200 fn from(value: String) -> Self {
201 Self(PathBuf::from(value))
202 }
203}
204
205impl AsRef<Path> for DestPath {
206 fn as_ref(&self) -> &Path {
207 &self.0
208 }
209}
210
211impl Borrow<Path> for DestPath {
212 fn borrow(&self) -> &Path {
213 &self.0
214 }
215}
216
217impl Borrow<str> for DestPath {
218 fn borrow(&self) -> &str {
219 self.0.to_str().expect("DestPath must be valid UTF-8")
220 }
221}
222
223impl Hash for DestPath {
224 fn hash<H: Hasher>(&self, state: &mut H) {
225 self.0.to_string_lossy().hash(state);
226 }
227}
228
229impl Deref for DestPath {
230 type Target = Path;
231
232 fn deref(&self) -> &Self::Target {
233 &self.0
234 }
235}
236
237impl fmt::Display for DestPath {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 write!(f, "{}", self.0.display())
240 }
241}
242
243impl Serialize for DestPath {
244 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
245 self.0.to_string_lossy().serialize(serializer)
246 }
247}
248
249impl<'de> Deserialize<'de> for DestPath {
250 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
251 String::deserialize(deserializer).map(|s| Self(PathBuf::from(s)))
252 }
253}
254
255#[derive(Debug, Clone)]
259pub struct MarsContext {
260 pub project_root: PathBuf,
262 pub managed_root: PathBuf,
264}
265
266#[cfg(test)]
267impl MarsContext {
268 pub fn for_test(project_root: PathBuf, managed_root: PathBuf) -> Self {
270 MarsContext {
271 project_root,
272 managed_root,
273 }
274 }
275}
276
277#[derive(Hash, Eq, PartialEq, Clone, Debug, Ord, PartialOrd)]
279pub enum SourceId {
280 Git { url: SourceUrl },
281 Path { canonical: PathBuf },
282}
283
284impl SourceId {
285 pub fn git(url: SourceUrl) -> Self {
286 Self::Git { url }
287 }
288
289 pub fn path(base: &Path, relative_or_absolute: &Path) -> std::io::Result<Self> {
290 let candidate = if relative_or_absolute.is_absolute() {
291 relative_or_absolute.to_path_buf()
292 } else {
293 base.join(relative_or_absolute)
294 };
295 let canonical = candidate.canonicalize()?;
296 Ok(Self::Path { canonical })
297 }
298}
299
300impl fmt::Display for SourceId {
301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 match self {
303 Self::Git { url } => write!(f, "git:{url}"),
304 Self::Path { canonical } => write!(f, "path:{}", canonical.display()),
305 }
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct RenameRule {
311 pub from: ItemName,
312 pub to: ItemName,
313}
314
315#[derive(Debug, Clone, Default, PartialEq, Eq)]
317pub struct RenameMap(Vec<RenameRule>);
318
319impl RenameMap {
320 pub fn new() -> Self {
321 Self(Vec::new())
322 }
323
324 pub fn insert(&mut self, from: ItemName, to: ItemName) {
325 if let Some(existing) = self.0.iter_mut().find(|r| r.from == from) {
326 existing.to = to;
327 return;
328 }
329 self.0.push(RenameRule { from, to });
330 }
331
332 pub fn push(&mut self, rule: RenameRule) {
333 self.insert(rule.from, rule.to);
334 }
335
336 pub fn get(&self, from: &str) -> Option<&ItemName> {
337 self.0.iter().find(|r| r.from == from).map(|r| &r.to)
338 }
339
340 pub fn iter(&self) -> impl Iterator<Item = &RenameRule> {
341 self.0.iter()
342 }
343
344 pub fn is_empty(&self) -> bool {
345 self.0.is_empty()
346 }
347
348 pub fn len(&self) -> usize {
349 self.0.len()
350 }
351}
352
353impl Serialize for RenameMap {
354 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
355 use serde::ser::SerializeMap;
356 let mut map = serializer.serialize_map(Some(self.0.len()))?;
357 for rule in &self.0 {
358 map.serialize_entry(rule.from.as_str(), rule.to.as_str())?;
359 }
360 map.end()
361 }
362}
363
364impl<'de> Deserialize<'de> for RenameMap {
365 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
366 let map = indexmap::IndexMap::<String, String>::deserialize(deserializer)?;
367 Ok(Self(
368 map.into_iter()
369 .map(|(from, to)| RenameRule {
370 from: ItemName::from(from),
371 to: ItemName::from(to),
372 })
373 .collect(),
374 ))
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use serde::{Deserialize, Serialize};
382
383 #[derive(Debug, Serialize, Deserialize, PartialEq)]
384 struct Wrapper<T> {
385 value: T,
386 }
387
388 #[test]
389 fn dest_path_roundtrip() {
390 let v = Wrapper {
391 value: DestPath::from("agents/coder.md"),
392 };
393 let s = toml::to_string(&v).unwrap();
394 let out: Wrapper<DestPath> = toml::from_str(&s).unwrap();
395 assert_eq!(v, out);
396 }
397
398 #[test]
399 fn rename_map_toml_roundtrip_compat() {
400 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
401 struct RenameWrapper {
402 rename: RenameMap,
403 }
404
405 let input = r#"rename = { "coder" = "cool-coder" }"#;
406 let parsed: RenameWrapper = toml::from_str(input).unwrap();
407 assert_eq!(
408 parsed.rename.get("coder").map(|v| v.as_str()),
409 Some("cool-coder")
410 );
411
412 let serialized = toml::to_string(&parsed).unwrap();
413 let reparsed: RenameWrapper = toml::from_str(&serialized).unwrap();
414 assert_eq!(parsed, reparsed);
415 }
416}