1use crate::error::PackageError;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(untagged)]
13pub enum DependencySpec {
14 Git(GitDependency),
16 Path(PathDependency),
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24pub struct GitDependency {
25 pub git: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub tag: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub branch: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub rev: Option<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub struct PathDependency {
41 pub path: String,
43}
44
45impl DependencySpec {
46 pub fn with_tag(git: impl Into<String>, tag: impl Into<String>) -> Self {
48 Self::Git(GitDependency {
49 git: git.into(),
50 tag: Some(tag.into()),
51 branch: None,
52 rev: None,
53 })
54 }
55
56 pub fn with_branch(git: impl Into<String>, branch: impl Into<String>) -> Self {
58 Self::Git(GitDependency {
59 git: git.into(),
60 tag: None,
61 branch: Some(branch.into()),
62 rev: None,
63 })
64 }
65
66 pub fn with_rev(git: impl Into<String>, rev: impl Into<String>) -> Self {
68 Self::Git(GitDependency {
69 git: git.into(),
70 tag: None,
71 branch: None,
72 rev: Some(rev.into()),
73 })
74 }
75
76 pub fn with_path(path: impl Into<String>) -> Self {
78 Self::Path(PathDependency { path: path.into() })
79 }
80
81 pub fn is_path(&self) -> bool {
83 matches!(self, Self::Path(_))
84 }
85
86 pub fn is_git(&self) -> bool {
88 matches!(self, Self::Git(_))
89 }
90
91 pub fn git_url(&self) -> Option<&str> {
93 match self {
94 Self::Git(g) => Some(&g.git),
95 Self::Path(_) => None,
96 }
97 }
98
99 pub fn path(&self) -> Option<&str> {
101 match self {
102 Self::Path(p) => Some(&p.path),
103 Self::Git(_) => None,
104 }
105 }
106
107 pub fn validate(&self, package_name: &str) -> Result<(), PackageError> {
109 match self {
110 Self::Git(g) => {
111 let count = [&g.tag, &g.branch, &g.rev]
112 .iter()
113 .filter(|x| x.is_some())
114 .count();
115
116 if count != 1 {
117 return Err(PackageError::InvalidDependencySpec {
118 package: package_name.to_string(),
119 });
120 }
121 Ok(())
122 }
123 Self::Path(_) => Ok(()), }
125 }
126
127 pub fn ref_string(&self) -> &str {
129 match self {
130 Self::Git(g) => g
131 .tag
132 .as_deref()
133 .or(g.branch.as_deref())
134 .or(g.rev.as_deref())
135 .unwrap_or("HEAD"),
136 Self::Path(_) => "path",
137 }
138 }
139
140 pub fn ref_type(&self) -> &'static str {
142 match self {
143 Self::Git(g) => {
144 if g.tag.is_some() {
145 "tag"
146 } else if g.branch.is_some() {
147 "branch"
148 } else if g.rev.is_some() {
149 "rev"
150 } else {
151 "HEAD"
152 }
153 }
154 Self::Path(_) => "path",
155 }
156 }
157}
158
159impl GitDependency {
160 pub fn ref_string(&self) -> &str {
162 self.tag
163 .as_deref()
164 .or(self.branch.as_deref())
165 .or(self.rev.as_deref())
166 .unwrap_or("HEAD")
167 }
168}
169
170pub fn parse_dependencies(
172 table: &toml::Table,
173) -> Result<HashMap<String, DependencySpec>, PackageError> {
174 let mut deps = HashMap::new();
175
176 for (name, value) in table {
177 let spec = parse_dependency_value(name, value)?;
178 deps.insert(name.clone(), spec);
179 }
180
181 Ok(deps)
182}
183
184fn parse_dependency_value(name: &str, value: &toml::Value) -> Result<DependencySpec, PackageError> {
185 match value {
186 toml::Value::Table(t) => {
187 if let Some(path) = t.get("path").and_then(|v| v.as_str()) {
189 return Ok(DependencySpec::Path(PathDependency {
190 path: path.to_string(),
191 }));
192 }
193
194 let git = t
196 .get("git")
197 .and_then(|v| v.as_str())
198 .ok_or_else(|| PackageError::MissingGitUrl {
199 package: name.to_string(),
200 })?
201 .to_string();
202
203 let tag = t.get("tag").and_then(|v| v.as_str()).map(String::from);
204 let branch = t.get("branch").and_then(|v| v.as_str()).map(String::from);
205 let rev = t.get("rev").and_then(|v| v.as_str()).map(String::from);
206
207 let spec = DependencySpec::Git(GitDependency {
208 git,
209 tag,
210 branch,
211 rev,
212 });
213 spec.validate(name)?;
214 Ok(spec)
215 }
216 _ => Err(PackageError::InvalidDependencySpec {
217 package: name.to_string(),
218 }),
219 }
220}
221
222pub fn resolve_path(base_dir: &std::path::Path, path: &str) -> PathBuf {
224 let path = PathBuf::from(path);
225 if path.is_absolute() {
226 path
227 } else {
228 base_dir.join(path)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn parse_tag_dependency() {
238 let toml_str = r#"
239git = "https://github.com/example/package"
240tag = "v1.0.0"
241"#;
242 let value: toml::Value = toml::from_str(toml_str).unwrap();
243 let spec = parse_dependency_value("test", &value).unwrap();
244
245 match spec {
246 DependencySpec::Git(g) => {
247 assert_eq!(g.git, "https://github.com/example/package");
248 assert_eq!(g.tag, Some("v1.0.0".to_string()));
249 assert_eq!(g.branch, None);
250 assert_eq!(g.rev, None);
251 }
252 _ => panic!("Expected git dependency"),
253 }
254 }
255
256 #[test]
257 fn parse_branch_dependency() {
258 let toml_str = r#"
259git = "https://github.com/example/package"
260branch = "develop"
261"#;
262 let value: toml::Value = toml::from_str(toml_str).unwrap();
263 let spec = parse_dependency_value("test", &value).unwrap();
264
265 match spec {
266 DependencySpec::Git(g) => {
267 assert_eq!(g.branch, Some("develop".to_string()));
268 }
269 _ => panic!("Expected git dependency"),
270 }
271 }
272
273 #[test]
274 fn parse_rev_dependency() {
275 let toml_str = r#"
276git = "https://github.com/example/package"
277rev = "abc123"
278"#;
279 let value: toml::Value = toml::from_str(toml_str).unwrap();
280 let spec = parse_dependency_value("test", &value).unwrap();
281
282 match spec {
283 DependencySpec::Git(g) => {
284 assert_eq!(g.rev, Some("abc123".to_string()));
285 }
286 _ => panic!("Expected git dependency"),
287 }
288 }
289
290 #[test]
291 fn parse_path_dependency() {
292 let toml_str = r#"
293path = "../my-local-lib"
294"#;
295 let value: toml::Value = toml::from_str(toml_str).unwrap();
296 let spec = parse_dependency_value("test", &value).unwrap();
297
298 match spec {
299 DependencySpec::Path(p) => {
300 assert_eq!(p.path, "../my-local-lib");
301 }
302 _ => panic!("Expected path dependency"),
303 }
304 }
305
306 #[test]
307 fn parse_absolute_path_dependency() {
308 let toml_str = r#"
309path = "/Users/someone/projects/my-lib"
310"#;
311 let value: toml::Value = toml::from_str(toml_str).unwrap();
312 let spec = parse_dependency_value("test", &value).unwrap();
313
314 assert!(spec.is_path());
315 assert_eq!(spec.path(), Some("/Users/someone/projects/my-lib"));
316 }
317
318 #[test]
319 fn reject_missing_git_for_git_dep() {
320 let toml_str = r#"
321tag = "v1.0.0"
322"#;
323 let value: toml::Value = toml::from_str(toml_str).unwrap();
324 let result = parse_dependency_value("test", &value);
325
326 assert!(matches!(result, Err(PackageError::MissingGitUrl { .. })));
327 }
328
329 #[test]
330 fn reject_multiple_refs() {
331 let toml_str = r#"
332git = "https://github.com/example/package"
333tag = "v1.0.0"
334branch = "main"
335"#;
336 let value: toml::Value = toml::from_str(toml_str).unwrap();
337 let result = parse_dependency_value("test", &value);
338
339 assert!(matches!(
340 result,
341 Err(PackageError::InvalidDependencySpec { .. })
342 ));
343 }
344
345 #[test]
346 fn reject_no_ref() {
347 let toml_str = r#"
348git = "https://github.com/example/package"
349"#;
350 let value: toml::Value = toml::from_str(toml_str).unwrap();
351 let result = parse_dependency_value("test", &value);
352
353 assert!(matches!(
354 result,
355 Err(PackageError::InvalidDependencySpec { .. })
356 ));
357 }
358
359 #[test]
360 fn parse_multiple_dependencies() {
361 let table: toml::Table = toml::from_str(
362 r#"
363[foo]
364git = "https://github.com/example/foo"
365tag = "v1.0.0"
366
367[bar]
368git = "https://github.com/example/bar"
369branch = "main"
370
371[local]
372path = "../local-lib"
373"#,
374 )
375 .unwrap();
376
377 let deps = parse_dependencies(&table).unwrap();
378 assert_eq!(deps.len(), 3);
379 assert!(deps.contains_key("foo"));
380 assert!(deps.contains_key("bar"));
381 assert!(deps.contains_key("local"));
382 assert!(deps.get("local").unwrap().is_path());
383 }
384
385 #[test]
386 fn resolve_relative_path() {
387 use std::path::Path;
388 let base = Path::new("/home/user/project");
389 let resolved = resolve_path(base, "../lib");
390 assert_eq!(resolved, PathBuf::from("/home/user/project/../lib"));
391 }
392
393 #[test]
394 fn resolve_absolute_path() {
395 use std::path::Path;
396 let base = Path::new("/home/user/project");
397 let resolved = resolve_path(base, "/opt/libs/mylib");
398 assert_eq!(resolved, PathBuf::from("/opt/libs/mylib"));
399 }
400
401 #[test]
402 fn dependency_spec_helpers() {
403 let git = DependencySpec::with_tag("https://example.com", "v1.0");
404 assert!(git.is_git());
405 assert!(!git.is_path());
406 assert_eq!(git.git_url(), Some("https://example.com"));
407 assert_eq!(git.path(), None);
408
409 let path = DependencySpec::with_path("../lib");
410 assert!(path.is_path());
411 assert!(!path.is_git());
412 assert_eq!(path.git_url(), None);
413 assert_eq!(path.path(), Some("../lib"));
414 }
415}