1mod api;
2mod id;
3mod stack;
4mod target;
5mod version;
6
7use crate::generic::GenericMetadata;
8use crate::sbom::SbomFormat;
9pub use api::*;
10pub use id::*;
11use serde::Deserialize;
12pub use stack::*;
13use std::collections::HashSet;
14pub use target::*;
15pub use version::*;
16
17#[derive(Deserialize, Debug)]
59#[serde(untagged)]
60pub enum BuildpackDescriptor<BM = GenericMetadata> {
61 Component(ComponentBuildpackDescriptor<BM>),
62 Composite(CompositeBuildpackDescriptor<BM>),
63}
64
65impl<BM> BuildpackDescriptor<BM> {
66 pub fn buildpack(&self) -> &Buildpack {
67 match self {
68 BuildpackDescriptor::Component(descriptor) => &descriptor.buildpack,
69 BuildpackDescriptor::Composite(descriptor) => &descriptor.buildpack,
70 }
71 }
72}
73
74#[derive(Deserialize, Debug)]
120#[serde(deny_unknown_fields)]
121pub struct ComponentBuildpackDescriptor<BM = GenericMetadata> {
122 pub api: BuildpackApi,
123 pub buildpack: Buildpack,
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
125 pub stacks: Vec<Stack>,
126 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 pub targets: Vec<BuildpackTarget>,
128 pub metadata: BM,
129 }
134
135#[derive(Deserialize, Debug)]
176#[serde(deny_unknown_fields)]
177pub struct CompositeBuildpackDescriptor<BM = GenericMetadata> {
178 pub api: BuildpackApi,
179 pub buildpack: Buildpack,
180 pub order: Vec<Order>,
181 pub metadata: BM,
182 }
187
188#[derive(Deserialize, Debug)]
189#[serde(deny_unknown_fields)]
190pub struct Buildpack {
191 pub id: BuildpackId,
192 pub name: Option<String>,
193 pub version: BuildpackVersion,
194 pub homepage: Option<String>,
195 #[serde(default, rename = "clear-env")]
196 pub clear_env: bool,
197 pub description: Option<String>,
198 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 pub keywords: Vec<String>,
200 #[serde(default, skip_serializing_if = "Vec::is_empty")]
201 pub licenses: Vec<License>,
202 #[serde(
203 default,
204 rename = "sbom-formats",
205 skip_serializing_if = "HashSet::is_empty"
206 )]
207 pub sbom_formats: HashSet<SbomFormat>,
208}
209
210#[derive(Deserialize, Debug, Eq, PartialEq)]
211#[serde(deny_unknown_fields)]
212pub struct License {
213 pub r#type: Option<String>,
214 pub uri: Option<String>,
215}
216
217#[derive(Deserialize, Debug, Eq, PartialEq)]
218#[serde(deny_unknown_fields)]
219pub struct Order {
220 pub group: Vec<Group>,
221}
222
223#[derive(Deserialize, Debug, Eq, PartialEq)]
224#[serde(deny_unknown_fields)]
225pub struct Group {
226 pub id: BuildpackId,
227 pub version: BuildpackVersion,
228 #[serde(default)]
229 pub optional: bool,
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 #[allow(clippy::too_many_lines)]
238 fn deserialize_component_buildpack() {
239 let toml_str = r#"
240api = "0.10"
241
242[buildpack]
243id = "foo/bar"
244name = "Bar Buildpack"
245version = "0.0.1"
246homepage = "https://example.tld"
247clear-env = true
248description = "A buildpack for Foo Bar"
249keywords = ["foo", "bar"]
250# Duplication of the Syft entry is intentional!
251sbom-formats = ["application/vnd.cyclonedx+json", "application/spdx+json", "application/vnd.syft+json", "application/vnd.syft+json"]
252
253[[buildpack.licenses]]
254type = "BSD-3-Clause"
255
256[[buildpack.licenses]]
257type = "Custom license with type and URI"
258uri = "https://example.tld/my-license"
259
260[[buildpack.licenses]]
261uri = "https://example.tld/my-license"
262
263[[stacks]]
264id = "heroku-20"
265
266[[stacks]]
267id = "io.buildpacks.stacks.bionic"
268mixins = []
269
270[[stacks]]
271id = "io.buildpacks.stacks.focal"
272mixins = ["build:jq", "wget"]
273
274[[stacks]]
275id = "*"
276
277[[targets]]
278os = "linux"
279arch = "amd64"
280[[targets.distros]]
281name = "ubuntu"
282version = "18.04"
283
284[[targets]]
285os = "linux"
286arch = "arm"
287variant = "v8"
288
289[[targets]]
290os = "windows"
291arch = "amd64"
292
293[[targets]]
294
295[metadata]
296checksum = "abc123"
297 "#;
298
299 let buildpack_descriptor =
300 toml::from_str::<ComponentBuildpackDescriptor>(toml_str).unwrap();
301
302 assert_eq!(
303 buildpack_descriptor.api,
304 BuildpackApi {
305 major: 0,
306 minor: 10
307 }
308 );
309 assert_eq!(
310 buildpack_descriptor.buildpack.id,
311 "foo/bar".parse().unwrap()
312 );
313 assert_eq!(
314 buildpack_descriptor.buildpack.name,
315 Some(String::from("Bar Buildpack"))
316 );
317 assert_eq!(
318 buildpack_descriptor.buildpack.version,
319 BuildpackVersion::new(0, 0, 1)
320 );
321 assert_eq!(
322 buildpack_descriptor.buildpack.homepage,
323 Some(String::from("https://example.tld"))
324 );
325 assert!(buildpack_descriptor.buildpack.clear_env);
326 assert_eq!(
327 buildpack_descriptor.buildpack.description,
328 Some(String::from("A buildpack for Foo Bar"))
329 );
330 assert_eq!(
331 buildpack_descriptor.buildpack.keywords,
332 [String::from("foo"), String::from("bar")]
333 );
334 assert_eq!(
335 buildpack_descriptor.buildpack.licenses,
336 [
337 License {
338 r#type: Some(String::from("BSD-3-Clause")),
339 uri: None
340 },
341 License {
342 r#type: Some(String::from("Custom license with type and URI")),
343 uri: Some(String::from("https://example.tld/my-license"))
344 },
345 License {
346 r#type: None,
347 uri: Some(String::from("https://example.tld/my-license"))
348 }
349 ]
350 );
351 assert_eq!(
352 buildpack_descriptor.buildpack.sbom_formats,
353 HashSet::from([
354 SbomFormat::SyftJson,
355 SbomFormat::CycloneDxJson,
356 SbomFormat::SpdxJson
357 ])
358 );
359 assert_eq!(
360 buildpack_descriptor.stacks,
361 [
362 Stack {
363 id: String::from("heroku-20"),
364 mixins: Vec::new(),
365 },
366 Stack {
367 id: String::from("io.buildpacks.stacks.bionic"),
368 mixins: Vec::new(),
369 },
370 Stack {
371 id: String::from("io.buildpacks.stacks.focal"),
372 mixins: vec![String::from("build:jq"), String::from("wget")]
373 },
374 Stack {
375 id: String::from("*"),
376 mixins: Vec::new()
377 }
378 ]
379 );
380 assert_eq!(
381 buildpack_descriptor.targets,
382 [
383 BuildpackTarget {
384 os: Some(String::from("linux")),
385 arch: Some(String::from("amd64")),
386 variant: None,
387 distros: vec![Distro {
388 name: String::from("ubuntu"),
389 version: String::from("18.04"),
390 }],
391 },
392 BuildpackTarget {
393 os: Some(String::from("linux")),
394 arch: Some(String::from("arm")),
395 variant: Some(String::from("v8")),
396 distros: Vec::new(),
397 },
398 BuildpackTarget {
399 os: Some(String::from("windows")),
400 arch: Some(String::from("amd64")),
401 variant: None,
402 distros: Vec::new(),
403 },
404 BuildpackTarget {
405 os: None,
406 arch: None,
407 variant: None,
408 distros: Vec::new()
409 }
410 ]
411 );
412 assert_eq!(
413 buildpack_descriptor.metadata.unwrap().get("checksum"),
414 Some(&toml::value::Value::try_from("abc123").unwrap())
415 );
416 }
417
418 #[test]
419 fn deserialize_composite_buildpack() {
420 let toml_str = r#"
421api = "0.10"
422
423[buildpack]
424id = "foo/bar"
425name = "Bar Buildpack"
426version = "0.0.1"
427homepage = "https://example.tld"
428clear-env = true
429description = "A buildpack for Foo Bar"
430keywords = ["foo", "bar"]
431
432[[buildpack.licenses]]
433type = "BSD-3-Clause"
434
435[[buildpack.licenses]]
436type = "Custom license with type and URI"
437uri = "https://example.tld/my-license"
438
439[[buildpack.licenses]]
440uri = "https://example.tld/my-license"
441
442[[order]]
443
444[[order.group]]
445id = "foo/bar"
446version = "0.0.1"
447
448[[order.group]]
449id = "foo/baz"
450version = "0.1.0"
451optional = true
452
453[metadata]
454checksum = "abc123"
455 "#;
456
457 let buildpack_descriptor =
458 toml::from_str::<CompositeBuildpackDescriptor>(toml_str).unwrap();
459
460 assert_eq!(
461 buildpack_descriptor.api,
462 BuildpackApi {
463 major: 0,
464 minor: 10
465 }
466 );
467 assert_eq!(
468 buildpack_descriptor.buildpack.id,
469 "foo/bar".parse().unwrap()
470 );
471 assert_eq!(
472 buildpack_descriptor.buildpack.name,
473 Some(String::from("Bar Buildpack"))
474 );
475 assert_eq!(
476 buildpack_descriptor.buildpack.version,
477 BuildpackVersion::new(0, 0, 1)
478 );
479 assert_eq!(
480 buildpack_descriptor.buildpack.homepage,
481 Some(String::from("https://example.tld"))
482 );
483 assert!(buildpack_descriptor.buildpack.clear_env);
484 assert_eq!(
485 buildpack_descriptor.buildpack.description,
486 Some(String::from("A buildpack for Foo Bar"))
487 );
488 assert_eq!(
489 buildpack_descriptor.buildpack.keywords,
490 [String::from("foo"), String::from("bar")]
491 );
492 assert_eq!(
493 buildpack_descriptor.buildpack.licenses,
494 [
495 License {
496 r#type: Some(String::from("BSD-3-Clause")),
497 uri: None
498 },
499 License {
500 r#type: Some(String::from("Custom license with type and URI")),
501 uri: Some(String::from("https://example.tld/my-license"))
502 },
503 License {
504 r#type: None,
505 uri: Some(String::from("https://example.tld/my-license"))
506 }
507 ]
508 );
509 assert_eq!(
510 buildpack_descriptor.order,
511 [Order {
512 group: vec![
513 Group {
514 id: "foo/bar".parse().unwrap(),
515 version: BuildpackVersion::new(0, 0, 1),
516 optional: false
517 },
518 Group {
519 id: "foo/baz".parse().unwrap(),
520 version: BuildpackVersion::new(0, 1, 0),
521 optional: true
522 }
523 ]
524 }]
525 );
526 assert_eq!(
527 buildpack_descriptor.metadata.unwrap().get("checksum"),
528 Some(&toml::value::Value::try_from("abc123").unwrap())
529 );
530 }
531
532 #[test]
533 fn deserialize_minimal_component_buildpack() {
534 let toml_str = r#"
535api = "0.10"
536
537[buildpack]
538id = "foo/bar"
539version = "0.0.1"
540 "#;
541
542 let buildpack_descriptor =
543 toml::from_str::<ComponentBuildpackDescriptor>(toml_str).unwrap();
544
545 assert_eq!(
546 buildpack_descriptor.api,
547 BuildpackApi {
548 major: 0,
549 minor: 10
550 }
551 );
552 assert_eq!(
553 buildpack_descriptor.buildpack.id,
554 "foo/bar".parse().unwrap()
555 );
556 assert_eq!(buildpack_descriptor.buildpack.name, None);
557 assert_eq!(
558 buildpack_descriptor.buildpack.version,
559 BuildpackVersion::new(0, 0, 1)
560 );
561 assert_eq!(buildpack_descriptor.buildpack.homepage, None);
562 assert!(!buildpack_descriptor.buildpack.clear_env);
563 assert_eq!(buildpack_descriptor.buildpack.description, None);
564 assert_eq!(
565 buildpack_descriptor.buildpack.keywords,
566 Vec::<String>::new()
567 );
568 assert_eq!(buildpack_descriptor.buildpack.licenses, Vec::new());
569 assert_eq!(buildpack_descriptor.buildpack.sbom_formats, HashSet::new());
570 assert_eq!(buildpack_descriptor.stacks, []);
571 assert_eq!(buildpack_descriptor.targets, []);
572 assert_eq!(buildpack_descriptor.metadata, None);
573 }
574
575 #[test]
576 fn deserialize_minimal_composite_buildpack() {
577 let toml_str = r#"
578api = "0.10"
579
580[buildpack]
581id = "foo/bar"
582version = "0.0.1"
583
584[[order]]
585
586[[order.group]]
587id = "foo/bar"
588version = "0.0.1"
589"#;
590
591 let buildpack_descriptor =
592 toml::from_str::<CompositeBuildpackDescriptor>(toml_str).unwrap();
593
594 assert_eq!(
595 buildpack_descriptor.api,
596 BuildpackApi {
597 major: 0,
598 minor: 10
599 }
600 );
601 assert_eq!(
602 buildpack_descriptor.buildpack.id,
603 "foo/bar".parse().unwrap()
604 );
605 assert_eq!(buildpack_descriptor.buildpack.name, None);
606 assert_eq!(
607 buildpack_descriptor.buildpack.version,
608 BuildpackVersion::new(0, 0, 1)
609 );
610 assert_eq!(buildpack_descriptor.buildpack.homepage, None);
611 assert!(!buildpack_descriptor.buildpack.clear_env);
612 assert_eq!(buildpack_descriptor.buildpack.description, None);
613 assert_eq!(
614 buildpack_descriptor.buildpack.keywords,
615 Vec::<String>::new()
616 );
617 assert_eq!(buildpack_descriptor.buildpack.licenses, Vec::new());
618 assert_eq!(
619 buildpack_descriptor.order,
620 [Order {
621 group: vec![Group {
622 id: "foo/bar".parse().unwrap(),
623 version: BuildpackVersion::new(0, 0, 1),
624 optional: false
625 }]
626 }]
627 );
628 assert_eq!(buildpack_descriptor.metadata, None);
629 }
630
631 #[test]
632 fn deserialize_buildpackdescriptor_component() {
633 let toml_str = r#"
634api = "0.10"
635
636[buildpack]
637id = "foo/bar"
638version = "0.0.1"
639 "#;
640
641 let buildpack_descriptor = toml::from_str::<BuildpackDescriptor>(toml_str).unwrap();
642 assert!(matches!(
643 buildpack_descriptor,
644 BuildpackDescriptor::Component(_)
645 ));
646 }
647
648 #[test]
649 fn deserialize_buildpackdescriptor_composite() {
650 let toml_str = r#"
651api = "0.10"
652
653[buildpack]
654id = "foo/bar"
655version = "0.0.1"
656
657[[order]]
658
659[[order.group]]
660id = "foo/baz"
661version = "0.0.1"
662 "#;
663
664 let buildpack_descriptor = toml::from_str::<BuildpackDescriptor>(toml_str).unwrap();
665 assert!(matches!(
666 buildpack_descriptor,
667 BuildpackDescriptor::Composite(_)
668 ));
669 }
670
671 #[test]
672 fn reject_buildpack_with_both_targets_and_order() {
673 let toml_str = r#"
674api = "0.10"
675
676[buildpack]
677id = "foo/bar"
678version = "0.0.1"
679
680[[targets]]
681os = "linux"
682
683[[order]]
684
685[[order.group]]
686id = "foo/baz"
687version = "0.0.1"
688"#;
689
690 let err = toml::from_str::<BuildpackDescriptor>(toml_str).unwrap_err();
691 assert_eq!(
692 err.to_string(),
693 "data did not match any variant of untagged enum BuildpackDescriptor\n"
694 );
695
696 let err = toml::from_str::<ComponentBuildpackDescriptor>(toml_str).unwrap_err();
697 assert!(err.to_string().contains("unknown field `order`"));
698
699 let err = toml::from_str::<CompositeBuildpackDescriptor>(toml_str).unwrap_err();
700 assert!(err.to_string().contains("unknown field `targets`"));
701 }
702}