Skip to main content

ephemeris_forge_github/config/rulesets/rules/
list.rs

1use ephemeris_forge::FieldDiff;
2use facet::Facet;
3
4use super::{
5    CodeScanningRule, DeploymentsRule, MergeQueueRule, PatternRule, PullRequestRule,
6    StatusChecksRule, UpdateRule, WorkflowsRule,
7};
8
9/// Define `Rules` and everything else that lists its rule types.
10///
11/// `switch` entries are rules without parameters, `rule` entries carry
12/// parameters. The entries are in the order the rules are written to GitHub.
13macro_rules! rules {
14    (
15        $($kind:ident $field:ident $(: $ty:ty)? => $variant:ident $(#[doc = $doc:literal])+;)+
16    ) => {
17        /// The rules of a ruleset. A `false` switch and a `null` rule are absent.
18        #[derive(Clone, Debug, Eq, Facet, PartialEq)]
19        #[facet(deny_unknown_fields)]
20        pub struct Rules {
21            $(
22                $(#[doc = $doc])+
23                pub $field: rules!(@field $kind $($ty)?),
24            )+
25        }
26
27        /// One rule as it is written to GitHub.
28        #[derive(Facet)]
29        #[repr(u8)]
30        #[expect(dead_code, reason = "the rule payloads are only read by the encoder")]
31        #[facet(tag = "type", content = "parameters", rename_all = "snake_case")]
32        pub(crate) enum RuleRef<'a> {
33            $($variant $((&'a $ty))?,)+
34        }
35
36        impl Rules {
37            /// Rules with every switch off and every rule absent.
38            #[must_use]
39            pub(crate) const fn none() -> Self {
40                Self { $($field: rules!(@none $kind),)+ }
41            }
42
43            /// The rules in the order they are written to GitHub.
44            pub(crate) fn entries(&self) -> Vec<RuleRef<'_>> {
45                let mut entries = Vec::new();
46                $(rules!(@push $kind entries self $field $variant);)+
47                entries
48            }
49
50            /// Read one rule of a live ruleset into the matching field.
51            ///
52            /// Returns whether the rule type is one `Rules` can hold.
53            ///
54            /// # Errors
55            /// Returns the decoder's error when the parameters do not decode.
56            pub(crate) fn absorb<D: RuleDecoder>(
57                &mut self,
58                kind: &str,
59                decoder: &D,
60            ) -> Result<bool, D::Error> {
61                match kind {
62                    $(stringify!($field) => rules!(@read $kind self decoder $field),)+
63                    _ => return Ok(false),
64                }
65                Ok(true)
66            }
67
68            /// Compare every rule with the live rules.
69            pub(crate) fn compare(&self, live: Option<&Self>, diff: &mut FieldDiff) {
70                $(diff.compare(
71                    concat!("rules.", stringify!($field)),
72                    live.map(|live| &live.$field),
73                    &self.$field,
74                );)+
75            }
76        }
77    };
78    (@field switch) => { bool };
79    (@field rule $ty:ty) => { Option<$ty> };
80    (@none switch) => { false };
81    (@none rule) => { None };
82    (@push switch $entries:ident $rules:ident $field:ident $variant:ident) => {
83        if $rules.$field {
84            $entries.push(RuleRef::$variant);
85        }
86    };
87    (@push rule $entries:ident $rules:ident $field:ident $variant:ident) => {
88        $entries.extend($rules.$field.as_ref().map(RuleRef::$variant));
89    };
90    (@read switch $rules:ident $decoder:ident $field:ident) => {
91        $rules.$field = true
92    };
93    (@read rule $rules:ident $decoder:ident $field:ident) => {
94        $rules.$field = Some($decoder.parameters()?)
95    };
96}
97
98/// Reads the parameters of one rule of a live ruleset.
99pub(crate) trait RuleDecoder {
100    /// Why the parameters cannot be read.
101    type Error;
102
103    /// Decode the parameters of the rule.
104    ///
105    /// # Errors
106    /// Returns an error when the parameters do not fit `T`.
107    fn parameters<T: for<'a> Facet<'a>>(&self) -> Result<T, Self::Error>;
108}
109
110rules! {
111    rule required_status_checks: StatusChecksRule => RequiredStatusChecks
112        #[doc = "Require status checks to pass."];
113    rule pull_request: PullRequestRule => PullRequest
114        #[doc = "Require changes to arrive through a pull request."];
115    switch required_signatures => RequiredSignatures
116        #[doc = "Require signed commits."];
117    switch non_fast_forward => NonFastForward
118        #[doc = "Block force pushes."];
119    switch deletion => Deletion
120        #[doc = "Block deleting matching refs."];
121    switch creation => Creation
122        #[doc = "Block creating matching refs."];
123    switch required_linear_history => RequiredLinearHistory
124        #[doc = "Require a linear history."];
125    rule update: UpdateRule => Update
126        #[doc = "Restrict updating matching refs."];
127    rule required_deployments: DeploymentsRule => RequiredDeployments
128        #[doc = "Require successful deployments."];
129    rule commit_message_pattern: PatternRule => CommitMessagePattern
130        #[doc = "Require commit messages to match a pattern."];
131    rule commit_author_email_pattern: PatternRule => CommitAuthorEmailPattern
132        #[doc = "Require commit author emails to match a pattern."];
133    rule committer_email_pattern: PatternRule => CommitterEmailPattern
134        #[doc = "Require committer emails to match a pattern."];
135    rule branch_name_pattern: PatternRule => BranchNamePattern
136        #[doc = "Require branch names to match a pattern."];
137    rule tag_name_pattern: PatternRule => TagNamePattern
138        #[doc = "Require tag names to match a pattern."];
139    rule workflows: WorkflowsRule => Workflows
140        #[doc = "Require workflows to pass."];
141    rule code_scanning: CodeScanningRule => CodeScanning
142        #[doc = "Require code scanning results."];
143    rule merge_queue: MergeQueueRule => MergeQueue
144        #[doc = "Require a merge queue."];
145}