automatons_github/
macros.rs

1/// Generate an identifier type
2///
3/// GitHub uses unique numerical ids for most of its resources. The [`id!`] macro generates a struct
4/// that represents such an identifier, making it easy to generate unique types for different
5/// resources.
6///
7/// # Example
8///
9/// ```rust
10/// use automatons_github::id;
11///
12/// id!(RepositoryId);
13/// id!(UserId);
14/// ```
15#[macro_export]
16macro_rules! id {
17    (
18        $(#[$meta:meta])*
19        $id:ident
20    ) => {
21        $(#[$meta])*
22        #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
23        #[derive(serde::Deserialize, serde::Serialize)]
24        pub struct $id(u64);
25
26        #[allow(dead_code)]
27        impl $id {
28            /// Initializes a new id.
29            #[cfg_attr(feature = "tracing", tracing::instrument)]
30            pub fn new(id: u64) -> Self {
31                Self(id)
32            }
33
34            /// Returns the inner value of the id.
35            #[cfg_attr(feature = "tracing", tracing::instrument)]
36            pub fn get(&self) -> u64 {
37                self.0
38            }
39        }
40
41        impl std::fmt::Display for $id {
42            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43                write!(f, "{}", self.0)
44            }
45        }
46
47        impl From<u64> for $id {
48            #[cfg_attr(feature = "tracing", tracing::instrument)]
49            fn from(id: u64) -> $id {
50                $id(id)
51            }
52        }
53    };
54}
55
56/// Generate a resource name
57///
58/// Many resources on GitHub have unique names that identify them. For example, user names are
59/// unique across the platform. Since these names can be used as identifiers, it is recommended to
60/// encode them using the Rust type system. This avoids passing them around as strings, and
61/// eventually using them in the wrong place.
62///
63/// The [`name!`] macro makes it easy to generate a newtype that represents a specific name.
64///
65/// # Example
66///
67/// ```rust
68/// use automatons_github::name;
69///
70/// name!(RepositoryName);
71/// name!(UserName);
72/// ```
73#[macro_export]
74macro_rules! name {
75    (
76        $(#[$meta:meta])*
77        $name:ident
78    ) => {
79        $(#[$meta])*
80        #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
81        #[derive(serde::Deserialize, serde::Serialize)]
82        pub struct $name(String);
83
84        #[allow(dead_code)]
85        impl $name {
86            /// Initializes a new name.
87            #[cfg_attr(feature = "tracing", tracing::instrument)]
88            pub fn new(name: &str) -> Self {
89                Self(name.into())
90            }
91
92            /// Returns the inner value of the name.
93            #[cfg_attr(feature = "tracing", tracing::instrument)]
94            pub fn get(&self) -> &str {
95                &self.0
96            }
97        }
98
99        impl std::fmt::Display for $name {
100            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101                write!(f, "{}", self.0)
102            }
103        }
104
105        impl From<&str> for $name {
106            #[cfg_attr(feature = "tracing", tracing::instrument)]
107            fn from(string: &str) -> $name {
108                $name(string.into())
109            }
110        }
111
112        impl From<String> for $name {
113            #[cfg_attr(feature = "tracing", tracing::instrument)]
114            fn from(string: String) -> $name {
115                $name(string)
116            }
117        }
118    };
119}
120
121/// Generate a secret type
122///
123/// GitHub Apps have several secrets that must be configured, for example the app's private key and
124/// webhook secret. The [`secret`] macro can generate a type for these secrets that both protects
125/// the value from accidental exposure and allows the Rust compiler to enforce its type safety.
126///
127/// # Example
128///
129/// ```rust
130/// use automatons_github::secret;
131///
132/// secret!(PrivateKey);
133/// secret!(WebhookSecret);
134/// ```
135#[macro_export]
136macro_rules! secret {
137    (
138        $(#[$meta:meta])*
139        $secret:ident
140    ) => {
141        $(#[$meta])*
142        #[derive(Clone, Debug)]
143        pub struct $secret(secrecy::SecretString);
144
145        #[allow(dead_code)]
146        impl $secret {
147            /// Initializes a new secret.
148            #[cfg_attr(feature = "tracing", tracing::instrument)]
149            pub fn new(secret: &str) -> Self {
150                Self(secrecy::SecretString::new(String::from(secret)))
151            }
152
153            /// Returns the inner value of the secret.
154            #[cfg_attr(feature = "tracing", tracing::instrument)]
155            pub fn expose(&self) -> &str {
156                use secrecy::ExposeSecret;
157                self.0.expose_secret()
158            }
159        }
160
161        impl std::fmt::Display for $secret {
162            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163                write!(f, "[REDACTED]")
164            }
165        }
166
167        impl From<&str> for $secret {
168            #[cfg_attr(feature = "tracing", tracing::instrument)]
169            fn from(secret: &str) -> $secret {
170                $secret(secrecy::SecretString::new(String::from(secret)))
171            }
172        }
173
174        impl From<String> for $secret {
175            #[cfg_attr(feature = "tracing", tracing::instrument)]
176            fn from(secret: String) -> $secret {
177                $secret(secrecy::SecretString::new(secret))
178            }
179        }
180    };
181}
182
183#[cfg(test)]
184mod tests {
185    use crate::{id, name};
186
187    id!(
188        /// Identifier for tests
189        TestId
190    );
191
192    #[test]
193    fn id() {
194        let id = TestId::new(42);
195
196        assert_eq!(42, id.get());
197        assert_eq!("42", id.to_string());
198    }
199
200    #[test]
201    fn id_from_u64() {
202        let _id: TestId = 42.into();
203    }
204
205    name!(
206        /// Name for tests
207        TestName
208    );
209
210    #[test]
211    fn name() {
212        let name = TestName::new("test");
213
214        assert_eq!("test", name.get());
215        assert_eq!("test", name.to_string());
216    }
217
218    #[test]
219    fn name_from_str() {
220        let _name: TestName = "test".into();
221    }
222
223    #[test]
224    fn name_from_string() {
225        let _name: TestName = String::from("test").into();
226    }
227
228    secret!(
229        /// Secret for tests
230        TestSecret
231    );
232
233    #[test]
234    fn secret() {
235        let secret = TestSecret::new("test");
236
237        assert_eq!("test", secret.expose());
238        assert_eq!("[REDACTED]", secret.to_string());
239    }
240
241    #[test]
242    fn secret_from_str() {
243        let _secret: TestSecret = "test".into();
244    }
245
246    #[test]
247    fn secret_from_string() {
248        let _secret: TestSecret = String::from("test").into();
249    }
250}