1use crate::deserialize::*;
2use crate::dofigen_struct::*;
3use regex::Regex;
4use serde::de::{Error as DeError, value::Error};
5use std::str::FromStr;
6use struct_patch::Patch;
7use url::Url;
8
9const GIT_HTTP_REPO_REGEX: &str = "https?://(?:.+@)?[a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)+/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+\\.git(?:#[a-zA-Z0-9_/.-]*(?::[a-zA-Z0-9_/-]+)?)?";
10const GIT_SSH_REPO_REGEX: &str = "[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)+:[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+(?:#[a-zA-Z0-9_/.-]+)?(?::[a-zA-Z0-9_/-]+)?";
11const URL_REGEX: &str = "https?://(?:.+@)?[a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)+(/[a-zA-Z0-9_.-]+)*";
12
13macro_rules! impl_parsable_patch {
14 ($struct:ty, $patch:ty, $param:ident, $expression:expr_2021) => {
15 impl Patch<ParsableStruct<$patch>> for $struct {
16 fn apply(&mut self, patch: ParsableStruct<$patch>) {
17 self.apply(patch.0);
18 }
19
20 fn into_patch(self) -> ParsableStruct<$patch> {
21 ParsableStruct(self.into_patch())
22 }
23
24 fn into_patch_by_diff(self, previous_struct: Self) -> ParsableStruct<$patch> {
25 ParsableStruct(self.into_patch_by_diff(previous_struct))
26 }
27
28 fn new_empty_patch() -> ParsableStruct<$patch> {
29 ParsableStruct(Self::new_empty_patch())
30 }
31 }
32
33 impl From<ParsableStruct<$patch>> for $struct {
34 fn from(value: ParsableStruct<$patch>) -> Self {
35 value.0.into()
36 }
37 }
38
39 impl FromStr for $patch {
40 type Err = Error;
41
42 fn from_str($param: &str) -> std::result::Result<Self, Self::Err> {
43 $expression
44 }
45 }
46 };
47}
48
49impl_parsable_patch!(ImageName, ImageNamePatch, s, {
50 let regex = Regex::new(r"^(?:(?<host>[^:\/.]+(?:\.[^:\/.]+)+)(?::(?<port>\d{1,5}))?\/)?(?<path>[a-zA-Z0-9-]{1,63}(?:\/[a-zA-Z0-9-]{1,63})*)(?:(?<version_char>[:@])(?<version_value>[a-zA-Z0-9_.:-]{1,128}))?$").unwrap();
51 let Some(captures) = regex.captures(s) else {
52 return Err(Error::custom(format!(
53 "String '{}' is not matching image name pattern",
54 s
55 )));
56 };
57 Ok(ImageNamePatch {
58 host: Some(captures.name("host").map(|m| m.as_str().into())),
59 port: Some(captures.name("port").map(|m| m.as_str().parse().unwrap())),
60 path: Some(captures["path"].into()),
61 version: Some(
62 match (
63 captures.name("version_char").map(|m| m.as_str()),
64 captures.name("version_value"),
65 ) {
66 (Some(":"), Some(value)) => Some(ImageVersion::Tag(value.as_str().into())),
67 (Some("@"), Some(value)) => Some(ImageVersion::Digest(value.as_str().into())),
68 (None, None) => None,
69 _ => return Err(Error::custom("Invalid version format")),
70 },
71 ),
72 platform: Some(None),
73 })
74});
75
76impl_parsable_patch!(CopyResource, CopyResourcePatch, s, {
77 let parts_regex = format!(
78 r#"^(?:(?<git>(?:{git_http}|"{git_http}"|'{git_http}'|{git_ssh}|"{git_ssh}"|'{git_ssh}'))|(?<url>{url}|"{url}"|'{url}')|\S+)(?: (?:"\S+"|'\S+'|\S+))*(?: (?:"\S+"|'\S+'|\S+))?$"#,
79 git_http = GIT_HTTP_REPO_REGEX,
80 git_ssh = GIT_SSH_REPO_REGEX,
81 url = URL_REGEX
82 );
83 let regex = Regex::new(parts_regex.as_str()).unwrap();
84 let Some(captures) = regex.captures(s) else {
85 return Err(Error::custom("Not matching copy resources pattern"));
86 };
87 if captures.name("git").is_some() {
88 return Ok(CopyResourcePatch::AddGitRepo(s.parse().unwrap()));
89 }
90 if captures.name("url").is_some() {
91 return Ok(CopyResourcePatch::Add(s.parse().unwrap()));
92 }
93 Ok(CopyResourcePatch::Copy(s.parse().unwrap()))
94});
95
96impl_parsable_patch!(Copy, CopyPatch, s, {
97 let mut parts: Vec<String> = collect_path_list(s);
98 let from = if parts
99 .first()
100 .map(|s| {
101 s.starts_with('@')
102 || s.starts_with("image@")
103 || s.starts_with("builder@")
104 || s.starts_with("context@")
105 })
106 .unwrap_or(false)
107 {
108 let from_str = parts.remove(0);
109 let from_parts = from_str.split('@').collect::<Vec<_>>();
110 if from_parts.len() != 2 {
111 return Err(Error::custom("Invalid from context format"));
112 }
113 let from_type = from_parts[0];
114 let from_value = from_parts[1];
115 match from_type {
116 "image" => FromContextPatch::FromImage(from_value.parse()?),
117 "builder" => FromContextPatch::FromBuilder(from_value.into()),
118 "context" | "" => FromContextPatch::FromContext(Some(from_value.into())),
119 _ => return Err(Error::custom("Invalid from context type")),
120 }
121 } else {
122 FromContextPatch::default()
123 };
124 let target = if parts.len() > 1 { parts.pop() } else { None };
125 Ok(Self {
126 paths: Some(parts.into_patch()),
127 options: Some(CopyOptionsPatch {
128 target: Some(target),
129 chmod: Some(None),
130 chown: Some(None),
131 link: Some(None),
132 }),
133 from: Some(from),
134 exclude: Some(VecPatch::default()),
135 parents: Some(None),
136 })
137});
138
139impl_parsable_patch!(AddGitRepo, AddGitRepoPatch, s, {
140 let paths = collect_path_list(s);
141 let (repo, target) = match paths.as_slice() {
142 [repo, target] => (repo, Some(target)),
143 [repo] => (repo, None),
144 _ => return Err(Error::custom("Invalid add git repo format")),
145 };
146 Ok(Self {
147 repo: Some(repo.clone()),
148 options: Some(CopyOptionsPatch {
149 target: Some(target.cloned()),
150 chmod: Some(None),
151 chown: Some(None),
152 link: Some(None),
153 }),
154 keep_git_dir: Some(None),
155 checksum: Some(None),
156 exclude: Some(VecPatch::default()),
157 })
158});
159
160impl_parsable_patch!(Add, AddPatch, s, {
161 let mut parts = collect_path_list(s);
162 let target = if parts.len() > 1 { parts.pop() } else { None };
163 let parts: Vec<_> = parts
164 .iter()
165 .map(|s| {
166 Url::parse(s)
167 .map(Resource::Url)
168 .ok()
169 .unwrap_or(Resource::File(s.into()))
170 })
171 .collect();
172 Ok(Self {
173 files: Some(parts.into_patch()),
174 options: Some(CopyOptionsPatch {
175 target: Some(target),
176 chmod: Some(None),
177 chown: Some(None),
178 link: Some(None),
179 }),
180 checksum: Some(None),
181 unpack: Some(None),
182 })
183});
184
185fn collect_path_list(s: &str) -> Vec<String> {
186 let regex = Regex::new(r#"(?:"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\S+)"#).unwrap();
187 regex
188 .find_iter(s)
189 .map(|m| {
190 let mut m = m.as_str().to_string();
191 let first = m.chars().next().unwrap();
192 if first == '"' || first == '\'' {
193 m = m[1..m.len() - 1].to_string();
195 while let Some(pos) = m.find(format!("\\{}", first).as_str()) {
196 m = format!("{}{}", &m[..pos], &m[pos + 1..]);
198 }
199 } else {
200 while let Some(pos) = m.find("\\\"").or(m.find("\\'")) {
201 m = format!("{}{}", &m[..pos], &m[pos + 1..]);
203 }
204 }
205 m
206 })
207 .collect()
208}
209
210impl_parsable_patch!(User, UserPatch, s, {
211 let regex = Regex::new(r"^(?<user>[a-zA-Z0-9_-]+)(?::(?<group>[a-zA-Z0-9_-]+))?$").unwrap();
212 let Some(captures) = regex.captures(s) else {
213 return Err(Error::custom("Not matching chown pattern"));
214 };
215 Ok(Self {
216 user: Some(captures["user"].into()),
217 group: Some(captures.name("group").map(|m| m.as_str().into())),
218 })
219});
220
221impl_parsable_patch!(Port, PortPatch, s, {
222 let regex = Regex::new(r"^(?<port>\d+)(?:/(?<protocol>(tcp|udp)))?$").unwrap();
223 let Some(captures) = regex.captures(s) else {
224 return Err(Error::custom("Not matching chown pattern"));
225 };
226 Ok(Self {
227 port: Some(captures["port"].parse().map_err(Error::custom)?),
228 protocol: Some(captures.name("protocol").map(|m| match m.as_str() {
229 "tcp" => PortProtocol::Tcp,
230 "udp" => PortProtocol::Udp,
231 _ => unreachable!(),
232 })),
233 })
234});
235
236impl_parsable_patch!(Bind, BindPatch, s, {
237 let regex = Regex::new(r"^(?:(?:(?P<fromType>image|builder|context)\((?P<from>[^:]+)\):)?(?P<source>\S+) )?(?P<target>\S+)$").unwrap();
238 let Some(captures) = regex.captures(s) else {
239 return Err(Error::custom("Not matching bind pattern"));
240 };
241
242 let target = Some(captures["target"].to_string());
243 Ok(Self {
244 source: Some(
245 captures
246 .name("source")
247 .map(|m| m.as_str().into())
248 .or(target.clone()),
249 ),
250 target,
251 from: captures.name("from").map(|m| {
252 let from_type = captures.name("fromType").map(|m| m.as_str()).unwrap();
253 let from = m.as_str();
254 match from_type {
255 "image" => {
256 FromContextPatch::FromImage(ImageNamePatch::from_str(from).unwrap().into())
257 }
258 "builder" => FromContextPatch::FromBuilder(from.into()),
259 "context" => FromContextPatch::FromContext(Some(from.into())),
260 _ => unreachable!(),
261 }
262 }),
263
264 readwrite: Some(None),
265 })
266});
267
268impl_parsable_patch!(Cache, CachePatch, s, {
269 let regex = Regex::new(r"^(?:(?:(?P<fromType>image|builder|context)\((?P<from>[^:]+)\):)?(?P<source>\S+) )?(?P<target>\S+)$").unwrap();
270 let Some(captures) = regex.captures(s) else {
271 return Err(Error::custom("Not matching bind pattern"));
272 };
273
274 let target = Some(captures["target"].to_string());
275 Ok(Self {
276 source: Some(captures.name("source").map(|m| m.as_str().into())),
277 target,
278 from: captures.name("from").map(|m| {
279 let from_type = captures.name("fromType").map(|m| m.as_str()).unwrap();
280 let from = m.as_str();
281 match from_type {
282 "image" => {
283 FromContextPatch::FromImage(ImageNamePatch::from_str(from).unwrap().into())
284 }
285 "builder" => FromContextPatch::FromBuilder(from.into()),
286 "context" => FromContextPatch::FromContext(Some(from.into())),
287 _ => unreachable!(),
288 }
289 }),
290 chmod: Some(None),
291 chown: Some(None),
292 id: Some(None),
293 readonly: Some(None),
294 sharing: Some(None),
295 })
296});
297
298impl_parsable_patch!(TmpFs, TmpFsPatch, s, {
299 Ok(Self {
300 target: Some(s.into()),
301 size: Some(None),
302 })
303});
304
305#[cfg(test)]
306mod test {
307 use super::*;
308 use pretty_assertions_sorted::assert_eq_sorted;
309 mod from_str {
310 use super::*;
311
312 mod image_name {
313 use pretty_assertions_sorted::assert_eq_sorted;
314
315 use super::*;
316
317 #[test]
318 fn simple() {
319 let input = "example/image";
320 let result = ImageNamePatch::from_str(input).unwrap();
321 assert_eq_sorted!(result.host, Some(None));
322 assert_eq_sorted!(result.path, Some("example/image".into()));
323 assert_eq_sorted!(result.port, Some(None));
324 assert_eq_sorted!(result.version, Some(None));
325 }
326
327 #[test]
328 fn with_host() {
329 let input = "docker.io/example/image";
330 let result = ImageNamePatch::from_str(input).unwrap();
331 assert_eq_sorted!(result.host, Some(Some("docker.io".into())));
332 assert_eq_sorted!(result.path, Some("example/image".into()));
333 assert_eq_sorted!(result.port, Some(None));
334 assert_eq_sorted!(result.version, Some(None));
335 }
336
337 #[test]
338 fn with_tag() {
339 let input = "example/image:tag";
340 let result = ImageNamePatch::from_str(input).unwrap();
341 assert_eq_sorted!(result.host, Some(None));
342 assert_eq_sorted!(result.path, Some("example/image".into()));
343 assert_eq_sorted!(result.port, Some(None));
344 assert_eq_sorted!(result.version, Some(Some(ImageVersion::Tag("tag".into()))));
345 }
346
347 #[test]
348 fn with_digest() {
349 let input = "example/image@sha256:my-sha";
350 let result = ImageNamePatch::from_str(input).unwrap();
351 assert_eq_sorted!(result.host, Some(None));
352 assert_eq_sorted!(result.path, Some("example/image".into()));
353 assert_eq_sorted!(result.port, Some(None));
354 assert_eq_sorted!(
355 result.version,
356 Some(Some(ImageVersion::Digest("sha256:my-sha".into())))
357 );
358 }
359
360 #[test]
361 fn full() {
362 let input = "registry.my-host.io:5001/example/image:stable";
363 let result = ImageNamePatch::from_str(input).unwrap();
364 assert_eq_sorted!(result.host, Some(Some("registry.my-host.io".into())));
365 assert_eq_sorted!(result.path, Some("example/image".into()));
366 assert_eq_sorted!(result.port, Some(Some(5001)));
367 assert_eq_sorted!(
368 result.version,
369 Some(Some(ImageVersion::Tag("stable".into())))
370 );
371 }
372
373 #[test]
374 fn with_arg() {
375 let input = "example/image:${VERSION}";
376 let result = ImageNamePatch::from_str(input);
377 assert!(result.is_err());
378 }
379 }
380
381 mod copy {
382
383 use super::*;
384
385 #[test]
386 fn simple() {
387 let result = CopyPatch::from_str("src").unwrap();
388 assert_eq_sorted!(
389 result,
390 CopyPatch {
391 paths: Some(vec!["src".to_string()].into_patch()),
392 options: Some(CopyOptionsPatch {
393 target: Some(None),
394 chown: Some(None),
395 chmod: Some(None),
396 link: Some(None),
397 }),
398 from: Some(FromContextPatch::default()),
399 exclude: Some(VecPatch::default()),
400 parents: Some(None),
401 }
402 );
403 }
404
405 #[test]
406 fn with_target_option() {
407 let result = CopyPatch::from_str("src /app").unwrap();
408 assert_eq_sorted!(
409 result,
410 CopyPatch {
411 paths: Some(vec!["src".to_string()].into_patch()),
412 options: Some(CopyOptionsPatch {
413 target: Some(Some("/app".into())),
414 chown: Some(None),
415 chmod: Some(None),
416 link: Some(None),
417 }),
418 from: Some(FromContextPatch::default()),
419 exclude: Some(VecPatch::default()),
420 parents: Some(None),
421 }
422 );
423 }
424
425 #[test]
426 fn with_multiple_sources_and_target() {
427 let result = CopyPatch::from_str("src1 src2 /app").unwrap();
428 assert_eq_sorted!(
429 result,
430 CopyPatch {
431 paths: Some(vec!["src1".to_string(), "src2".to_string()].into_patch()),
432 options: Some(CopyOptionsPatch {
433 target: Some(Some("/app".into())),
434 chown: Some(None),
435 chmod: Some(None),
436 link: Some(None),
437 }),
438 from: Some(FromContextPatch::default()),
439 exclude: Some(VecPatch::default()),
440 parents: Some(None),
441 }
442 );
443 }
444
445 #[test]
446 fn with_target_option_and_from_image() {
447 let result = CopyPatch::from_str("image@alpine src /app").unwrap();
448 assert_eq_sorted!(
449 result,
450 CopyPatch {
451 paths: Some(vec!["src".to_string()].into_patch()),
452 options: Some(CopyOptionsPatch {
453 target: Some(Some("/app".into())),
454 chown: Some(None),
455 chmod: Some(None),
456 link: Some(None),
457 }),
458 from: Some(FromContextPatch::FromImage("alpine".parse().unwrap())),
459 exclude: Some(VecPatch::default()),
460 parents: Some(None),
461 }
462 );
463 }
464
465 #[test]
466 fn with_target_option_and_from_builder() {
467 let result = CopyPatch::from_str("builder@my-builder src /app").unwrap();
468 assert_eq_sorted!(
469 result,
470 CopyPatch {
471 paths: Some(vec!["src".to_string()].into_patch()),
472 options: Some(CopyOptionsPatch {
473 target: Some(Some("/app".into())),
474 chown: Some(None),
475 chmod: Some(None),
476 link: Some(None),
477 }),
478 from: Some(FromContextPatch::FromBuilder("my-builder".to_string())),
479 exclude: Some(VecPatch::default()),
480 parents: Some(None),
481 }
482 );
483 }
484
485 #[test]
486 fn with_target_option_and_from_context() {
487 let result = CopyPatch::from_str("@my-context src /app").unwrap();
488 assert_eq_sorted!(
489 result,
490 CopyPatch {
491 paths: Some(vec!["src".to_string()].into_patch()),
492 options: Some(CopyOptionsPatch {
493 target: Some(Some("/app".into())),
494 chown: Some(None),
495 chmod: Some(None),
496 link: Some(None),
497 }),
498 from: Some(FromContextPatch::FromContext(Some(
499 "my-context".to_string()
500 ))),
501 exclude: Some(VecPatch::default()),
502 parents: Some(None),
503 }
504 );
505 }
506 }
507
508 mod add_git_repo {
509
510 use super::*;
511
512 #[test]
513 fn ssh() {
514 let result =
515 AddGitRepoPatch::from_str("git@github.com:lenra-io/dofigen.git").unwrap();
516 assert_eq_sorted!(
517 result,
518 AddGitRepoPatch {
519 repo: Some("git@github.com:lenra-io/dofigen.git".into()),
520 options: Some(CopyOptionsPatch {
521 target: Some(None),
522 chown: Some(None),
523 chmod: Some(None),
524 link: Some(None),
525 }),
526 keep_git_dir: Some(None),
527 exclude: Some(VecPatch::default()),
528 checksum: Some(None),
529 }
530 );
531 }
532
533 #[test]
534 fn ssh_with_target() {
535 let result =
536 AddGitRepoPatch::from_str("git@github.com:lenra-io/dofigen.git /app").unwrap();
537 assert_eq_sorted!(
538 result,
539 AddGitRepoPatch {
540 repo: Some("git@github.com:lenra-io/dofigen.git".into()),
541 options: Some(CopyOptionsPatch {
542 target: Some(Some("/app".into())),
543 chown: Some(None),
544 chmod: Some(None),
545 link: Some(None),
546 }),
547 keep_git_dir: Some(None),
548 exclude: Some(VecPatch::default()),
549 checksum: Some(None),
550 }
551 );
552 }
553
554 #[test]
555 fn http() {
556 let result =
557 AddGitRepoPatch::from_str("https://github.com/lenra-io/dofigen.git").unwrap();
558 assert_eq_sorted!(
559 result,
560 AddGitRepoPatch {
561 repo: Some("https://github.com/lenra-io/dofigen.git".into()),
562 options: Some(CopyOptionsPatch {
563 target: Some(None),
564 chown: Some(None),
565 chmod: Some(None),
566 link: Some(None),
567 }),
568 keep_git_dir: Some(None),
569 exclude: Some(VecPatch::default()),
570 checksum: Some(None),
571 }
572 );
573 }
574
575 #[test]
576 fn http_with_target() {
577 let result =
578 AddGitRepoPatch::from_str("https://github.com/lenra-io/dofigen.git /app")
579 .unwrap();
580 assert_eq_sorted!(
581 result,
582 AddGitRepoPatch {
583 repo: Some("https://github.com/lenra-io/dofigen.git".into()),
584 options: Some(CopyOptionsPatch {
585 target: Some(Some("/app".into())),
586 chown: Some(None),
587 chmod: Some(None),
588 link: Some(None),
589 }),
590 keep_git_dir: Some(None),
591 exclude: Some(VecPatch::default()),
592 checksum: Some(None),
593 }
594 );
595 }
596 }
597
598 mod add {
599 use struct_patch::Patch;
600
601 use crate::{CopyOptions, Resource};
602
603 use super::*;
604
605 #[test]
606 fn simple() {
607 let result =
608 AddPatch::from_str("https://github.com/lenra-io/dofigen/raw/main/README.md")
609 .unwrap();
610 assert_eq_sorted!(
611 result,
612 Add {
613 files: vec![Resource::Url(
614 "https://github.com/lenra-io/dofigen/raw/main/README.md"
615 .parse()
616 .unwrap()
617 )],
618 options: CopyOptions::default(),
619 ..Default::default()
620 }
621 .into_patch()
622 );
623 }
624
625 #[test]
626 fn with_target_option() {
627 let result = AddPatch::from_str(
628 "https://github.com/lenra-io/dofigen/raw/main/README.md /app",
629 )
630 .unwrap();
631 assert_eq_sorted!(
632 result,
633 Add {
634 files: vec![Resource::Url(
635 "https://github.com/lenra-io/dofigen/raw/main/README.md"
636 .parse()
637 .unwrap()
638 )],
639 options: CopyOptions {
640 target: Some("/app".into()),
641 ..Default::default()
642 },
643 ..Default::default()
644 }
645 .into_patch()
646 );
647 }
648
649 #[test]
650 fn with_multiple_sources_and_target() {
651 let result = AddPatch::from_str("https://github.com/lenra-io/dofigen/raw/main/README.md https://github.com/lenra-io/dofigen/raw/main/LICENSE /app").unwrap();
652 assert_eq_sorted!(
653 result,
654 Add {
655 files: vec![
656 Resource::Url(
657 "https://github.com/lenra-io/dofigen/raw/main/README.md"
658 .parse()
659 .unwrap()
660 ),
661 Resource::Url(
662 "https://github.com/lenra-io/dofigen/raw/main/LICENSE"
663 .parse()
664 .unwrap()
665 )
666 ],
667 options: CopyOptions {
668 target: Some("/app".into()),
669 ..Default::default()
670 },
671 ..Default::default()
672 }
673 .into_patch()
674 );
675 }
676 }
677
678 mod copy_resources {
679 use super::*;
680
681 #[test]
682 fn copy() {
683 let result = CopyResourcePatch::from_str("src").unwrap();
684 assert_eq_sorted!(
685 result,
686 CopyResourcePatch::Copy(CopyPatch::from_str("src").unwrap())
687 );
688 }
689
690 #[test]
691 fn add_git_repo_ssh() {
692 let result =
693 CopyResourcePatch::from_str("git@github.com:lenra-io/dofigen.git target")
694 .unwrap();
695 assert_eq_sorted!(
696 result,
697 CopyResourcePatch::AddGitRepo(
698 AddGitRepoPatch::from_str("git@github.com:lenra-io/dofigen.git target")
699 .unwrap()
700 )
701 );
702 }
703
704 #[test]
705 fn add_quoted_git_repo_ssh() {
706 let result =
707 CopyResourcePatch::from_str(r#""git@github.com:lenra-io/dofigen.git" target"#)
708 .unwrap();
709 assert_eq_sorted!(
710 result,
711 CopyResourcePatch::AddGitRepo(
712 AddGitRepoPatch::from_str("git@github.com:lenra-io/dofigen.git target")
713 .unwrap()
714 )
715 );
716 }
717
718 #[test]
719 fn add_git_repo_http() {
720 let result =
721 CopyResourcePatch::from_str("https://github.com/lenra-io/dofigen.git target")
722 .unwrap();
723 assert_eq_sorted!(
724 result,
725 CopyResourcePatch::AddGitRepo(
726 AddGitRepoPatch::from_str("https://github.com/lenra-io/dofigen.git target")
727 .unwrap()
728 )
729 );
730 }
731
732 #[test]
733 fn add_quoted_git_repo_http() {
734 let result =
735 CopyResourcePatch::from_str("'https://github.com/lenra-io/dofigen.git' target")
736 .unwrap();
737 assert_eq_sorted!(
738 result,
739 CopyResourcePatch::AddGitRepo(
740 AddGitRepoPatch::from_str("https://github.com/lenra-io/dofigen.git target")
741 .unwrap()
742 )
743 );
744 }
745
746 #[test]
747 fn add() {
748 let result = CopyResourcePatch::from_str(
749 "https://github.com/lenra-io/dofigen/raw/main/README.md",
750 )
751 .unwrap();
752 assert_eq_sorted!(
753 result,
754 CopyResourcePatch::Add(
755 AddPatch::from_str(
756 "https://github.com/lenra-io/dofigen/raw/main/README.md"
757 )
758 .unwrap()
759 )
760 );
761 }
762 }
763
764 mod user {
765 use pretty_assertions_sorted::assert_eq_sorted;
766
767 use super::*;
768
769 #[test]
770 fn user() {
771 let result = UserPatch::from_str("user").unwrap();
772
773 assert_eq_sorted!(result.user, Some("user".into()));
774 assert_eq_sorted!(result.group, Some(None));
775 }
776
777 #[test]
778 fn with_group() {
779 let result = UserPatch::from_str("user:group").unwrap();
780
781 assert_eq_sorted!(result.user, Some("user".into()));
782 assert_eq_sorted!(result.group, Some(Some("group".into())));
783 }
784
785 #[test]
786 fn uid() {
787 let result = UserPatch::from_str("1000").unwrap();
788
789 assert_eq_sorted!(result.user, Some("1000".into()));
790 assert_eq_sorted!(result.group, Some(None));
791 }
792
793 #[test]
794 fn uid_with_gid() {
795 let result = UserPatch::from_str("1000:1000").unwrap();
796
797 assert_eq_sorted!(result.user, Some("1000".into()));
798 assert_eq_sorted!(result.group, Some(Some("1000".into())));
799 }
800
801 #[test]
802 fn invalid_username() {
803 let result = UserPatch::from_str("user*name");
804
805 assert!(result.is_err());
806 }
807
808 #[test]
809 fn invalid_extra() {
810 let result = UserPatch::from_str("user:group:extra");
811
812 assert!(result.is_err());
813 }
814 }
815
816 mod port {
817
818 use super::*;
819
820 #[test]
821 fn simple() {
822 let result = PortPatch::from_str("80").unwrap();
823
824 assert_eq_sorted!(result.port, Some(80));
825 assert_eq_sorted!(result.protocol, Some(None));
826 }
827
828 #[test]
829 fn with_tcp_protocol() {
830 let result = PortPatch::from_str("80/tcp").unwrap();
831
832 assert_eq_sorted!(result.port, Some(80));
833 assert_eq_sorted!(result.protocol, Some(Some(PortProtocol::Tcp)));
834 }
835
836 #[test]
837 fn with_udp_protocol() {
838 let result = PortPatch::from_str("80/udp").unwrap();
839
840 assert_eq_sorted!(result.port, Some(80));
841 assert_eq_sorted!(result.protocol, Some(Some(PortProtocol::Udp)));
842 }
843
844 #[test]
845 fn invalid() {
846 let result = PortPatch::from_str("80/invalid");
847
848 assert!(result.is_err());
849 }
850 }
851 }
852
853 mod collect_path_list {
854 use super::*;
855
856 #[test]
857 fn simple() {
858 let input = "path1 path2 path3";
859 let result = collect_path_list(input);
860 assert_eq!(result, vec!["path1", "path2", "path3"]);
861 }
862
863 #[test]
864 fn with_quotes() {
865 let input = r#""path 1" 'path 2' path3"#;
866 let result = collect_path_list(input);
867 assert_eq!(result, vec!["path 1", "path 2", "path3"]);
868 }
869
870 #[test]
871 fn with_escaped_quotes() {
872 let input = r#""path\" 1" 'path\' 2' path\"3"#;
873 let result = collect_path_list(input);
874 assert_eq!(result, vec![r#"path" 1"#, "path' 2", r#"path"3"#]);
875 }
876 }
877}