1use crate::deserialize::*;
2#[cfg(feature = "json_schema")]
3use crate::json_schema::optional_string_or_number_schema;
4#[cfg(feature = "json_schema")]
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::{collections::HashMap, path::PathBuf};
8use struct_patch::Patch;
9use url::Url;
10
11#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
13#[serde(rename_all = "camelCase")]
14#[patch(
15 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
16 attribute(serde(default, rename_all = "camelCase"))
18)]
19#[cfg_attr(
20 feature = "json_schema",
21 patch(
22 attribute(derive(JsonSchema)),
23 attribute(schemars(
24 title = "Dofigen",
25 rename = "Dofigen",
26 extend("$id" = "https://json.schemastore.org/dofigen.json"),
27 description = "Dofigen is a Dockerfile generator using a simplified description in YAML or JSON format"
28 ))
29 )
30)]
31pub struct Dofigen {
32 #[patch(name = "VecPatch<String>")]
35 #[serde(skip_serializing_if = "Vec::is_empty")]
36 pub context: Vec<String>,
37
38 #[patch(name = "VecPatch<String>")]
41 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "ignores"))))]
42 #[serde(skip_serializing_if = "Vec::is_empty")]
43 pub ignore: Vec<String>,
44
45 #[patch(name = "HashMapPatch<String, String>")]
48 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "globalArgs"))))]
49 #[serde(skip_serializing_if = "HashMap::is_empty")]
50 pub global_arg: HashMap<String, String>,
51
52 #[patch(name = "HashMapDeepPatch<String, StagePatch>")]
54 #[serde(skip_serializing_if = "HashMap::is_empty")]
55 pub builders: HashMap<String, Stage>,
56
57 #[patch(name = "StagePatch", attribute(serde(flatten)))]
59 #[serde(flatten)]
60 pub stage: Stage,
61
62 #[patch(name = "VecPatch<String>")]
65 #[serde(skip_serializing_if = "Vec::is_empty")]
66 pub entrypoint: Vec<String>,
67
68 #[patch(name = "VecPatch<String>")]
71 #[serde(skip_serializing_if = "Vec::is_empty")]
72 pub cmd: Vec<String>,
73
74 #[patch(name = "VecPatch<String>")]
77 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "volumes"))))]
78 #[serde(skip_serializing_if = "Vec::is_empty")]
79 pub volume: Vec<String>,
80
81 #[cfg_attr(
84 feature = "permissive",
85 patch(name = "VecDeepPatch<Port, ParsableStruct<PortPatch>>")
86 )]
87 #[cfg_attr(
88 not(feature = "permissive"),
89 patch(name = "VecDeepPatch<Port, PortPatch>")
90 )]
91 #[cfg_attr(
92 not(feature = "strict"),
93 patch(attribute(serde(alias = "port", alias = "ports")))
94 )]
95 #[serde(skip_serializing_if = "Vec::is_empty")]
96 pub expose: Vec<Port>,
97
98 #[patch(name = "Option<HealthcheckPatch>")]
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub healthcheck: Option<Healthcheck>,
103}
104
105#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
107#[patch(
108 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
109 attribute(serde(default)),
111)]
112#[cfg_attr(
113 feature = "json_schema",
114 patch(
115 attribute(derive(JsonSchema)),
116 attribute(schemars(title = "Stage", rename = "Stage"))
117 )
118)]
119pub struct Stage {
120 #[serde(flatten, skip_serializing_if = "FromContext::is_empty")]
123 #[patch(name = "FromContextPatch", attribute(serde(flatten, default)))]
124 pub from: FromContext,
125
126 #[cfg_attr(not(feature = "strict"), patch(name = "NestedMap<String>"))]
129 #[cfg_attr(feature = "strict", patch(name = "HashMapPatch<String, String>"))]
130 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "labels"))))]
131 #[serde(skip_serializing_if = "HashMap::is_empty")]
132 pub label: HashMap<String, String>,
133
134 #[cfg_attr(
137 feature = "permissive",
138 patch(name = "Option<ParsableStruct<UserPatch>>")
139 )]
140 #[cfg_attr(not(feature = "permissive"), patch(name = "Option<UserPatch>"))]
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub user: Option<User>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
147 pub workdir: Option<String>,
148
149 #[patch(name = "HashMapPatch<String, String>")]
152 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "args"))))]
153 #[serde(skip_serializing_if = "HashMap::is_empty")]
154 pub arg: HashMap<String, String>,
155
156 #[patch(name = "HashMapPatch<String, String>")]
159 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "envs"))))]
160 #[serde(skip_serializing_if = "HashMap::is_empty")]
161 pub env: HashMap<String, String>,
162
163 #[cfg_attr(
166 not(feature = "strict"),
167 patch(attribute(serde(
168 alias = "add",
169 alias = "adds",
170 alias = "artifact",
171 alias = "artifacts"
172 )))
173 )]
174 #[cfg_attr(
175 feature = "permissive",
176 patch(name = "VecDeepPatch<CopyResource, ParsableStruct<CopyResourcePatch>>")
177 )]
178 #[cfg_attr(
179 not(feature = "permissive"),
180 patch(name = "VecDeepPatch<CopyResource, CopyResourcePatch>")
181 )]
182 #[serde(skip_serializing_if = "Vec::is_empty")]
183 pub copy: Vec<CopyResource>,
184
185 #[patch(name = "Option<RunPatch>")]
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub root: Option<Run>,
189
190 #[patch(name = "RunPatch", attribute(serde(flatten)))]
193 #[serde(flatten)]
194 pub run: Run,
195}
196
197#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
199#[patch(
200 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
201 attribute(serde(default)),
203)]
204#[cfg_attr(
205 feature = "json_schema",
206 patch(
207 attribute(derive(JsonSchema)),
208 attribute(schemars(title = "Run", rename = "Run"))
209 )
210)]
211pub struct Run {
212 #[patch(name = "VecPatch<String>")]
214 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "script"))))]
215 #[serde(skip_serializing_if = "Vec::is_empty")]
216 pub run: Vec<String>,
217
218 #[patch(name = "VecPatch<String>")]
221 #[serde(skip_serializing_if = "Vec::is_empty")]
222 pub shell: Vec<String>,
223
224 #[cfg_attr(
227 feature = "permissive",
228 patch(name = "VecDeepPatch<Cache, ParsableStruct<CachePatch>>")
229 )]
230 #[cfg_attr(
231 not(feature = "permissive"),
232 patch(name = "VecDeepPatch<Cache, CachePatch>")
233 )]
234 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "caches"))))]
235 #[serde(skip_serializing_if = "Vec::is_empty")]
236 pub cache: Vec<Cache>,
237
238 #[cfg_attr(
242 feature = "permissive",
243 patch(name = "VecDeepPatch<Bind, ParsableStruct<BindPatch>>")
244 )]
245 #[cfg_attr(
246 not(feature = "permissive"),
247 patch(name = "VecDeepPatch<Bind, BindPatch>")
248 )]
249 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "binds"))))]
250 #[serde(skip_serializing_if = "Vec::is_empty")]
251 pub bind: Vec<Bind>,
252}
253
254#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
257#[patch(
258 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
259 attribute(serde(default))
260)]
261#[cfg_attr(
262 feature = "json_schema",
263 patch(
264 attribute(derive(JsonSchema)),
265 attribute(schemars(title = "Cache", rename = "Cache"))
266 )
267)]
268pub struct Cache {
269 #[serde(skip_serializing_if = "Option::is_none")]
272 pub id: Option<String>,
273
274 #[cfg_attr(
276 not(feature = "strict"),
277 patch(attribute(serde(alias = "dst", alias = "destination")))
278 )]
279 pub target: String,
280
281 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "ro"))))]
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub readonly: Option<bool>,
285
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub sharing: Option<CacheSharing>,
289
290 #[serde(flatten, skip_serializing_if = "FromContext::is_empty")]
292 #[patch(name = "FromContextPatch", attribute(serde(flatten)))]
293 pub from: FromContext,
294
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub source: Option<String>,
298
299 #[cfg_attr(
301 feature = "permissive",
302 patch(attribute(serde(
303 deserialize_with = "deserialize_from_optional_string_or_number",
304 default
305 )))
306 )]
307 #[serde(skip_serializing_if = "Option::is_none")]
308 #[cfg_attr(
309 feature = "json_schema",
310 patch(attribute(schemars(schema_with = "optional_string_or_number_schema")))
311 )]
312 pub chmod: Option<String>,
313
314 #[patch(name = "Option<UserPatch>")]
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub chown: Option<User>,
318}
319
320#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
323#[patch(
324 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
325 attribute(serde(default))
326)]
327#[cfg_attr(
328 feature = "json_schema",
329 patch(
330 attribute(derive(JsonSchema)),
331 attribute(schemars(title = "Bind", rename = "Bind"))
332 )
333)]
334pub struct Bind {
335 pub target: String,
337
338 #[serde(flatten, skip_serializing_if = "FromContext::is_empty")]
340 #[patch(name = "FromContextPatch", attribute(serde(flatten)))]
341 pub from: FromContext,
342
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub source: Option<String>,
346
347 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "rw"))))]
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub readwrite: Option<bool>,
351}
352
353#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
356#[patch(
357 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
358 attribute(serde(deny_unknown_fields, default))
359)]
360#[cfg_attr(
361 feature = "json_schema",
362 patch(
363 attribute(derive(JsonSchema)),
364 attribute(schemars(title = "Healthcheck", rename = "Healthcheck"))
365 )
366)]
367pub struct Healthcheck {
368 pub cmd: String,
370
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub interval: Option<String>,
374
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub timeout: Option<String>,
378
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub start: Option<String>,
382
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub retries: Option<u16>,
386}
387
388#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch, Hash, Eq, PartialOrd)]
390#[patch(
391 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
392 attribute(serde(deny_unknown_fields, default))
393)]
394#[cfg_attr(
395 feature = "json_schema",
396 patch(
397 attribute(derive(JsonSchema)),
398 attribute(schemars(title = "ImageName", rename = "ImageName"))
399 )
400)]
401pub struct ImageName {
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub host: Option<String>,
405
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub port: Option<u16>,
409
410 pub path: String,
412
413 #[serde(flatten, skip_serializing_if = "Option::is_none")]
415 #[patch(attribute(serde(flatten)))]
416 pub version: Option<ImageVersion>,
417
418 #[serde(skip_serializing_if = "Option::is_none")]
423 pub platform: Option<String>,
424}
425
426#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
429#[patch(
430 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
431 attribute(serde(deny_unknown_fields, default))
432)]
433#[cfg_attr(
434 feature = "json_schema",
435 patch(
436 attribute(derive(JsonSchema)),
437 attribute(schemars(title = "Copy", rename = "Copy"))
438 )
439)]
440pub struct Copy {
441 #[serde(flatten, skip_serializing_if = "FromContext::is_empty")]
444 #[patch(name = "FromContextPatch", attribute(serde(flatten)))]
445 pub from: FromContext,
446
447 #[patch(name = "VecPatch<String>")]
449 #[cfg_attr(
450 not(feature = "strict"),
451 patch(attribute(serde(alias = "path", alias = "source")))
452 )]
453 #[serde(skip_serializing_if = "Vec::is_empty")]
454 pub paths: Vec<String>,
455
456 #[serde(flatten)]
458 #[patch(name = "CopyOptionsPatch", attribute(serde(flatten)))]
459 pub options: CopyOptions,
460
461 #[patch(name = "VecPatch<String>")]
463 #[serde(skip_serializing_if = "Vec::is_empty")]
464 pub exclude: Vec<String>,
465
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub parents: Option<bool>,
469}
470
471#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
474#[patch(
475 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
476 attribute(serde(deny_unknown_fields, default))
477)]
478#[cfg_attr(
479 feature = "json_schema",
480 patch(
481 attribute(derive(JsonSchema)),
482 attribute(schemars(title = "CopyContent", rename = "CopyContent"))
483 )
484)]
485pub struct CopyContent {
486 pub content: String,
488
489 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "subst"))))]
491 #[serde(skip_serializing_if = "Option::is_none")]
492 pub substitute: Option<bool>,
493
494 #[serde(flatten)]
496 #[patch(name = "CopyOptionsPatch", attribute(serde(flatten)))]
497 pub options: CopyOptions,
498}
499
500#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
503#[patch(
504 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
505 attribute(serde(deny_unknown_fields, default, rename_all = "camelCase"))
506)]
507#[cfg_attr(
508 feature = "json_schema",
509 patch(
510 attribute(derive(JsonSchema)),
511 attribute(schemars(title = "AddGitRepo", rename = "AddGitRepo"))
512 )
513)]
514pub struct AddGitRepo {
515 pub repo: String,
517
518 #[serde(flatten)]
520 #[patch(name = "CopyOptionsPatch", attribute(serde(flatten)))]
521 pub options: CopyOptions,
522
523 #[patch(name = "VecPatch<String>")]
525 #[serde(skip_serializing_if = "Vec::is_empty")]
526 pub exclude: Vec<String>,
527
528 #[serde(skip_serializing_if = "Option::is_none")]
531 pub keep_git_dir: Option<bool>,
532}
533
534#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
536#[patch(
537 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
538 attribute(serde(deny_unknown_fields, default))
539)]
540#[cfg_attr(
541 feature = "json_schema",
542 patch(
543 attribute(derive(JsonSchema)),
544 attribute(schemars(title = "Add", rename = "Add"))
545 )
546)]
547pub struct Add {
548 #[patch(name = "VecPatch<Resource>")]
550 #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "file"))))]
551 #[serde(skip_serializing_if = "Vec::is_empty")]
552 pub files: Vec<Resource>,
553
554 #[serde(flatten)]
556 #[patch(name = "CopyOptionsPatch", attribute(serde(flatten)))]
557 pub options: CopyOptions,
558
559 #[serde(skip_serializing_if = "Option::is_none")]
562 pub checksum: Option<String>,
563}
564
565#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
567#[patch(
568 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
569 attribute(serde(deny_unknown_fields, default))
570)]
571#[cfg_attr(
572 feature = "json_schema",
573 patch(
574 attribute(derive(JsonSchema)),
575 attribute(schemars(title = "CopyOptions", rename = "CopyOptions"))
576 )
577)]
578pub struct CopyOptions {
579 #[cfg_attr(
581 not(feature = "strict"),
582 patch(attribute(serde(alias = "destination")))
583 )]
584 #[serde(skip_serializing_if = "Option::is_none")]
585 pub target: Option<String>,
586
587 #[patch(name = "Option<UserPatch>")]
590 #[serde(skip_serializing_if = "Option::is_none")]
591 pub chown: Option<User>,
592
593 #[cfg_attr(
596 feature = "permissive",
597 patch(attribute(serde(
598 deserialize_with = "deserialize_from_optional_string_or_number",
599 default
600 )))
601 )]
602 #[serde(skip_serializing_if = "Option::is_none")]
603 #[cfg_attr(
604 feature = "json_schema",
605 patch(attribute(schemars(schema_with = "optional_string_or_number_schema")))
606 )]
607 pub chmod: Option<String>,
608
609 #[serde(skip_serializing_if = "Option::is_none")]
612 pub link: Option<bool>,
613}
614
615#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
617#[patch(
618 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
619 attribute(serde(deny_unknown_fields, default))
620)]
621#[cfg_attr(
622 feature = "json_schema",
623 patch(
624 attribute(derive(JsonSchema)),
625 attribute(schemars(title = "User", rename = "User"))
626 )
627)]
628pub struct User {
629 pub user: String,
632
633 #[serde(skip_serializing_if = "Option::is_none")]
636 pub group: Option<String>,
637}
638
639#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
641#[patch(
642 attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
643 attribute(serde(deny_unknown_fields, default))
644)]
645#[cfg_attr(
646 feature = "json_schema",
647 patch(
648 attribute(derive(JsonSchema)),
649 attribute(schemars(title = "Port", rename = "Port"))
650 )
651)]
652pub struct Port {
653 pub port: u16,
655
656 #[serde(skip_serializing_if = "Option::is_none")]
658 pub protocol: Option<PortProtocol>,
659}
660
661#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Hash, Eq, PartialOrd)]
665#[serde(rename_all = "camelCase")]
666#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
667pub enum ImageVersion {
668 Tag(String),
669 Digest(String),
670}
671
672#[derive(Serialize, Debug, Clone, PartialEq)]
674#[serde(rename_all = "camelCase")]
675pub enum FromContext {
676 FromImage(ImageName),
677 FromBuilder(String),
678 FromContext(Option<String>),
679}
680
681#[derive(Serialize, Debug, Clone, PartialEq)]
682#[serde(untagged)]
683pub enum CopyResource {
684 Copy(Copy),
685 Content(CopyContent),
686 AddGitRepo(AddGitRepo),
687 Add(Add),
688}
689
690#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
692#[serde(rename_all = "camelCase")]
693#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
694pub enum CacheSharing {
695 Shared,
696 Private,
697 Locked,
698}
699
700#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
702#[serde(rename_all = "camelCase")]
703#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
704pub enum PortProtocol {
705 Tcp,
706 Udp,
707}
708
709#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Hash, Eq, PartialOrd)]
711#[serde(untagged)]
712#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
713pub enum Resource {
714 Url(Url),
715 File(PathBuf),
716}
717
718#[derive(Debug, Clone, PartialEq, Deserialize)]
721#[serde(rename_all = "camelCase")]
722#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
723pub enum FromContextPatch {
724 #[cfg(not(feature = "permissive"))]
725 #[cfg_attr(not(feature = "strict"), serde(alias = "image"))]
726 FromImage(ImageNamePatch),
727
728 #[cfg(feature = "permissive")]
729 #[cfg_attr(not(feature = "strict"), serde(alias = "image"))]
730 FromImage(ParsableStruct<ImageNamePatch>),
731
732 #[cfg_attr(not(feature = "strict"), serde(alias = "builder"))]
733 FromBuilder(String),
734
735 #[cfg_attr(not(feature = "strict"), serde(alias = "from"))]
736 FromContext(Option<String>),
737}
738
739#[derive(Debug, Clone, PartialEq, Deserialize)]
740#[serde(untagged)]
741#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
742pub enum CopyResourcePatch {
743 Copy(CopyPatch),
744 Content(CopyContentPatch),
745 AddGitRepo(AddGitRepoPatch),
746 Add(AddPatch),
747 Unknown(UnknownPatch),
748}
749
750#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
751#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
752pub struct UnknownPatch {
753 #[serde(flatten)]
754 pub options: Option<CopyOptionsPatch>,
755 pub exclude: Option<VecPatch<String>>,
756}
757
758#[cfg(test)]
761mod test {
762 use super::*;
763 use pretty_assertions_sorted::assert_eq_sorted;
764
765 mod deserialize {
766 use super::*;
767
768 mod dofigen {
769 use super::*;
770
771 #[test]
772 fn empty() {
773 let data = r#""#;
774
775 let dofigen: DofigenPatch = serde_yaml::from_str(data).unwrap();
776 let dofigen: Dofigen = dofigen.into();
777
778 assert_eq_sorted!(dofigen, Dofigen::default());
779 }
780
781 #[test]
782 fn from() {
783 let data = r#"
784 fromImage:
785 path: ubuntu
786 "#;
787
788 let dofigen: DofigenPatch = serde_yaml::from_str(data).unwrap();
789 let dofigen: Dofigen = dofigen.into();
790
791 assert_eq_sorted!(
792 dofigen,
793 Dofigen {
794 stage: Stage {
795 from: FromContext::FromImage(ImageName {
796 path: "ubuntu".into(),
797 ..Default::default()
798 }),
799 ..Default::default()
800 },
801 ..Default::default()
802 }
803 );
804 }
805
806 #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
807 #[test]
808 fn duplicate_from() {
809 let data = r#"
810 from:
811 path: ubuntu
812 from:
813 path: alpine
814 "#;
815
816 let dofigen: serde_yaml::Result<DofigenPatch> = serde_yaml::from_str(data);
817
818 println!("{:?}", dofigen);
819
820 assert!(dofigen.is_err());
821 }
822
823 #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
824 #[test]
825 fn duplicate_from_and_alias() {
826 let data = r#"
827 from:
828 path: ubuntu
829 image:
830 path: alpine
831 "#;
832
833 let dofigen: serde_yaml::Result<DofigenPatch> = serde_yaml::from_str(data);
834
835 println!("{:?}", dofigen);
836
837 assert!(dofigen.is_err());
838 }
839
840 #[test]
841 fn global_arg() {
842 let data = r#"
843 globalArg:
844 IMAGE: ubuntu
845 fromContext: ${IMAGE}
846 "#;
847
848 let dofigen: DofigenPatch = serde_yaml::from_str(data).unwrap();
849 let dofigen: Dofigen = dofigen.into();
850
851 assert_eq_sorted!(
852 dofigen,
853 Dofigen {
854 global_arg: HashMap::from([("IMAGE".into(), "ubuntu".into())]),
855 stage: Stage {
856 from: FromContext::FromContext(Some("${IMAGE}".into())).into(),
857 ..Default::default()
858 },
859 ..Default::default()
860 }
861 );
862 }
863 }
864
865 mod stage {
866 use super::*;
867
868 #[test]
869 fn empty() {
870 let data = r#""#;
871
872 let stage: StagePatch = serde_yaml::from_str(data).unwrap();
873 let stage: Stage = stage.into();
874
875 assert_eq_sorted!(stage, Stage::default());
876 }
877
878 #[test]
879 fn from() {
880 let data = r#"
881 fromImage:
882 path: ubuntu
883 "#;
884
885 let stage: StagePatch = serde_yaml::from_str(data).unwrap();
886 let stage: Stage = stage.into();
887
888 assert_eq_sorted!(
889 stage,
890 Stage {
891 from: FromContext::FromImage(ImageName {
892 path: "ubuntu".into(),
893 ..Default::default()
894 })
895 .into(),
896 ..Default::default()
897 }
898 );
899 }
900
901 #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
902 #[test]
903 fn duplicate_from() {
904 let data = r#"
905 fromImage:
906 path: ubuntu
907 fromImage:
908 path: alpine
909 "#;
910
911 let stage: serde_yaml::Result<StagePatch> = serde_yaml::from_str(data);
912
913 assert!(stage.is_err());
914 }
915
916 #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
917 #[test]
918 fn duplicate_from_and_alias() {
919 let data = r#"
920 fromImage:
921 path: ubuntu
922 image:
923 path: alpine
924 "#;
925
926 let stage: serde_yaml::Result<StagePatch> = serde_yaml::from_str(data);
927
928 assert!(stage.is_err());
929 }
930
931 #[test]
932 fn label() {
933 #[cfg(not(feature = "strict"))]
934 let data = r#"
935 label:
936 io.dofigen:
937 test: test
938 "#;
939 #[cfg(feature = "strict")]
940 let data = r#"
941 label:
942 io.dofigen.test: test
943 "#;
944
945 let stage: StagePatch = serde_yaml::from_str(data).unwrap();
946 let stage: Stage = stage.into();
947
948 assert_eq_sorted!(
949 stage,
950 Stage {
951 label: HashMap::from([("io.dofigen.test".into(), "test".into())]),
952 ..Default::default()
953 }
954 );
955 }
956 }
957
958 mod user {
959 use super::*;
960
961 #[test]
962 fn name_and_group() {
963 let json_data = r#"{
964 "user": "test",
965 "group": "test"
966}"#;
967
968 let user: UserPatch = serde_yaml::from_str(json_data).unwrap();
969 let user: User = user.into();
970
971 assert_eq_sorted!(
972 user,
973 User {
974 user: "test".into(),
975 group: Some("test".into())
976 }
977 );
978 }
979 }
980
981 mod copy_resource {
982
983 use super::*;
984
985 #[test]
986 fn copy() {
987 let json_data = r#"{
988 "paths": ["file1.txt", "file2.txt"],
989 "target": "destination/",
990 "chown": {
991 "user": "root",
992 "group": "root"
993 },
994 "chmod": "755",
995 "link": true,
996 "fromImage": {"path": "my-image"}
997}"#;
998
999 let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1000 let copy_resource: CopyResource = copy_resource.into();
1001
1002 assert_eq_sorted!(
1003 copy_resource,
1004 CopyResource::Copy(Copy {
1005 paths: vec!["file1.txt".into(), "file2.txt".into()].into(),
1006 options: CopyOptions {
1007 target: Some("destination/".into()),
1008 chown: Some(User {
1009 user: "root".into(),
1010 group: Some("root".into())
1011 }),
1012 chmod: Some("755".into()),
1013 link: Some(true),
1014 },
1015 from: FromContext::FromImage(ImageName {
1016 path: "my-image".into(),
1017 ..Default::default()
1018 }),
1019 exclude: vec![].into(),
1020 parents: None,
1021 })
1022 );
1023 }
1024
1025 #[test]
1026 fn copy_simple() {
1027 let json_data = r#"{
1028 "paths": ["file1.txt"]
1029}"#;
1030
1031 let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1032
1033 assert_eq_sorted!(
1034 copy_resource,
1035 CopyResourcePatch::Copy(CopyPatch {
1036 paths: Some(vec!["file1.txt".into()].into_patch()),
1037 options: Some(CopyOptionsPatch::default()),
1038 ..Default::default()
1039 })
1040 );
1041
1042 let copy_resource: CopyResource = copy_resource.into();
1043
1044 assert_eq_sorted!(
1045 copy_resource,
1046 CopyResource::Copy(Copy {
1047 paths: vec!["file1.txt".into()].into(),
1048 options: CopyOptions::default(),
1049 ..Default::default()
1050 })
1051 );
1052 }
1053
1054 #[cfg(feature = "permissive")]
1055 #[test]
1056 fn copy_chmod_int() {
1057 let json_data = r#"{
1058 "paths": ["file1.txt"],
1059 "chmod": 755
1060}"#;
1061
1062 let copy_resource: CopyPatch = serde_yaml::from_str(json_data).unwrap();
1063
1064 assert_eq_sorted!(
1065 copy_resource,
1066 CopyPatch {
1067 paths: Some(vec!["file1.txt".into()].into_patch()),
1068 options: Some(CopyOptionsPatch {
1069 chmod: Some(Some("755".into())),
1070 ..Default::default()
1071 }),
1072 ..Default::default()
1073 }
1074 );
1075 }
1076
1077 #[cfg(feature = "permissive")]
1078 #[test]
1079 fn deserialize_copy_from_str() {
1080 let json_data = "file1.txt destination/";
1081
1082 let copy_resource: ParsableStruct<CopyResourcePatch> =
1083 serde_yaml::from_str(json_data).unwrap();
1084 let copy_resource: CopyResource = copy_resource.into();
1085
1086 assert_eq_sorted!(
1087 copy_resource,
1088 CopyResource::Copy(Copy {
1089 paths: vec!["file1.txt".into()].into(),
1090 options: CopyOptions {
1091 target: Some("destination/".into()),
1092 ..Default::default()
1093 },
1094 ..Default::default()
1095 })
1096 );
1097 }
1098
1099 #[test]
1100 fn copy_content() {
1101 let json_data = r#"{
1102 "content": "echo coucou",
1103 "substitute": false,
1104 "target": "test.sh",
1105 "chown": {
1106 "user": "1001",
1107 "group": "1001"
1108 },
1109 "chmod": "555",
1110 "link": true
1111}"#;
1112
1113 let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1114 let copy_resource: CopyResource = copy_resource.into();
1115
1116 assert_eq_sorted!(
1117 copy_resource,
1118 CopyResource::Content(CopyContent {
1119 content: "echo coucou".into(),
1120 substitute: Some(false),
1121 options: CopyOptions {
1122 target: Some("test.sh".into()),
1123 chown: Some(User {
1124 user: "1001".into(),
1125 group: Some("1001".into())
1126 }),
1127 chmod: Some("555".into()),
1128 link: Some(true),
1129 }
1130 })
1131 );
1132 }
1133
1134 #[test]
1135 fn add_git_repo() {
1136 let json_data = r#"{
1137 "repo": "https://github.com/example/repo.git",
1138 "target": "destination/",
1139 "chown": {
1140 "user": "root",
1141 "group": "root"
1142 },
1143 "chmod": "755",
1144 "link": true,
1145 "keepGitDir": true
1146 }"#;
1147
1148 let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1149 let copy_resource: CopyResource = copy_resource.into();
1150
1151 assert_eq_sorted!(
1152 copy_resource,
1153 CopyResource::AddGitRepo(AddGitRepo {
1154 repo: "https://github.com/example/repo.git".into(),
1155 options: CopyOptions {
1156 target: Some("destination/".into()),
1157 chown: Some(User {
1158 user: "root".into(),
1159 group: Some("root".into())
1160 }),
1161 chmod: Some("755".into()),
1162 link: Some(true),
1163 },
1164 keep_git_dir: Some(true),
1165 exclude: vec![].into(),
1166 })
1167 );
1168 }
1169
1170 #[test]
1171 fn add() {
1172 let json_data = r#"{
1173 "files": ["file1.txt", "file2.txt"],
1174 "target": "destination/",
1175 "checksum": "sha256:abcdef123456",
1176 "chown": {
1177 "user": "root",
1178 "group": "root"
1179 },
1180 "chmod": "755",
1181 "link": true
1182 }"#;
1183
1184 let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1185 let copy_resource: CopyResource = copy_resource.into();
1186
1187 assert_eq_sorted!(
1188 copy_resource,
1189 CopyResource::Add(Add {
1190 files: vec![
1191 Resource::File("file1.txt".into()),
1192 Resource::File("file2.txt".into())
1193 ]
1194 .into(),
1195 options: CopyOptions {
1196 target: Some("destination/".into()),
1197 chown: Some(User {
1198 user: "root".into(),
1199 group: Some("root".into())
1200 }),
1201 chmod: Some("755".into()),
1202 link: Some(true),
1203 },
1204 checksum: Some("sha256:abcdef123456".into()),
1205 })
1206 );
1207 }
1208 }
1209
1210 mod builder {
1211 use super::*;
1212
1213 #[test]
1214 fn with_bind() {
1215 let json_data = r#"
1216fromImage:
1217 path: clux/muslrust:stable
1218workdir: /app
1219bind:
1220 - target: /app
1221run:
1222 - cargo build --release
1223 - mv target/x86_64-unknown-linux-musl/release/dofigen /app/
1224"#;
1225
1226 let builder: Stage = serde_yaml::from_str::<StagePatch>(json_data)
1227 .unwrap()
1228 .into();
1229
1230 assert_eq_sorted!(
1231 builder,
1232 Stage {
1233 from: FromContext::FromImage(ImageName {
1234 path: "clux/muslrust:stable".into(),
1235 ..Default::default()
1236 }),
1237 workdir: Some("/app".into()),
1238 run: Run {
1239 bind: vec![Bind {
1240 target: "/app".into(),
1241 ..Default::default()
1242 }],
1243 run: vec![
1244 "cargo build --release".into(),
1245 "mv target/x86_64-unknown-linux-musl/release/dofigen /app/".into()
1246 ],
1247 ..Default::default()
1248 },
1249 ..Default::default()
1250 }
1251 );
1252 }
1253 }
1254 }
1255}