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