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.path,
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 pub 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
332#[derive(Debug, PartialEq)]
333struct RepositoryUsesInner<'a> {
334 owner: &'a str,
336 repo: &'a str,
338 slug: &'a str,
340 subpath: Option<&'a str>,
342 git_ref: &'a str,
344}
345
346impl<'a> RepositoryUsesInner<'a> {
347 fn from_str(uses: &'a str) -> Result<Self, UsesError> {
348 let uses = uses.trim();
350
351 let (path, git_ref) = match uses.rsplit_once('@') {
354 Some((path, git_ref)) => (path, git_ref),
355 None => return Err(UsesError(format!("missing `@<ref>` in {uses}"))),
356 };
357
358 let mut components = path.splitn(3, '/');
359
360 if let Some(owner) = components.next()
361 && let Some(repo) = components.next()
362 {
363 let subpath = components.next();
364
365 let slug = if subpath.is_none() {
366 path
367 } else {
368 &path[..owner.len() + 1 + repo.len()]
369 };
370
371 Ok(RepositoryUsesInner {
372 owner,
373 repo,
374 slug,
375 subpath,
376 git_ref,
377 })
378 } else {
379 Err(UsesError(format!("owner/repo slug is too short: {uses}")))
380 }
381 }
382}
383
384self_cell!(
385 pub struct RepositoryUses {
387 owner: String,
388
389 #[covariant]
390 dependent: RepositoryUsesInner,
391 }
392
393 impl {Debug, PartialEq}
394);
395
396impl Display for RepositoryUses {
397 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398 write!(f, "{}", self.raw())
399 }
400}
401
402impl RepositoryUses {
403 pub fn parse(uses: impl Into<String>) -> Result<Self, UsesError> {
405 RepositoryUses::try_new(uses.into(), |s| {
406 let inner = RepositoryUsesInner::from_str(s)?;
407 Ok(inner)
408 })
409 }
410
411 pub fn raw(&self) -> &str {
413 self.borrow_owner()
414 }
415
416 pub fn owner(&self) -> &str {
418 self.borrow_dependent().owner
419 }
420
421 pub fn repo(&self) -> &str {
423 self.borrow_dependent().repo
424 }
425
426 pub fn slug(&self) -> &str {
428 self.borrow_dependent().slug
429 }
430
431 pub fn subpath(&self) -> Option<&str> {
433 self.borrow_dependent().subpath
434 }
435
436 pub fn git_ref(&self) -> &str {
438 self.borrow_dependent().git_ref
439 }
440}
441
442#[derive(Debug, PartialEq)]
443#[non_exhaustive]
444pub struct DockerUsesInner<'a> {
445 registry: Option<&'a str>,
447 image: &'a str,
449 tag: Option<&'a str>,
451 hash: Option<&'a str>,
453}
454
455impl<'a> DockerUsesInner<'a> {
456 fn is_registry(registry: &str) -> bool {
457 registry == "localhost" || registry.contains('.') || registry.contains(':')
459 }
460
461 fn from_str(uses: &'a str) -> Self {
462 let uses = uses.trim();
464
465 let (registry, image) = match uses.split_once('/') {
466 Some((registry, image)) if Self::is_registry(registry) => (Some(registry), image),
467 _ => (None, uses),
468 };
469
470 if let Some(at_pos) = image.find('@') {
474 let (image, hash) = image.split_at(at_pos);
475
476 let hash = if hash.is_empty() {
477 None
478 } else {
479 Some(&hash[1..])
480 };
481
482 DockerUsesInner {
483 registry,
484 image,
485 tag: None,
486 hash,
487 }
488 } else {
489 let (image, tag) = match image.split_once(':') {
490 Some((image, "")) => (image, None),
491 Some((image, tag)) => (image, Some(tag)),
492 _ => (image, None),
493 };
494
495 DockerUsesInner {
496 registry,
497 image,
498 tag,
499 hash: None,
500 }
501 }
502 }
503}
504
505self_cell!(
506 pub struct DockerUses {
508 owner: String,
509
510 #[covariant]
511 dependent: DockerUsesInner,
512 }
513
514 impl {Debug, PartialEq}
515);
516
517impl DockerUses {
518 pub fn parse(uses: impl Into<String>) -> Self {
520 DockerUses::new(uses.into(), |s| DockerUsesInner::from_str(s))
521 }
522
523 pub fn raw(&self) -> &str {
525 self.borrow_owner()
526 }
527
528 pub fn registry(&self) -> Option<&str> {
530 self.borrow_dependent().registry
531 }
532
533 pub fn image(&self) -> &str {
535 self.borrow_dependent().image
536 }
537
538 pub fn tag(&self) -> Option<&str> {
540 self.borrow_dependent().tag
541 }
542
543 pub fn hash(&self) -> Option<&str> {
545 self.borrow_dependent().hash
546 }
547}
548
549impl<'de> Deserialize<'de> for DockerUses {
550 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
551 where
552 D: Deserializer<'de>,
553 {
554 let uses = <Cow<'de, str>>::deserialize(deserializer)?;
555 Ok(DockerUses::parse(uses))
556 }
557}
558
559pub(crate) fn custom_error<'de, D>(msg: impl Display) -> D::Error
565where
566 D: Deserializer<'de>,
567{
568 let msg = msg.to_string();
569 tracing::error!(msg);
570 de::Error::custom(msg)
571}
572
573pub(crate) fn step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
575where
576 D: Deserializer<'de>,
577{
578 let uses = <Cow<'de, str>>::deserialize(de)?;
579 Uses::parse(uses).map_err(custom_error::<D>)
580}
581
582pub(crate) fn reusable_step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
584where
585 D: Deserializer<'de>,
586{
587 let uses = step_uses(de)?;
588
589 match uses {
590 Uses::Repository(_) => Ok(uses),
591 Uses::Local(ref local) => {
592 if local.path.contains('@') {
597 Err(custom_error::<D>(
598 "local reusable workflow reference can't specify `@<ref>`",
599 ))
600 } else {
601 Ok(uses)
602 }
603 }
604 Uses::Docker(_) => Err(custom_error::<D>(
606 "docker action invalid in reusable workflow `uses`",
607 )),
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use indexmap::IndexMap;
614 use serde::Deserialize;
615
616 use crate::common::{BasePermission, Env, EnvValue, Permission};
617
618 use super::{Permissions, Uses, reusable_step_uses};
619
620 #[test]
621 fn test_permissions() {
622 assert_eq!(
623 yaml_serde::from_str::<Permissions>("read-all").unwrap(),
624 Permissions::Base(BasePermission::ReadAll)
625 );
626
627 let perm = "security-events: write";
628 assert_eq!(
629 yaml_serde::from_str::<Permissions>(perm).unwrap(),
630 Permissions::Explicit(IndexMap::from([(
631 "security-events".into(),
632 Permission::Write
633 )]))
634 );
635 }
636
637 #[test]
638 fn test_env_empty_value() {
639 let env = "foo:";
640 assert_eq!(
641 yaml_serde::from_str::<Env>(env).unwrap()["foo"],
642 EnvValue::String("".into())
643 );
644 }
645
646 #[test]
647 fn test_env_value_csharp_trueish() {
648 let vectors = [
649 (EnvValue::Boolean(true), true),
650 (EnvValue::Boolean(false), false),
651 (EnvValue::String("true".to_string()), true),
652 (EnvValue::String("TRUE".to_string()), true),
653 (EnvValue::String("TrUe".to_string()), true),
654 (EnvValue::String(" true ".to_string()), true),
655 (EnvValue::String(" \n\r\t True\n\n".to_string()), true),
656 (EnvValue::String("false".to_string()), false),
657 (EnvValue::String("1".to_string()), false),
658 (EnvValue::String("yes".to_string()), false),
659 (EnvValue::String("on".to_string()), false),
660 (EnvValue::String("random".to_string()), false),
661 (EnvValue::Number(1.0), false),
662 (EnvValue::Number(0.0), false),
663 (EnvValue::Number(666.0), false),
664 ];
665
666 for (val, expected) in vectors {
667 assert_eq!(val.csharp_bool(), expected, "failed for {val:?}");
668 }
669 }
670
671 #[test]
672 fn test_uses_parses() {
673 insta::assert_debug_snapshot!(
675 Uses::parse("actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
676 @r#"
677 Repository(
678 RepositoryUses {
679 owner: "actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
680 dependent: RepositoryUsesInner {
681 owner: "actions",
682 repo: "checkout",
683 slug: "actions/checkout",
684 subpath: None,
685 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
686 },
687 },
688 )
689 "#,
690 );
691
692 insta::assert_debug_snapshot!(
694 Uses::parse("actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
695 @r#"
696 Repository(
697 RepositoryUses {
698 owner: "actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
699 dependent: RepositoryUsesInner {
700 owner: "actions",
701 repo: "aws",
702 slug: "actions/aws",
703 subpath: Some(
704 "ec2",
705 ),
706 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
707 },
708 },
709 )
710 "#
711 );
712
713 insta::assert_debug_snapshot!(
715 Uses::parse("example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
716 @r#"
717 Repository(
718 RepositoryUses {
719 owner: "example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
720 dependent: RepositoryUsesInner {
721 owner: "example",
722 repo: "foo",
723 slug: "example/foo",
724 subpath: Some(
725 "bar/baz/quux",
726 ),
727 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
728 },
729 },
730 )
731 "#
732 );
733
734 insta::assert_debug_snapshot!(
736 Uses::parse("actions/checkout@v4").unwrap(),
737 @r#"
738 Repository(
739 RepositoryUses {
740 owner: "actions/checkout@v4",
741 dependent: RepositoryUsesInner {
742 owner: "actions",
743 repo: "checkout",
744 slug: "actions/checkout",
745 subpath: None,
746 git_ref: "v4",
747 },
748 },
749 )
750 "#
751 );
752
753 insta::assert_debug_snapshot!(
754 Uses::parse("actions/checkout@abcd").unwrap(),
755 @r#"
756 Repository(
757 RepositoryUses {
758 owner: "actions/checkout@abcd",
759 dependent: RepositoryUsesInner {
760 owner: "actions",
761 repo: "checkout",
762 slug: "actions/checkout",
763 subpath: None,
764 git_ref: "abcd",
765 },
766 },
767 )
768 "#
769 );
770
771 insta::assert_debug_snapshot!(
773 Uses::parse("actions/checkout").unwrap_err(),
774 @r#"
775 UsesError(
776 "missing `@<ref>` in actions/checkout",
777 )
778 "#
779 );
780
781 insta::assert_debug_snapshot!(
783 Uses::parse("docker://alpine:3.8").unwrap(),
784 @r#"
785 Docker(
786 DockerUses {
787 owner: "alpine:3.8",
788 dependent: DockerUsesInner {
789 registry: None,
790 image: "alpine",
791 tag: Some(
792 "3.8",
793 ),
794 hash: None,
795 },
796 },
797 )
798 "#
799 );
800
801 insta::assert_debug_snapshot!(
803 Uses::parse("docker://localhost/alpine:3.8").unwrap(),
804 @r#"
805 Docker(
806 DockerUses {
807 owner: "localhost/alpine:3.8",
808 dependent: DockerUsesInner {
809 registry: Some(
810 "localhost",
811 ),
812 image: "alpine",
813 tag: Some(
814 "3.8",
815 ),
816 hash: None,
817 },
818 },
819 )
820 "#
821 );
822
823 insta::assert_debug_snapshot!(
825 Uses::parse("docker://localhost:1337/alpine:3.8").unwrap(),
826 @r#"
827 Docker(
828 DockerUses {
829 owner: "localhost:1337/alpine:3.8",
830 dependent: DockerUsesInner {
831 registry: Some(
832 "localhost:1337",
833 ),
834 image: "alpine",
835 tag: Some(
836 "3.8",
837 ),
838 hash: None,
839 },
840 },
841 )
842 "#
843 );
844
845 insta::assert_debug_snapshot!(
847 Uses::parse("docker://ghcr.io/foo/alpine:3.8").unwrap(),
848 @r#"
849 Docker(
850 DockerUses {
851 owner: "ghcr.io/foo/alpine:3.8",
852 dependent: DockerUsesInner {
853 registry: Some(
854 "ghcr.io",
855 ),
856 image: "foo/alpine",
857 tag: Some(
858 "3.8",
859 ),
860 hash: None,
861 },
862 },
863 )
864 "#
865 );
866
867 insta::assert_debug_snapshot!(
869 Uses::parse("docker://ghcr.io/foo/alpine").unwrap(),
870 @r#"
871 Docker(
872 DockerUses {
873 owner: "ghcr.io/foo/alpine",
874 dependent: DockerUsesInner {
875 registry: Some(
876 "ghcr.io",
877 ),
878 image: "foo/alpine",
879 tag: None,
880 hash: None,
881 },
882 },
883 )
884 "#
885 );
886
887 insta::assert_debug_snapshot!(
889 Uses::parse("docker://ghcr.io/foo/alpine:").unwrap(),
890 @r#"
891 Docker(
892 DockerUses {
893 owner: "ghcr.io/foo/alpine:",
894 dependent: DockerUsesInner {
895 registry: Some(
896 "ghcr.io",
897 ),
898 image: "foo/alpine",
899 tag: None,
900 hash: None,
901 },
902 },
903 )
904 "#
905 );
906
907 insta::assert_debug_snapshot!(
909 Uses::parse("docker://alpine").unwrap(),
910 @r#"
911 Docker(
912 DockerUses {
913 owner: "alpine",
914 dependent: DockerUsesInner {
915 registry: None,
916 image: "alpine",
917 tag: None,
918 hash: None,
919 },
920 },
921 )
922 "#
923 );
924
925 insta::assert_debug_snapshot!(
927 Uses::parse("docker://alpine@hash").unwrap(),
928 @r#"
929 Docker(
930 DockerUses {
931 owner: "alpine@hash",
932 dependent: DockerUsesInner {
933 registry: None,
934 image: "alpine",
935 tag: None,
936 hash: Some(
937 "hash",
938 ),
939 },
940 },
941 )
942 "#
943 );
944
945 insta::assert_debug_snapshot!(
947 Uses::parse("./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89").unwrap(),
948 @r#"
949 Local(
950 LocalUses {
951 path: "./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89",
952 },
953 )
954 "#
955 );
956
957 insta::assert_debug_snapshot!(
959 Uses::parse("./.github/actions/hello-world-action").unwrap(),
960 @r#"
961 Local(
962 LocalUses {
963 path: "./.github/actions/hello-world-action",
964 },
965 )
966 "#
967 );
968
969 insta::assert_debug_snapshot!(
971 Uses::parse("$/.github/actions/hello-world-action").unwrap(),
972 @r#"
973 Local(
974 LocalUses {
975 path: "$/.github/actions/hello-world-action",
976 },
977 )
978 "#
979 );
980
981 insta::assert_debug_snapshot!(
983 Uses::parse("checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap_err(),
984 @r#"
985 UsesError(
986 "owner/repo slug is too short: checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
987 )
988 "#
989 );
990
991 insta::assert_debug_snapshot!(
993 Uses::parse("\nactions/checkout@v4 \n").unwrap(),
994 @r#"
995 Repository(
996 RepositoryUses {
997 owner: "actions/checkout@v4",
998 dependent: RepositoryUsesInner {
999 owner: "actions",
1000 repo: "checkout",
1001 slug: "actions/checkout",
1002 subpath: None,
1003 git_ref: "v4",
1004 },
1005 },
1006 )
1007 "#,
1008 );
1009
1010 insta::assert_debug_snapshot!(
1011 Uses::parse("\ndocker://alpine:3.8 \n").unwrap(),
1012 @r#"
1013 Docker(
1014 DockerUses {
1015 owner: "alpine:3.8",
1016 dependent: DockerUsesInner {
1017 registry: None,
1018 image: "alpine",
1019 tag: Some(
1020 "3.8",
1021 ),
1022 hash: None,
1023 },
1024 },
1025 )
1026 "#
1027 );
1028
1029 insta::assert_debug_snapshot!(
1030 Uses::parse("\n./.github/workflows/example.yml \n").unwrap(),
1031 @r#"
1032 Local(
1033 LocalUses {
1034 path: "./.github/workflows/example.yml",
1035 },
1036 )
1037 "#
1038 );
1039 }
1040
1041 #[test]
1042 fn test_uses_deser_reusable() {
1043 #[derive(Deserialize)]
1045 #[serde(transparent)]
1046 struct Dummy(#[serde(deserialize_with = "reusable_step_uses")] Uses);
1047
1048 insta::assert_debug_snapshot!(
1049 yaml_serde::from_str::<Dummy>(
1050 "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1051 )
1052 .map(|d| d.0)
1053 .unwrap(),
1054 @r#"
1055 Repository(
1056 RepositoryUses {
1057 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89",
1058 dependent: RepositoryUsesInner {
1059 owner: "octo-org",
1060 repo: "this-repo",
1061 slug: "octo-org/this-repo",
1062 subpath: Some(
1063 ".github/workflows/workflow-1.yml",
1064 ),
1065 git_ref: "172239021f7ba04fe7327647b213799853a9eb89",
1066 },
1067 },
1068 )
1069 "#
1070 );
1071
1072 insta::assert_debug_snapshot!(
1073 yaml_serde::from_str::<Dummy>(
1074 "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash"
1075 ).map(|d| d.0).unwrap(),
1076 @r#"
1077 Repository(
1078 RepositoryUses {
1079 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash",
1080 dependent: RepositoryUsesInner {
1081 owner: "octo-org",
1082 repo: "this-repo",
1083 slug: "octo-org/this-repo",
1084 subpath: Some(
1085 ".github/workflows/workflow-1.yml",
1086 ),
1087 git_ref: "notahash",
1088 },
1089 },
1090 )
1091 "#
1092 );
1093
1094 insta::assert_debug_snapshot!(
1095 yaml_serde::from_str::<Dummy>(
1096 "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd"
1097 ).map(|d| d.0).unwrap(),
1098 @r#"
1099 Repository(
1100 RepositoryUses {
1101 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd",
1102 dependent: RepositoryUsesInner {
1103 owner: "octo-org",
1104 repo: "this-repo",
1105 slug: "octo-org/this-repo",
1106 subpath: Some(
1107 ".github/workflows/workflow-1.yml",
1108 ),
1109 git_ref: "abcd",
1110 },
1111 },
1112 )
1113 "#
1114 );
1115
1116 insta::assert_debug_snapshot!(
1118 yaml_serde::from_str::<Dummy>(
1119 "octo-org/this-repo/.github/workflows/workflow-1.yml"
1120 ).map(|d| d.0).unwrap_err(),
1121 @r#"Error("malformed `uses` ref: missing `@<ref>` in octo-org/this-repo/.github/workflows/workflow-1.yml")"#
1122 );
1123
1124 insta::assert_debug_snapshot!(
1126 yaml_serde::from_str::<Dummy>(
1127 "./.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1128 ).map(|d| d.0).unwrap_err(),
1129 @r#"Error("local reusable workflow reference can't specify `@<ref>`")"#
1130 );
1131
1132 insta::assert_debug_snapshot!(
1134 yaml_serde::from_str::<Dummy>(
1135 ".github/workflows/workflow-1.yml"
1136 ).map(|d| d.0).unwrap_err(),
1137 @r#"Error("malformed `uses` ref: missing `@<ref>` in .github/workflows/workflow-1.yml")"#
1138 );
1139
1140 insta::assert_debug_snapshot!(
1142 yaml_serde::from_str::<Dummy>(
1143 "workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1144 ).map(|d| d.0).unwrap_err(),
1145 @r#"Error("malformed `uses` ref: owner/repo slug is too short: workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89")"#
1146 );
1147 }
1148
1149 #[test]
1150 fn test_bool_or_unit() {
1151 #[derive(Deserialize)]
1152 struct Dummy {
1153 #[serde(deserialize_with = "crate::common::bool_or_unit")]
1154 x: bool,
1155 }
1156
1157 assert_eq!(yaml_serde::from_str::<Dummy>("x:").unwrap().x, true);
1158 assert_eq!(yaml_serde::from_str::<Dummy>("x: null").unwrap().x, true);
1160 assert_eq!(yaml_serde::from_str::<Dummy>("x: true").unwrap().x, true);
1161 assert_eq!(yaml_serde::from_str::<Dummy>("x: false").unwrap().x, false)
1162 }
1163}