1use std::{
4 borrow::Cow,
5 fmt::{self, Display},
6};
7
8use indexmap::IndexMap;
9use self_cell::self_cell;
10use serde::{Deserialize, Deserializer, Serialize, de};
11
12pub mod expr;
13
14#[derive(Deserialize, Debug, PartialEq)]
16#[serde(rename_all = "kebab-case", untagged)]
17pub enum Permissions {
18 Base(BasePermission),
20 Explicit(IndexMap<String, Permission>),
25}
26
27impl Default for Permissions {
28 fn default() -> Self {
29 Self::Base(BasePermission::Default)
30 }
31}
32
33#[derive(Deserialize, Debug, Default, PartialEq)]
36#[serde(rename_all = "kebab-case")]
37pub enum BasePermission {
38 #[default]
40 Default,
41 ReadAll,
43 WriteAll,
45}
46
47#[derive(Deserialize, Debug, Default, PartialEq)]
49#[serde(rename_all = "kebab-case")]
50pub enum Permission {
51 Read,
53
54 Write,
56
57 #[default]
59 None,
60}
61
62pub type Env = IndexMap<String, EnvValue>;
64
65#[derive(Deserialize, Serialize, Debug, PartialEq)]
73#[serde(untagged)]
74pub enum EnvValue {
75 #[serde(deserialize_with = "null_to_default")]
77 String(String),
78 Number(f64),
79 Boolean(bool),
80}
81
82impl Display for EnvValue {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 Self::String(s) => write!(f, "{s}"),
86 Self::Number(n) => write!(f, "{n}"),
87 Self::Boolean(b) => write!(f, "{b}"),
88 }
89 }
90}
91
92impl EnvValue {
93 pub fn is_empty(&self) -> bool {
97 match self {
98 EnvValue::String(s) => s.is_empty(),
99 _ => false,
100 }
101 }
102
103 pub fn csharp_bool(&self) -> bool {
110 match self {
111 EnvValue::Boolean(true) => true,
112 EnvValue::String(maybe) => maybe.trim().eq_ignore_ascii_case("true"),
113 _ => false,
114 }
115 }
116
117 pub fn actions_toolkit_bool(&self) -> Option<bool> {
124 match self {
125 EnvValue::Boolean(b) => Some(*b),
126 EnvValue::String(s) if matches!(s.trim(), "true" | "True" | "TRUE") => Some(true),
127 EnvValue::String(s) if matches!(s.trim(), "false" | "False" | "FALSE") => Some(false),
128 _ => None,
129 }
130 }
131}
132
133#[derive(Deserialize, Debug, PartialEq)]
138#[serde(untagged)]
139enum SoV<T> {
140 One(T),
141 Many(Vec<T>),
142}
143
144impl<T> From<SoV<T>> for Vec<T> {
145 fn from(val: SoV<T>) -> Vec<T> {
146 match val {
147 SoV::One(v) => vec![v],
148 SoV::Many(vs) => vs,
149 }
150 }
151}
152
153pub(crate) fn scalar_or_vector<'de, D, T>(de: D) -> Result<Vec<T>, D::Error>
154where
155 D: Deserializer<'de>,
156 T: Deserialize<'de>,
157{
158 SoV::deserialize(de).map(Into::into)
159}
160
161#[derive(Deserialize, Debug, PartialEq)]
166#[serde(untagged)]
167enum BoU {
168 Bool(bool),
169 Unit(()),
170}
171
172impl From<BoU> for bool {
173 fn from(value: BoU) -> Self {
174 match value {
175 BoU::Bool(bool) => bool,
176 BoU::Unit(_) => true,
177 }
178 }
179}
180
181pub(crate) fn bool_or_unit<'de, D>(de: D) -> Result<bool, D::Error>
182where
183 D: Deserializer<'de>,
184{
185 BoU::deserialize(de).map(Into::into)
186}
187
188#[derive(Deserialize, Debug, PartialEq)]
192#[serde(untagged)]
193enum BoS {
194 Bool(bool),
195 String(String),
196}
197
198impl From<BoS> for String {
199 fn from(value: BoS) -> Self {
200 match value {
201 BoS::Bool(b) => b.to_string(),
202 BoS::String(s) => s,
203 }
204 }
205}
206
207#[derive(Serialize, Debug, PartialEq)]
216pub enum If {
217 Bool(bool),
218 Expr(String),
221}
222
223impl<'de> Deserialize<'de> for If {
224 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225 where
226 D: Deserializer<'de>,
227 {
228 #[derive(Deserialize)]
231 #[serde(untagged)]
232 enum RawIf {
233 Bool(bool),
234 Int(i64),
235 Float(f64),
236 Expr(String),
237 }
238
239 match RawIf::deserialize(deserializer)? {
240 RawIf::Bool(b) => Ok(If::Bool(b)),
241 RawIf::Int(n) => Ok(If::Bool(n != 0)),
242 RawIf::Float(f) => Ok(If::Bool(f != 0.0 && !f.is_nan())),
243 RawIf::Expr(s) => Ok(If::Expr(s)),
244 }
245 }
246}
247
248pub(crate) fn bool_is_string<'de, D>(de: D) -> Result<String, D::Error>
249where
250 D: Deserializer<'de>,
251{
252 BoS::deserialize(de).map(Into::into)
253}
254
255fn null_to_default<'de, D, T>(de: D) -> Result<T, D::Error>
256where
257 D: Deserializer<'de>,
258 T: Default + Deserialize<'de>,
259{
260 let key = Option::<T>::deserialize(de)?;
261 Ok(key.unwrap_or_default())
262}
263
264#[derive(Debug, PartialEq)]
266pub struct UsesError(String);
267
268impl fmt::Display for UsesError {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 write!(f, "malformed `uses` ref: {}", self.0)
271 }
272}
273
274#[derive(Debug, PartialEq)]
275pub enum Uses {
276 Local(LocalUses),
278
279 Repository(RepositoryUses),
281
282 Docker(DockerUses),
284}
285
286impl Uses {
287 pub fn parse<'a>(uses: impl Into<Cow<'a, str>>) -> Result<Self, UsesError> {
289 let uses = uses.into();
290 let uses = uses.trim();
291
292 if uses.starts_with("./") || uses.starts_with("$/") {
293 Ok(Self::Local(LocalUses::new(uses)))
294 } else if let Some(image) = uses.strip_prefix("docker://") {
295 Ok(Self::Docker(DockerUses::parse(image)))
296 } else {
297 RepositoryUses::parse(uses).map(Self::Repository)
298 }
299 }
300
301 pub fn raw(&self) -> &str {
303 match self {
304 Uses::Local(local) => local.raw(),
305 Uses::Repository(repo) => repo.raw(),
306 Uses::Docker(docker) => docker.raw(),
307 }
308 }
309}
310
311#[derive(Debug, PartialEq)]
313#[non_exhaustive]
314pub struct LocalUses {
315 path: String,
316}
317
318impl LocalUses {
319 fn new(path: impl Into<String>) -> Self {
320 LocalUses { path: path.into() }
321 }
322
323 pub fn is_self_repository(&self) -> bool {
328 self.path.starts_with('$')
329 }
330
331 pub fn raw(&self) -> &str {
333 &self.path
334 }
335}
336
337#[derive(Debug, PartialEq)]
338struct RepositoryUsesInner<'a> {
339 owner: &'a str,
341 repo: &'a str,
343 slug: &'a str,
345 subpath: Option<&'a str>,
347 git_ref: &'a str,
349}
350
351impl<'a> RepositoryUsesInner<'a> {
352 fn from_str(uses: &'a str) -> Result<Self, UsesError> {
353 let uses = uses.trim();
355
356 let (path, git_ref) = match uses.rsplit_once('@') {
359 Some((path, git_ref)) => (path, git_ref),
360 None => return Err(UsesError(format!("missing `@<ref>` in {uses}"))),
361 };
362
363 let mut components = path.splitn(3, '/');
364
365 if let Some(owner) = components.next()
366 && let Some(repo) = components.next()
367 {
368 let subpath = components.next();
369
370 let slug = if subpath.is_none() {
371 path
372 } else {
373 &path[..owner.len() + 1 + repo.len()]
374 };
375
376 Ok(RepositoryUsesInner {
377 owner,
378 repo,
379 slug,
380 subpath,
381 git_ref,
382 })
383 } else {
384 Err(UsesError(format!("owner/repo slug is too short: {uses}")))
385 }
386 }
387}
388
389self_cell!(
390 pub struct RepositoryUses {
392 owner: String,
393
394 #[covariant]
395 dependent: RepositoryUsesInner,
396 }
397
398 impl {Debug, PartialEq}
399);
400
401impl Display for RepositoryUses {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 write!(f, "{}", self.raw())
404 }
405}
406
407impl RepositoryUses {
408 pub fn parse(uses: impl Into<String>) -> Result<Self, UsesError> {
410 RepositoryUses::try_new(uses.into(), |s| {
411 let inner = RepositoryUsesInner::from_str(s)?;
412 Ok(inner)
413 })
414 }
415
416 pub fn raw(&self) -> &str {
418 self.borrow_owner()
419 }
420
421 pub fn owner(&self) -> &str {
423 self.borrow_dependent().owner
424 }
425
426 pub fn repo(&self) -> &str {
428 self.borrow_dependent().repo
429 }
430
431 pub fn slug(&self) -> &str {
433 self.borrow_dependent().slug
434 }
435
436 pub fn subpath(&self) -> Option<&str> {
438 self.borrow_dependent().subpath
439 }
440
441 pub fn git_ref(&self) -> &str {
443 self.borrow_dependent().git_ref
444 }
445}
446
447#[derive(Debug, PartialEq)]
448#[non_exhaustive]
449pub struct DockerUsesInner<'a> {
450 registry: Option<&'a str>,
452 image: &'a str,
454 tag: Option<&'a str>,
456 hash: Option<&'a str>,
458}
459
460impl<'a> DockerUsesInner<'a> {
461 fn is_registry(registry: &str) -> bool {
462 registry == "localhost" || registry.contains('.') || registry.contains(':')
464 }
465
466 fn from_str(uses: &'a str) -> Self {
467 let uses = uses.trim();
469
470 let (registry, image) = match uses.split_once('/') {
471 Some((registry, image)) if Self::is_registry(registry) => (Some(registry), image),
472 _ => (None, uses),
473 };
474
475 if let Some(at_pos) = image.find('@') {
479 let (image, hash) = image.split_at(at_pos);
480
481 let hash = if hash.is_empty() {
482 None
483 } else {
484 Some(&hash[1..])
485 };
486
487 DockerUsesInner {
488 registry,
489 image,
490 tag: None,
491 hash,
492 }
493 } else {
494 let (image, tag) = match image.split_once(':') {
495 Some((image, "")) => (image, None),
496 Some((image, tag)) => (image, Some(tag)),
497 _ => (image, None),
498 };
499
500 DockerUsesInner {
501 registry,
502 image,
503 tag,
504 hash: None,
505 }
506 }
507 }
508}
509
510self_cell!(
511 pub struct DockerUses {
513 owner: String,
514
515 #[covariant]
516 dependent: DockerUsesInner,
517 }
518
519 impl {Debug, PartialEq}
520);
521
522impl DockerUses {
523 pub fn parse(uses: impl Into<String>) -> Self {
525 DockerUses::new(uses.into(), |s| DockerUsesInner::from_str(s))
526 }
527
528 pub fn raw(&self) -> &str {
530 self.borrow_owner()
531 }
532
533 pub fn registry(&self) -> Option<&str> {
535 self.borrow_dependent().registry
536 }
537
538 pub fn image(&self) -> &str {
540 self.borrow_dependent().image
541 }
542
543 pub fn tag(&self) -> Option<&str> {
545 self.borrow_dependent().tag
546 }
547
548 pub fn hash(&self) -> Option<&str> {
550 self.borrow_dependent().hash
551 }
552}
553
554impl<'de> Deserialize<'de> for DockerUses {
555 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
556 where
557 D: Deserializer<'de>,
558 {
559 let uses = <Cow<'de, str>>::deserialize(deserializer)?;
560 Ok(DockerUses::parse(uses))
561 }
562}
563
564pub(crate) fn custom_error<'de, D>(msg: impl Display) -> D::Error
570where
571 D: Deserializer<'de>,
572{
573 let msg = msg.to_string();
574 tracing::error!(msg);
575 de::Error::custom(msg)
576}
577
578pub(crate) fn step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
580where
581 D: Deserializer<'de>,
582{
583 let uses = <Cow<'de, str>>::deserialize(de)?;
584 Uses::parse(uses).map_err(custom_error::<D>)
585}
586
587pub(crate) fn reusable_step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
589where
590 D: Deserializer<'de>,
591{
592 let uses = step_uses(de)?;
593
594 match uses {
595 Uses::Repository(_) => Ok(uses),
596 Uses::Local(ref local) => {
597 if local.path.contains('@') {
602 Err(custom_error::<D>(
603 "local reusable workflow reference can't specify `@<ref>`",
604 ))
605 } else {
606 Ok(uses)
607 }
608 }
609 Uses::Docker(_) => Err(custom_error::<D>(
611 "docker action invalid in reusable workflow `uses`",
612 )),
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use indexmap::IndexMap;
619 use serde::Deserialize;
620
621 use crate::common::{BasePermission, Env, EnvValue, Permission};
622
623 use super::{Permissions, Uses, reusable_step_uses};
624
625 #[test]
626 fn test_permissions() {
627 assert_eq!(
628 yaml_serde::from_str::<Permissions>("read-all").unwrap(),
629 Permissions::Base(BasePermission::ReadAll)
630 );
631
632 let perm = "security-events: write";
633 assert_eq!(
634 yaml_serde::from_str::<Permissions>(perm).unwrap(),
635 Permissions::Explicit(IndexMap::from([(
636 "security-events".into(),
637 Permission::Write
638 )]))
639 );
640 }
641
642 #[test]
643 fn test_env_empty_value() {
644 let env = "foo:";
645 assert_eq!(
646 yaml_serde::from_str::<Env>(env).unwrap()["foo"],
647 EnvValue::String("".into())
648 );
649 }
650
651 #[test]
652 fn test_env_value_csharp_trueish() {
653 let vectors = [
654 (EnvValue::Boolean(true), true),
655 (EnvValue::Boolean(false), false),
656 (EnvValue::String("true".to_string()), true),
657 (EnvValue::String("TRUE".to_string()), true),
658 (EnvValue::String("TrUe".to_string()), true),
659 (EnvValue::String(" true ".to_string()), true),
660 (EnvValue::String(" \n\r\t True\n\n".to_string()), true),
661 (EnvValue::String("false".to_string()), false),
662 (EnvValue::String("1".to_string()), false),
663 (EnvValue::String("yes".to_string()), false),
664 (EnvValue::String("on".to_string()), false),
665 (EnvValue::String("random".to_string()), false),
666 (EnvValue::Number(1.0), false),
667 (EnvValue::Number(0.0), false),
668 (EnvValue::Number(666.0), false),
669 ];
670
671 for (val, expected) in vectors {
672 assert_eq!(val.csharp_bool(), expected, "failed for {val:?}");
673 }
674 }
675
676 #[test]
677 fn test_uses_parses() {
678 insta::assert_debug_snapshot!(
680 Uses::parse("actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
681 @r#"
682 Repository(
683 RepositoryUses {
684 owner: "actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
685 dependent: RepositoryUsesInner {
686 owner: "actions",
687 repo: "checkout",
688 slug: "actions/checkout",
689 subpath: None,
690 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
691 },
692 },
693 )
694 "#,
695 );
696
697 insta::assert_debug_snapshot!(
699 Uses::parse("actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
700 @r#"
701 Repository(
702 RepositoryUses {
703 owner: "actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
704 dependent: RepositoryUsesInner {
705 owner: "actions",
706 repo: "aws",
707 slug: "actions/aws",
708 subpath: Some(
709 "ec2",
710 ),
711 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
712 },
713 },
714 )
715 "#
716 );
717
718 insta::assert_debug_snapshot!(
720 Uses::parse("example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
721 @r#"
722 Repository(
723 RepositoryUses {
724 owner: "example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
725 dependent: RepositoryUsesInner {
726 owner: "example",
727 repo: "foo",
728 slug: "example/foo",
729 subpath: Some(
730 "bar/baz/quux",
731 ),
732 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
733 },
734 },
735 )
736 "#
737 );
738
739 insta::assert_debug_snapshot!(
741 Uses::parse("actions/checkout@v4").unwrap(),
742 @r#"
743 Repository(
744 RepositoryUses {
745 owner: "actions/checkout@v4",
746 dependent: RepositoryUsesInner {
747 owner: "actions",
748 repo: "checkout",
749 slug: "actions/checkout",
750 subpath: None,
751 git_ref: "v4",
752 },
753 },
754 )
755 "#
756 );
757
758 insta::assert_debug_snapshot!(
759 Uses::parse("actions/checkout@abcd").unwrap(),
760 @r#"
761 Repository(
762 RepositoryUses {
763 owner: "actions/checkout@abcd",
764 dependent: RepositoryUsesInner {
765 owner: "actions",
766 repo: "checkout",
767 slug: "actions/checkout",
768 subpath: None,
769 git_ref: "abcd",
770 },
771 },
772 )
773 "#
774 );
775
776 insta::assert_debug_snapshot!(
778 Uses::parse("actions/checkout").unwrap_err(),
779 @r#"
780 UsesError(
781 "missing `@<ref>` in actions/checkout",
782 )
783 "#
784 );
785
786 insta::assert_debug_snapshot!(
788 Uses::parse("docker://alpine:3.8").unwrap(),
789 @r#"
790 Docker(
791 DockerUses {
792 owner: "alpine:3.8",
793 dependent: DockerUsesInner {
794 registry: None,
795 image: "alpine",
796 tag: Some(
797 "3.8",
798 ),
799 hash: None,
800 },
801 },
802 )
803 "#
804 );
805
806 insta::assert_debug_snapshot!(
808 Uses::parse("docker://localhost/alpine:3.8").unwrap(),
809 @r#"
810 Docker(
811 DockerUses {
812 owner: "localhost/alpine:3.8",
813 dependent: DockerUsesInner {
814 registry: Some(
815 "localhost",
816 ),
817 image: "alpine",
818 tag: Some(
819 "3.8",
820 ),
821 hash: None,
822 },
823 },
824 )
825 "#
826 );
827
828 insta::assert_debug_snapshot!(
830 Uses::parse("docker://localhost:1337/alpine:3.8").unwrap(),
831 @r#"
832 Docker(
833 DockerUses {
834 owner: "localhost:1337/alpine:3.8",
835 dependent: DockerUsesInner {
836 registry: Some(
837 "localhost:1337",
838 ),
839 image: "alpine",
840 tag: Some(
841 "3.8",
842 ),
843 hash: None,
844 },
845 },
846 )
847 "#
848 );
849
850 insta::assert_debug_snapshot!(
852 Uses::parse("docker://ghcr.io/foo/alpine:3.8").unwrap(),
853 @r#"
854 Docker(
855 DockerUses {
856 owner: "ghcr.io/foo/alpine:3.8",
857 dependent: DockerUsesInner {
858 registry: Some(
859 "ghcr.io",
860 ),
861 image: "foo/alpine",
862 tag: Some(
863 "3.8",
864 ),
865 hash: None,
866 },
867 },
868 )
869 "#
870 );
871
872 insta::assert_debug_snapshot!(
874 Uses::parse("docker://ghcr.io/foo/alpine").unwrap(),
875 @r#"
876 Docker(
877 DockerUses {
878 owner: "ghcr.io/foo/alpine",
879 dependent: DockerUsesInner {
880 registry: Some(
881 "ghcr.io",
882 ),
883 image: "foo/alpine",
884 tag: None,
885 hash: None,
886 },
887 },
888 )
889 "#
890 );
891
892 insta::assert_debug_snapshot!(
894 Uses::parse("docker://ghcr.io/foo/alpine:").unwrap(),
895 @r#"
896 Docker(
897 DockerUses {
898 owner: "ghcr.io/foo/alpine:",
899 dependent: DockerUsesInner {
900 registry: Some(
901 "ghcr.io",
902 ),
903 image: "foo/alpine",
904 tag: None,
905 hash: None,
906 },
907 },
908 )
909 "#
910 );
911
912 insta::assert_debug_snapshot!(
914 Uses::parse("docker://alpine").unwrap(),
915 @r#"
916 Docker(
917 DockerUses {
918 owner: "alpine",
919 dependent: DockerUsesInner {
920 registry: None,
921 image: "alpine",
922 tag: None,
923 hash: None,
924 },
925 },
926 )
927 "#
928 );
929
930 insta::assert_debug_snapshot!(
932 Uses::parse("docker://alpine@hash").unwrap(),
933 @r#"
934 Docker(
935 DockerUses {
936 owner: "alpine@hash",
937 dependent: DockerUsesInner {
938 registry: None,
939 image: "alpine",
940 tag: None,
941 hash: Some(
942 "hash",
943 ),
944 },
945 },
946 )
947 "#
948 );
949
950 insta::assert_debug_snapshot!(
952 Uses::parse("./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89").unwrap(),
953 @r#"
954 Local(
955 LocalUses {
956 path: "./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89",
957 },
958 )
959 "#
960 );
961
962 insta::assert_debug_snapshot!(
964 Uses::parse("./.github/actions/hello-world-action").unwrap(),
965 @r#"
966 Local(
967 LocalUses {
968 path: "./.github/actions/hello-world-action",
969 },
970 )
971 "#
972 );
973
974 insta::assert_debug_snapshot!(
976 Uses::parse("$/.github/actions/hello-world-action").unwrap(),
977 @r#"
978 Local(
979 LocalUses {
980 path: "$/.github/actions/hello-world-action",
981 },
982 )
983 "#
984 );
985
986 insta::assert_debug_snapshot!(
988 Uses::parse("checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap_err(),
989 @r#"
990 UsesError(
991 "owner/repo slug is too short: checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
992 )
993 "#
994 );
995
996 insta::assert_debug_snapshot!(
998 Uses::parse("\nactions/checkout@v4 \n").unwrap(),
999 @r#"
1000 Repository(
1001 RepositoryUses {
1002 owner: "actions/checkout@v4",
1003 dependent: RepositoryUsesInner {
1004 owner: "actions",
1005 repo: "checkout",
1006 slug: "actions/checkout",
1007 subpath: None,
1008 git_ref: "v4",
1009 },
1010 },
1011 )
1012 "#,
1013 );
1014
1015 insta::assert_debug_snapshot!(
1016 Uses::parse("\ndocker://alpine:3.8 \n").unwrap(),
1017 @r#"
1018 Docker(
1019 DockerUses {
1020 owner: "alpine:3.8",
1021 dependent: DockerUsesInner {
1022 registry: None,
1023 image: "alpine",
1024 tag: Some(
1025 "3.8",
1026 ),
1027 hash: None,
1028 },
1029 },
1030 )
1031 "#
1032 );
1033
1034 insta::assert_debug_snapshot!(
1035 Uses::parse("\n./.github/workflows/example.yml \n").unwrap(),
1036 @r#"
1037 Local(
1038 LocalUses {
1039 path: "./.github/workflows/example.yml",
1040 },
1041 )
1042 "#
1043 );
1044 }
1045
1046 #[test]
1047 fn test_uses_deser_reusable() {
1048 #[derive(Deserialize)]
1050 #[serde(transparent)]
1051 struct Dummy(#[serde(deserialize_with = "reusable_step_uses")] Uses);
1052
1053 insta::assert_debug_snapshot!(
1054 yaml_serde::from_str::<Dummy>(
1055 "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1056 )
1057 .map(|d| d.0)
1058 .unwrap(),
1059 @r#"
1060 Repository(
1061 RepositoryUses {
1062 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89",
1063 dependent: RepositoryUsesInner {
1064 owner: "octo-org",
1065 repo: "this-repo",
1066 slug: "octo-org/this-repo",
1067 subpath: Some(
1068 ".github/workflows/workflow-1.yml",
1069 ),
1070 git_ref: "172239021f7ba04fe7327647b213799853a9eb89",
1071 },
1072 },
1073 )
1074 "#
1075 );
1076
1077 insta::assert_debug_snapshot!(
1078 yaml_serde::from_str::<Dummy>(
1079 "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash"
1080 ).map(|d| d.0).unwrap(),
1081 @r#"
1082 Repository(
1083 RepositoryUses {
1084 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash",
1085 dependent: RepositoryUsesInner {
1086 owner: "octo-org",
1087 repo: "this-repo",
1088 slug: "octo-org/this-repo",
1089 subpath: Some(
1090 ".github/workflows/workflow-1.yml",
1091 ),
1092 git_ref: "notahash",
1093 },
1094 },
1095 )
1096 "#
1097 );
1098
1099 insta::assert_debug_snapshot!(
1100 yaml_serde::from_str::<Dummy>(
1101 "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd"
1102 ).map(|d| d.0).unwrap(),
1103 @r#"
1104 Repository(
1105 RepositoryUses {
1106 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd",
1107 dependent: RepositoryUsesInner {
1108 owner: "octo-org",
1109 repo: "this-repo",
1110 slug: "octo-org/this-repo",
1111 subpath: Some(
1112 ".github/workflows/workflow-1.yml",
1113 ),
1114 git_ref: "abcd",
1115 },
1116 },
1117 )
1118 "#
1119 );
1120
1121 insta::assert_debug_snapshot!(
1123 yaml_serde::from_str::<Dummy>(
1124 "octo-org/this-repo/.github/workflows/workflow-1.yml"
1125 ).map(|d| d.0).unwrap_err(),
1126 @r#"Error("malformed `uses` ref: missing `@<ref>` in octo-org/this-repo/.github/workflows/workflow-1.yml")"#
1127 );
1128
1129 insta::assert_debug_snapshot!(
1131 yaml_serde::from_str::<Dummy>(
1132 "./.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1133 ).map(|d| d.0).unwrap_err(),
1134 @r#"Error("local reusable workflow reference can't specify `@<ref>`")"#
1135 );
1136
1137 insta::assert_debug_snapshot!(
1139 yaml_serde::from_str::<Dummy>(
1140 ".github/workflows/workflow-1.yml"
1141 ).map(|d| d.0).unwrap_err(),
1142 @r#"Error("malformed `uses` ref: missing `@<ref>` in .github/workflows/workflow-1.yml")"#
1143 );
1144
1145 insta::assert_debug_snapshot!(
1147 yaml_serde::from_str::<Dummy>(
1148 "workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1149 ).map(|d| d.0).unwrap_err(),
1150 @r#"Error("malformed `uses` ref: owner/repo slug is too short: workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89")"#
1151 );
1152 }
1153
1154 #[test]
1155 fn test_bool_or_unit() {
1156 #[derive(Deserialize)]
1157 struct Dummy {
1158 #[serde(deserialize_with = "crate::common::bool_or_unit")]
1159 x: bool,
1160 }
1161
1162 assert!(yaml_serde::from_str::<Dummy>("x:").unwrap().x);
1163 assert!(yaml_serde::from_str::<Dummy>("x: null").unwrap().x);
1165 assert!(yaml_serde::from_str::<Dummy>("x: true").unwrap().x);
1166 assert!(!yaml_serde::from_str::<Dummy>("x: false").unwrap().x)
1167 }
1168}