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