1use std::{
35 collections::HashMap,
36 fs,
37 path::{Path, PathBuf},
38};
39
40use serde::{Deserialize, Serialize};
41
42use crate::PkgError;
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct Package {
54 pub name: String,
56
57 pub version: String,
59
60 pub entry: Option<PathBuf>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct Dep {
77 pub git: String,
79
80 pub tag: Option<String>,
82
83 pub rev: Option<String>,
85
86 pub branch: Option<String>,
88
89 pub entry: Option<PathBuf>,
94
95 pub target_dir: Option<PathBuf>,
101}
102
103impl Dep {
104 fn validate_ref_exclusivity(&self, dep_name: &str) -> Result<(), PkgError> {
106 let count = [
107 self.tag.is_some(),
108 self.rev.is_some(),
109 self.branch.is_some(),
110 ]
111 .into_iter()
112 .filter(|&b| b)
113 .count();
114 if count > 1 {
115 return Err(PkgError::Validation {
116 message: format!(
117 "dep '{dep_name}': only one of `tag`, `rev`, `branch` may be specified, \
118 but multiple are set"
119 ),
120 });
121 }
122 Ok(())
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(deny_unknown_fields)]
137pub struct Manifest {
138 pub package: Package,
140
141 #[serde(default)]
145 pub deps: HashMap<String, Dep>,
146}
147
148impl Manifest {
149 pub fn from_path(path: impl AsRef<Path>) -> Result<Self, PkgError> {
163 let content = fs::read_to_string(path)?;
164 let manifest: Self = toml::from_str(&content)?;
165 manifest.validate()?;
166 Ok(manifest)
167 }
168
169 fn validate(&self) -> Result<(), PkgError> {
171 for (name, dep) in &self.deps {
172 dep.validate_ref_exclusivity(name)?;
173 }
174 Ok(())
175 }
176}
177
178#[cfg(test)]
181mod tests {
182 use super::*;
183 use std::io::Write as _;
184
185 fn temp_manifest(content: &str) -> tempfile::NamedTempFile {
188 let mut f = tempfile::NamedTempFile::new().unwrap();
189 f.write_all(content.as_bytes()).unwrap();
190 f
191 }
192
193 #[test]
196 fn consumer_happy_path() {
197 let toml = r#"
198[package]
199name = "my-app"
200version = "0.1.0"
201
202[deps]
203foo = { git = "https://github.com/x/foo", tag = "v1.2.0" }
204bar = { git = "https://github.com/y/bar", rev = "abc123" }
205baz = { git = "https://github.com/z/baz", branch = "main" }
206
207[deps.qux]
208git = "https://github.com/q/qux"
209tag = "v2.0.0"
210entry = "lib"
211"#;
212 let f = temp_manifest(toml);
213 let m = Manifest::from_path(f.path()).unwrap();
214
215 assert_eq!(m.package.name, "my-app");
216 assert_eq!(m.package.version, "0.1.0");
217 assert!(m.package.entry.is_none());
218 assert_eq!(m.deps.len(), 4);
219
220 let foo = &m.deps["foo"];
221 assert_eq!(foo.git, "https://github.com/x/foo");
222 assert_eq!(foo.tag.as_deref(), Some("v1.2.0"));
223 assert!(foo.rev.is_none());
224 assert!(foo.branch.is_none());
225 assert!(foo.entry.is_none());
226
227 let bar = &m.deps["bar"];
228 assert_eq!(bar.rev.as_deref(), Some("abc123"));
229 assert!(bar.tag.is_none());
230
231 let baz = &m.deps["baz"];
232 assert_eq!(baz.branch.as_deref(), Some("main"));
233 assert!(baz.tag.is_none());
234
235 let qux = &m.deps["qux"];
236 assert_eq!(qux.tag.as_deref(), Some("v2.0.0"));
237 assert_eq!(qux.entry, Some(PathBuf::from("lib")));
238 }
239
240 #[test]
243 fn author_happy_path() {
244 let toml = r#"
245[package]
246name = "foo"
247version = "1.2.0"
248entry = "src"
249"#;
250 let f = temp_manifest(toml);
251 let m = Manifest::from_path(f.path()).unwrap();
252
253 assert_eq!(m.package.name, "foo");
254 assert_eq!(m.package.version, "1.2.0");
255 assert_eq!(m.package.entry, Some(PathBuf::from("src")));
256 assert!(m.deps.is_empty());
257 }
258
259 #[test]
262 fn tag_and_rev_mutually_exclusive() {
263 let toml = r#"
264[package]
265name = "my-app"
266version = "0.1.0"
267
268[deps.bad]
269git = "https://github.com/x/bad"
270tag = "v1.0.0"
271rev = "abc123"
272"#;
273 let f = temp_manifest(toml);
274 let err = Manifest::from_path(f.path()).unwrap_err();
275 assert!(
276 matches!(err, PkgError::Validation { .. }),
277 "expected Validation error, got: {err}"
278 );
279 assert!(err.to_string().contains("bad"));
280 }
281
282 #[test]
285 fn invalid_toml_returns_parse_error() {
286 let toml = "this is not valid = [ toml";
287 let f = temp_manifest(toml);
288 let err = Manifest::from_path(f.path()).unwrap_err();
289 assert!(
290 matches!(err, PkgError::ManifestParse { .. }),
291 "expected ManifestParse error, got: {err}"
292 );
293 }
294
295 #[test]
298 fn missing_package_section_returns_parse_error() {
299 let toml = r#"
300[deps]
301foo = { git = "https://github.com/x/foo", tag = "v1.0.0" }
302"#;
303 let f = temp_manifest(toml);
304 let err = Manifest::from_path(f.path()).unwrap_err();
305 assert!(
306 matches!(err, PkgError::ManifestParse { .. }),
307 "expected ManifestParse error for missing [package], got: {err}"
308 );
309 }
310
311 #[test]
314 fn round_trip_serialize_deserialize() {
315 let original = Manifest {
316 package: Package {
317 name: "roundtrip".into(),
318 version: "0.1.0".into(),
319 entry: None,
320 },
321 deps: {
322 let mut m = HashMap::new();
323 m.insert(
324 "lib".into(),
325 Dep {
326 git: "https://github.com/x/lib".into(),
327 tag: Some("v1.0.0".into()),
328 rev: None,
329 branch: None,
330 entry: None,
331 target_dir: None,
332 },
333 );
334 m
335 },
336 };
337
338 let serialized = toml::to_string(&original).unwrap();
339 let deserialized: Manifest = toml::from_str(&serialized).unwrap();
340 assert_eq!(original, deserialized);
341 }
342
343 #[test]
346 fn all_three_ref_fields_is_validation_error() {
347 let toml = r#"
348[package]
349name = "my-app"
350version = "0.1.0"
351
352[deps.oops]
353git = "https://github.com/x/oops"
354tag = "v1.0.0"
355rev = "abc123"
356branch = "main"
357"#;
358 let f = temp_manifest(toml);
359 let err = Manifest::from_path(f.path()).unwrap_err();
360 assert!(matches!(err, PkgError::Validation { .. }));
361 }
362
363 #[test]
366 fn unknown_field_in_package_is_rejected() {
367 let toml = r#"
368[package]
369name = "my-app"
370version = "0.1.0"
371unknown = "should-fail"
372"#;
373 let f = temp_manifest(toml);
374 let err = Manifest::from_path(f.path()).unwrap_err();
375 assert!(
376 matches!(err, PkgError::ManifestParse { .. }),
377 "expected ManifestParse error for unknown field, got: {err}"
378 );
379 }
380}