celcat/
entities.rs

1//! # *Resources* and *entities*
2//!
3//! Celcat often require a *resource* type (like [`Student`]) in the request to
4//! know what it must send back, and sometimes a *resource* ID (like [`StudentId`])
5//! which identifies a particular resource.
6//! Calcat sends back *entities*, identified by an *entity* type and an *entity* ID.
7//!
8//! An *entity* can be a *resource*, in which case it has an associated ID.
9//! If it isn't a *resource*, is doesn't have an ID (`null` in JSON),
10//! and we represent its type with [`Unknown`], and its ID with [`UnknownId`].
11
12use std::{concat, convert::TryFrom, fmt::Debug, stringify};
13
14use paste::paste;
15use serde::{Deserialize, Serialize};
16
17/// A *resource* type.
18///
19/// This trait cannot be implemented outside of this crate.
20pub trait ResourceType:
21    Debug + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + private::Sealed
22{
23    type Id: ResourceId;
24}
25
26/// A *resource* ID.
27///
28/// This trait cannot be implemented outside of this crate.
29pub trait ResourceId: EntityId {}
30
31/// An *entity* type.
32///
33/// This trait cannot be implemented outside of this crate.
34pub trait EntityType:
35    Debug + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + private::Sealed
36{
37    type Id: EntityId;
38}
39
40/// An *entity* ID.
41///
42/// This trait cannot be implemented outside of this crate.
43pub trait EntityId:
44    Debug + Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + private::Sealed
45{
46}
47
48macro_rules! if_unknown {
49    (
50        if Unknown {
51            $($i:item)*
52        } else {
53            $($_:item)*
54        }
55    ) => {
56        $($i)*
57    };
58    (
59        if $not_unknown:ident {
60            $($_:item)*
61        } else {
62            $($i:item)*
63        }
64    ) => {
65        $($i)*
66    };
67}
68
69macro_rules! if_not_unknown {
70    (
71        if Unknown {
72            $($_:item)*
73        }
74    ) => { };
75    (
76        if $not_unknown:ident {
77            $($i:item)*
78        }
79    ) => {
80        $($i)*
81    };
82}
83
84macro_rules! entities {
85    (
86        $(
87            $r:ident = $n:literal,
88        )+
89    ) => {
90        $(
91            paste! {
92                // TODO: find a way to not write definitions 2 times
93                if_unknown! {
94                    if $r {
95                        #[doc = "The unknown entity type."]
96                        #[derive(Debug, Default, Clone, Copy, PartialEq)]
97                        #[derive(Serialize, Deserialize)]
98                        #[serde(try_from = "u8", into = "u8")]
99                        pub struct $r;
100                    } else {
101                        #[doc = "The " $r:lower " resource type."]
102                        #[derive(Debug, Default, Clone, Copy, PartialEq)]
103                        #[derive(Serialize, Deserialize)]
104                        #[serde(try_from = "u8", into = "u8")]
105                        pub struct $r;
106                    }
107                }
108
109                impl From<$r> for u8 {
110                    fn from(_: $r) -> Self {
111                        $n
112                    }
113                }
114
115                impl TryFrom<u8> for $r {
116                    type Error = &'static str;
117                    fn try_from(n: u8) -> Result<Self, Self::Error> {
118                        if n == $n {
119                            Ok($r)
120                        } else {
121                            Err(concat!("expected ", $n, " (", stringify!($r), ")"))
122                        }
123                    }
124                }
125
126                impl private::Sealed for $r {}
127                impl EntityType for $r {
128                    type Id = [<$r Id>];
129                }
130                if_not_unknown! {
131                    if $r {
132                        impl ResourceType for $r {
133                            type Id = [<$r Id>];
134                        }
135                    }
136                }
137
138                if_unknown! {
139                    if $r {
140                        #[derive(Debug, Default, Clone, PartialEq)]
141                        #[derive(Serialize, Deserialize)]
142                        #[serde(from = "()", into = "()")]
143                        pub struct [<$r Id>];
144                        impl From<[<$r Id>]> for () {
145                            fn from(_: [<$r Id>]) -> Self {}
146                        }
147                        impl From<()> for [<$r Id>] {
148                            fn from(_: ()) -> Self {
149                                Self
150                            }
151                        }
152                    } else {
153                        #[derive(Debug, Default, Clone, PartialEq)]
154                        #[derive(Serialize, Deserialize)]
155                        #[repr(transparent)]
156                        pub struct [<$r Id>](pub String);
157                    }
158                }
159
160                impl private::Sealed for [<$r Id>] {}
161                impl EntityId for [<$r Id>] {}
162                if_not_unknown! {
163                    if $r {
164                        impl ResourceId for [<$r Id>] {}
165                    }
166                }
167            }
168        )+
169    };
170}
171
172entities! {
173    Unknown = 0,
174    Module = 100,
175    Staff = 101,
176    Room = 102,
177    Group = 103,
178    Student = 104,
179    Team = 105,
180    Equipment = 106,
181    Course = 107,
182}
183
184mod private {
185    /// Empty trait that no struct/enum can implement outside of this crate.
186    ///
187    /// Used as a trait bound for traits that shouldn't be implemented outside of this crate.
188    pub trait Sealed {}
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use serde_json::{from_value, json, to_value};
195
196    #[test]
197    fn serialize_entity_type() {
198        assert_eq!(to_value(Unknown).unwrap(), json!(0));
199        assert_eq!(to_value(Student).unwrap(), json!(104));
200    }
201
202    #[test]
203    fn deserialize_entity_type() {
204        from_value::<Unknown>(json!(0)).unwrap();
205        from_value::<Group>(json!(103)).unwrap();
206        from_value::<Unknown>(json!(100)).unwrap_err();
207        from_value::<Staff>(json!(null)).unwrap_err();
208    }
209
210    #[test]
211    fn serialize_unknown_id() {
212        assert_eq!(to_value(UnknownId).unwrap(), json!(null));
213    }
214
215    #[test]
216    fn deserialize_unknown_id() {
217        from_value::<UnknownId>(json!(null)).unwrap();
218        from_value::<UnknownId>(json!(100)).unwrap_err();
219    }
220
221    #[test]
222    fn serialize_room_id() {
223        assert_eq!(
224            to_value(RoomId("1173077".to_owned())).unwrap(),
225            json!("1173077")
226        );
227    }
228
229    #[test]
230    fn deserialize_room_id() {
231        assert_eq!(
232            from_value::<RoomId>(json!("1172947")).unwrap(),
233            RoomId("1172947".to_owned())
234        );
235        from_value::<RoomId>(json!(1172976)).unwrap_err();
236    }
237}