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
96impl Dep {
97 fn validate_ref_exclusivity(&self, dep_name: &str) -> Result<(), PkgError> {
99 let count = [
100 self.tag.is_some(),
101 self.rev.is_some(),
102 self.branch.is_some(),
103 ]
104 .into_iter()
105 .filter(|&b| b)
106 .count();
107 if count > 1 {
108 return Err(PkgError::Validation {
109 message: format!(
110 "dep '{dep_name}': only one of `tag`, `rev`, `branch` may be specified, \
111 but multiple are set"
112 ),
113 });
114 }
115 Ok(())
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(deny_unknown_fields)]
130pub struct Manifest {
131 pub package: Package,
133
134 #[serde(default)]
138 pub deps: HashMap<String, Dep>,
139}
140
141impl Manifest {
142 pub fn from_path(path: impl AsRef<Path>) -> Result<Self, PkgError> {
156 let content = fs::read_to_string(path)?;
157 let manifest: Self = toml::from_str(&content)?;
158 manifest.validate()?;
159 Ok(manifest)
160 }
161
162 fn validate(&self) -> Result<(), PkgError> {
164 for (name, dep) in &self.deps {
165 dep.validate_ref_exclusivity(name)?;
166 }
167 Ok(())
168 }
169}
170
171#[cfg(test)]
174mod tests {
175 use super::*;
176 use std::io::Write as _;
177
178 fn temp_manifest(content: &str) -> tempfile::NamedTempFile {
181 let mut f = tempfile::NamedTempFile::new().unwrap();
182 f.write_all(content.as_bytes()).unwrap();
183 f
184 }
185
186 #[test]
189 fn consumer_happy_path() {
190 let toml = r#"
191[package]
192name = "my-app"
193version = "0.1.0"
194
195[deps]
196foo = { git = "https://github.com/x/foo", tag = "v1.2.0" }
197bar = { git = "https://github.com/y/bar", rev = "abc123" }
198baz = { git = "https://github.com/z/baz", branch = "main" }
199
200[deps.qux]
201git = "https://github.com/q/qux"
202tag = "v2.0.0"
203entry = "lib"
204"#;
205 let f = temp_manifest(toml);
206 let m = Manifest::from_path(f.path()).unwrap();
207
208 assert_eq!(m.package.name, "my-app");
209 assert_eq!(m.package.version, "0.1.0");
210 assert!(m.package.entry.is_none());
211 assert_eq!(m.deps.len(), 4);
212
213 let foo = &m.deps["foo"];
214 assert_eq!(foo.git, "https://github.com/x/foo");
215 assert_eq!(foo.tag.as_deref(), Some("v1.2.0"));
216 assert!(foo.rev.is_none());
217 assert!(foo.branch.is_none());
218 assert!(foo.entry.is_none());
219
220 let bar = &m.deps["bar"];
221 assert_eq!(bar.rev.as_deref(), Some("abc123"));
222 assert!(bar.tag.is_none());
223
224 let baz = &m.deps["baz"];
225 assert_eq!(baz.branch.as_deref(), Some("main"));
226 assert!(baz.tag.is_none());
227
228 let qux = &m.deps["qux"];
229 assert_eq!(qux.tag.as_deref(), Some("v2.0.0"));
230 assert_eq!(qux.entry, Some(PathBuf::from("lib")));
231 }
232
233 #[test]
236 fn author_happy_path() {
237 let toml = r#"
238[package]
239name = "foo"
240version = "1.2.0"
241entry = "src"
242"#;
243 let f = temp_manifest(toml);
244 let m = Manifest::from_path(f.path()).unwrap();
245
246 assert_eq!(m.package.name, "foo");
247 assert_eq!(m.package.version, "1.2.0");
248 assert_eq!(m.package.entry, Some(PathBuf::from("src")));
249 assert!(m.deps.is_empty());
250 }
251
252 #[test]
255 fn tag_and_rev_mutually_exclusive() {
256 let toml = r#"
257[package]
258name = "my-app"
259version = "0.1.0"
260
261[deps.bad]
262git = "https://github.com/x/bad"
263tag = "v1.0.0"
264rev = "abc123"
265"#;
266 let f = temp_manifest(toml);
267 let err = Manifest::from_path(f.path()).unwrap_err();
268 assert!(
269 matches!(err, PkgError::Validation { .. }),
270 "expected Validation error, got: {err}"
271 );
272 assert!(err.to_string().contains("bad"));
273 }
274
275 #[test]
278 fn invalid_toml_returns_parse_error() {
279 let toml = "this is not valid = [ toml";
280 let f = temp_manifest(toml);
281 let err = Manifest::from_path(f.path()).unwrap_err();
282 assert!(
283 matches!(err, PkgError::ManifestParse { .. }),
284 "expected ManifestParse error, got: {err}"
285 );
286 }
287
288 #[test]
291 fn missing_package_section_returns_parse_error() {
292 let toml = r#"
293[deps]
294foo = { git = "https://github.com/x/foo", tag = "v1.0.0" }
295"#;
296 let f = temp_manifest(toml);
297 let err = Manifest::from_path(f.path()).unwrap_err();
298 assert!(
299 matches!(err, PkgError::ManifestParse { .. }),
300 "expected ManifestParse error for missing [package], got: {err}"
301 );
302 }
303
304 #[test]
307 fn round_trip_serialize_deserialize() {
308 let original = Manifest {
309 package: Package {
310 name: "roundtrip".into(),
311 version: "0.1.0".into(),
312 entry: None,
313 },
314 deps: {
315 let mut m = HashMap::new();
316 m.insert(
317 "lib".into(),
318 Dep {
319 git: "https://github.com/x/lib".into(),
320 tag: Some("v1.0.0".into()),
321 rev: None,
322 branch: None,
323 entry: None,
324 },
325 );
326 m
327 },
328 };
329
330 let serialized = toml::to_string(&original).unwrap();
331 let deserialized: Manifest = toml::from_str(&serialized).unwrap();
332 assert_eq!(original, deserialized);
333 }
334
335 #[test]
338 fn all_three_ref_fields_is_validation_error() {
339 let toml = r#"
340[package]
341name = "my-app"
342version = "0.1.0"
343
344[deps.oops]
345git = "https://github.com/x/oops"
346tag = "v1.0.0"
347rev = "abc123"
348branch = "main"
349"#;
350 let f = temp_manifest(toml);
351 let err = Manifest::from_path(f.path()).unwrap_err();
352 assert!(matches!(err, PkgError::Validation { .. }));
353 }
354
355 #[test]
358 fn unknown_field_in_package_is_rejected() {
359 let toml = r#"
360[package]
361name = "my-app"
362version = "0.1.0"
363unknown = "should-fail"
364"#;
365 let f = temp_manifest(toml);
366 let err = Manifest::from_path(f.path()).unwrap_err();
367 assert!(
368 matches!(err, PkgError::ManifestParse { .. }),
369 "expected ManifestParse error for unknown field, got: {err}"
370 );
371 }
372}