1use super::error::FetchError;
4use super::git::Git;
5#[cfg(feature = "tar")]
6use super::tar::Tar;
7
8use derive_more::Deref;
9
10pub type SourceName = String;
12
13#[derive(Debug, thiserror::Error)]
15pub enum SourceParseError {
16 #[error("expected a valid source type for source '{source_name}': expected one of: {known}", known = SOURCE_VARIANTS.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))]
18 VariantUnknown {
19 source_name: SourceName,
21 },
22
23 #[error("multiple source types for source '{source_name}': expected exactly one of: {known}", known = SOURCE_VARIANTS.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))]
25 VariantMultiple {
26 source_name: SourceName,
28 },
29
30 #[error("source '{source_name}' has type '{variant}' but needs disabled feature '{requires}'")]
32 VariantDisabled {
33 source_name: SourceName,
35 variant: String,
37 requires: String,
39 },
40
41 #[error("expected value '{name}' to be a toml table")]
43 ValueNotTable {
44 name: String,
46 },
47
48 #[error("required table 'package.metadata.fetch-source' not found in string")]
50 SourceTableNotFound,
51
52 #[error(transparent)]
54 TomlInvalid(#[from] toml::de::Error),
55
56 #[error(transparent)]
58 JsonInvalid(#[from] serde_json::Error),
59}
60
61pub type FetchResult<T> = Result<T, crate::FetchError>;
63
64#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
68pub struct Artefact {
69 source: Source,
74 path: std::path::PathBuf,
76}
77
78impl Artefact {
79 pub fn path(&self) -> &std::path::Path {
81 &self.path
82 }
83}
84
85impl AsRef<std::path::Path> for Artefact {
86 fn as_ref(&self) -> &std::path::Path {
87 &self.path
88 }
89}
90
91impl AsRef<Source> for Artefact {
92 fn as_ref(&self) -> &Source {
93 &self.source
94 }
95}
96
97impl AsRef<Source> for Source {
98 fn as_ref(&self) -> &Source {
99 self
100 }
101}
102
103#[derive(Debug, PartialEq, Eq, Hash)]
105enum SourceVariant {
106 Tar,
107 Git,
108}
109
110const SOURCE_VARIANTS: &[SourceVariant] = &[SourceVariant::Tar, SourceVariant::Git];
111
112impl std::fmt::Display for SourceVariant {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self {
115 Self::Tar => write!(f, "tar"),
116 Self::Git => write!(f, "git"),
117 }
118 }
119}
120
121impl SourceVariant {
122 fn from<S: AsRef<str>>(name: S) -> Option<Self> {
123 match name.as_ref() {
124 "tar" => Some(Self::Tar),
125 "git" => Some(Self::Git),
126 _ => None,
127 }
128 }
129
130 fn is_enabled(&self) -> bool {
131 match self {
132 Self::Tar => cfg!(feature = "tar"),
133 Self::Git => true,
134 }
135 }
136
137 fn feature(&self) -> Option<&'static str> {
138 match self {
139 Self::Tar => Some("tar"),
140 Self::Git => None,
141 }
142 }
143}
144
145#[derive(
147 Debug,
148 Default,
149 serde::Deserialize,
150 serde::Serialize,
151 PartialEq,
152 Eq,
153 PartialOrd,
154 Ord,
155 Clone,
156 Deref,
157)]
158pub struct Digest(String);
159
160impl AsRef<str> for Digest {
161 fn as_ref(&self) -> &str {
162 self.0.as_ref()
163 }
164}
165
166#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
168#[serde(untagged)]
169pub enum Source {
170 #[cfg(feature = "tar")]
171 #[serde(rename = "tar")]
172 Tar(Tar),
174 #[serde(rename = "git")]
175 Git(Git),
177}
178
179impl std::fmt::Display for Source {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 match self {
182 #[cfg(feature = "tar")]
183 Source::Tar(tar) => write!(f, "tar source: {tar:?}"),
184 Source::Git(git) => write!(f, "git source: {git:?}"),
185 }
186 }
187}
188
189impl Source {
190 pub fn digest<S: AsRef<Self>>(value: S) -> Digest {
192 let json = serde_json::to_string(value.as_ref())
193 .expect("Serialisation of Source should never fail");
194 Digest(sha256::digest(json))
195 }
196
197 pub fn fetch<P: AsRef<std::path::Path>>(self, dir: P) -> FetchResult<Artefact> {
199 let dest = dir.as_ref();
200 let result = match self {
201 #[cfg(feature = "tar")]
202 Source::Tar(ref tar) => tar.fetch(dest),
203 Source::Git(ref git) => git.fetch(dest),
204 };
205 match result {
206 Ok(path) => Ok(Artefact { source: self, path }),
207 Err(err) => Err(FetchError::new(err, self)),
208 }
209 }
210
211 pub fn as_path_component<S: AsRef<str>>(name: S) -> std::path::PathBuf {
213 std::path::PathBuf::from_iter(name.as_ref().split("::"))
214 }
215
216 fn enforce_one_valid_variant<S: ToString>(
217 name: S,
218 source: &toml::Table,
219 ) -> Result<SourceVariant, SourceParseError> {
220 let mut detected_variant = None;
221 for key in source.keys() {
222 if let Some(variant) = SourceVariant::from(key) {
223 if detected_variant.is_some() {
224 return Err(SourceParseError::VariantMultiple {
225 source_name: name.to_string(),
226 });
227 }
228 if !variant.is_enabled() {
229 return Err(SourceParseError::VariantDisabled {
230 source_name: name.to_string(),
231 variant: variant.to_string(),
232 requires: variant.feature().unwrap_or("?").to_string(),
233 });
234 }
235 detected_variant = Some(variant);
236 }
237 }
238 detected_variant.ok_or(SourceParseError::VariantUnknown {
239 source_name: name.to_string(),
240 })
241 }
242
243 pub fn parse<S: ToString>(name: S, source: toml::Table) -> Result<Self, SourceParseError> {
246 Self::enforce_one_valid_variant(name, &source)?;
247 Ok(toml::Value::Table(source).try_into::<Self>()?)
248 }
249}
250
251pub type SourcesTable = std::collections::HashMap<SourceName, Source>;
253
254pub fn try_parse(table: &toml::Table) -> Result<SourcesTable, SourceParseError> {
256 table
257 .iter()
258 .map(|(k, v)| match v.as_table() {
259 Some(t) => Source::parse(k, t.to_owned()).map(|s| (k.to_owned(), s)),
260 None => Err(SourceParseError::ValueNotTable { name: k.to_owned() }),
261 })
262 .collect()
263}
264
265pub fn try_parse_toml<S: AsRef<str>>(toml_str: S) -> Result<SourcesTable, SourceParseError> {
268 let table = toml_str.as_ref().parse::<toml::Table>()?;
269 let sources_table = table
270 .get("package")
271 .and_then(|v| v.get("metadata"))
272 .and_then(|v| v.get("fetch-source"))
273 .and_then(|v| v.as_table())
274 .ok_or(SourceParseError::SourceTableNotFound)?;
275 try_parse(sources_table)
276}
277
278#[cfg(test)]
279use SourceParseError::*;
280
281#[cfg(test)]
282mod test_parsing_single_source_value {
283 use super::*;
284 use crate::build_from_json;
285
286 #[test]
287 fn parse_good_git_source() {
288 let source = build_from_json! {
289 Source,
290 "git": "git@github.com:foo/bar.git"
291 };
292 assert!(source.is_ok());
293 }
294
295 #[cfg(feature = "tar")]
296 #[test]
297 fn parse_good_tar_source() {
298 let source = build_from_json! {
299 Source,
300 "tar": "https://example.com/foo.tar.gz"
301 };
302 assert!(source.is_ok());
303 }
304
305 #[cfg(not(feature = "tar"))]
306 #[test]
307 fn parse_good_tar_source_fails_when_feature_disabled() {
308 let source = build_from_json! {
309 Source,
310 "tar": "https://example.com/foo.tar.gz"
311 };
312 assert!(
313 matches!(source, Err(VariantDisabled { source_name: _, variant, requires })
314 if variant == "tar" && requires == "tar"
315 )
316 );
317 }
318
319 #[test]
320 fn parse_multiple_types_fails() {
321 let source = Source::parse(
323 "src",
324 toml::toml! {
325 tar = "https://example.com/foo.tar.gz"
326 git = "git@github.com:foo/bar.git"
327 },
328 );
329 assert!(matches!(source, Err(VariantMultiple { source_name })
330 if source_name == "src"
331 ));
332 }
333
334 #[test]
335 fn parse_missing_type_fails() {
336 let source = Source::parse(
338 "src",
339 toml::toml! {
340 foo = "git@github.com:foo/bar.git"
341 },
342 );
343 assert!(matches!(source, Err(VariantUnknown { source_name })
344 if source_name == "src"
345 ));
346 }
347}
348
349#[cfg(test)]
350mod test_parsing_sources_table_failure_modes {
351 use super::*;
352
353 #[test]
354 fn parse_invalid_toml_str_fails() {
355 let document = "this is not a valid toml document :( uh-oh!";
356 let result = try_parse_toml(document);
357 assert!(matches!(result, Err(TomlInvalid(_))));
358 }
359
360 #[test]
361 fn parse_doc_missing_sources_table_fails() {
362 let document = r#"
363 [package]
364 name = "my_fun_test_suite"
365
366 [package.metadata.wrong-name]
367 foo = { git = "git@github.com:foo/bar.git" }
368 bar = { tar = "https://example.com/foo.tar.gz" }
369 "#;
370 assert!(matches!(try_parse_toml(document), Err(SourceTableNotFound)));
371 }
372
373 #[test]
374 fn parse_doc_source_value_not_a_table_fails() {
375 let document = r#"
376 [package]
377 name = "my_fun_test_suite"
378
379 [package.metadata.fetch-source]
380 not-a-table = "actually a string"
381 "#;
382 assert!(matches!(
383 try_parse_toml(document),
384 Err(ValueNotTable { name }) if name == "not-a-table"
385 ));
386 }
387
388 #[cfg(not(feature = "tar"))]
389 #[test]
390 fn parse_doc_source_variant_disabled_fails() {
391 let document = r#"
392 [package]
393 name = "my_fun_test_suite"
394
395 [package.metadata.fetch-source]
396 bar = { tar = "https://example.com/foo.tar.gz" }
397 "#;
398 assert!(matches!(
399 try_parse_toml(document),
400 Err(VariantDisabled {
401 source_name,
402 variant,
403 requires,
404 }) if source_name == "bar" && variant == "tar" && requires == "tar"
405 ));
406 }
407
408 #[test]
409 fn parse_doc_source_multiple_variants_fails() {
410 let document = r#"
411 [package]
412 name = "my_fun_test_suite"
413
414 [package.metadata.fetch-source]
415 bar = { tar = "https://example.com/foo.tar.gz", git = "git@github.com:foo/bar.git" }
416 "#;
417 assert!(matches!(
418 try_parse_toml(document),
419 Err(VariantMultiple { source_name }) if source_name == "bar"
420 ));
421 }
422
423 #[test]
424 fn parse_doc_source_unknown_variant_fails() {
425 let document = r#"
426 [package]
427 name = "my_fun_test_suite"
428
429 [package.metadata.fetch-source]
430 bar = { zim = "https://example.com/foo.tar.gz" }
431 "#;
432 assert!(matches!(
433 try_parse_toml(document),
434 Err(VariantUnknown { source_name }) if source_name == "bar"
435 ));
436 }
437}