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