netsblox_api_common/
lib.rs

1#[cfg(feature = "bson")]
2mod bson;
3pub mod oauth;
4
5use core::fmt;
6use derive_more::{Display, Error, FromStr};
7use serde::{
8    de::{self, Visitor},
9    Deserialize, Deserializer, Serialize,
10};
11use serde_json::Value;
12use std::{collections::HashMap, str::FromStr, time::SystemTime};
13use ts_rs::TS;
14use uuid::Uuid;
15
16const APP_NAME: &str = "NetsBlox";
17
18#[derive(Deserialize, Serialize, TS)]
19#[serde(rename_all = "camelCase")]
20#[ts(export)]
21pub struct ClientConfig {
22    pub client_id: String,
23    #[ts(optional)]
24    pub username: Option<String>,
25    pub services_hosts: Vec<ServiceHost>,
26    pub cloud_url: String,
27}
28
29#[derive(Deserialize, Serialize, TS)]
30#[ts(export)]
31pub struct InvitationResponse {
32    pub response: FriendLinkState,
33}
34
35#[derive(Serialize, Deserialize, Clone, Debug, TS)]
36#[serde(rename_all = "camelCase")]
37#[ts(export)]
38pub struct User {
39    pub username: String,
40    pub email: String,
41    #[ts(optional)]
42    pub group_id: Option<GroupId>,
43    pub role: UserRole,
44    #[ts(skip)]
45    pub created_at: SystemTime,
46    pub linked_accounts: Vec<LinkedAccount>,
47    #[ts(optional)]
48    pub services_hosts: Option<Vec<ServiceHost>>,
49}
50
51#[derive(Serialize, Deserialize, TS, Clone)]
52#[serde(rename_all = "camelCase")]
53#[ts(export)]
54pub struct UpdateUserData {
55    pub email: Option<String>,
56    pub group_id: Option<GroupId>,
57    pub role: Option<UserRole>,
58}
59
60#[derive(Serialize, Deserialize, Debug, TS)]
61#[serde(rename_all = "camelCase")]
62#[ts(export)]
63pub struct NewUser {
64    pub username: String,
65    pub email: String,
66    #[ts(optional)]
67    pub password: Option<String>,
68    #[ts(optional)]
69    pub group_id: Option<GroupId>,
70    #[ts(optional)]
71    pub role: Option<UserRole>,
72}
73
74#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, TS)]
75#[serde(rename_all = "camelCase")]
76#[ts(export)]
77pub enum UserRole {
78    User,
79    Teacher,
80    Moderator,
81    Admin,
82}
83
84#[derive(Deserialize, Serialize, Clone, Debug, TS)]
85#[serde(rename_all = "camelCase")]
86#[ts(export)]
87pub struct NetworkTraceMetadata {
88    pub id: String,
89    #[ts(type = "any")] // FIXME
90    pub start_time: SystemTime,
91    #[ts(type = "any | null")] // FIXME
92    #[ts(optional)]
93    pub end_time: Option<SystemTime>,
94}
95
96#[derive(Deserialize, Serialize, Debug, Clone, TS)]
97#[serde(rename_all = "camelCase")]
98#[ts(export)]
99pub struct SentMessage {
100    pub project_id: ProjectId,
101    pub recipients: Vec<ClientState>,
102    #[ts(type = "any")] // FIXME
103    pub time: SystemTime,
104    pub source: ClientState,
105
106    #[ts(type = "any")]
107    pub content: serde_json::Value,
108}
109
110#[derive(TS, Deserialize, Serialize, Debug, Clone)]
111#[serde(rename_all = "camelCase")]
112#[ts(export)]
113pub struct OccupantInvite {
114    pub username: String,
115    pub project_id: ProjectId,
116    pub role_id: RoleId,
117    #[ts(type = "any")] // FIXME
118    pub created_at: SystemTime,
119}
120
121#[derive(Debug, Display, Error, TS)]
122#[display(fmt = "Unable to parse user role. Expected admin, moderator, or user.")]
123#[ts(export)]
124pub struct UserRoleError;
125
126impl FromStr for UserRole {
127    type Err = UserRoleError;
128    fn from_str(s: &str) -> Result<Self, Self::Err> {
129        match s {
130            "admin" => Ok(UserRole::Admin),
131            "moderator" => Ok(UserRole::Moderator),
132            "teacher" => Ok(UserRole::Teacher),
133            "user" => Ok(UserRole::User),
134            _ => Err(UserRoleError),
135        }
136    }
137}
138
139#[derive(Serialize, Deserialize, Clone, Debug, TS)]
140#[serde(rename_all = "camelCase")]
141#[ts(export)]
142pub struct ServiceHost {
143    pub url: String,
144    pub categories: Vec<String>,
145}
146
147#[derive(Serialize, Deserialize, Clone, Debug, TS)]
148#[ts(export)]
149pub struct LinkedAccount {
150    pub username: String,
151    pub strategy: String,
152}
153
154#[derive(TS, Serialize, Deserialize, Clone)]
155#[serde(rename_all = "camelCase")]
156#[ts(export)]
157pub struct BannedAccount {
158    pub username: String,
159    pub email: String,
160    #[ts(type = "any")] // FIXME
161    pub banned_at: SystemTime,
162}
163
164#[derive(Serialize, Deserialize, Debug, TS)]
165#[serde(rename_all = "camelCase")]
166#[ts(export)]
167pub struct LoginRequest {
168    pub credentials: Credentials,
169    #[ts(optional)]
170    pub client_id: Option<ClientId>, // TODO: add a secret token for the client?
171}
172
173#[derive(Deserialize, Serialize, Debug, Clone, TS)]
174#[ts(export)]
175pub enum Credentials {
176    Snap { username: String, password: String },
177    NetsBlox { username: String, password: String },
178}
179
180impl From<Credentials> for LinkedAccount {
181    fn from(creds: Credentials) -> LinkedAccount {
182        match creds {
183            Credentials::Snap { username, .. } => LinkedAccount {
184                username,
185                strategy: "snap".to_owned(),
186            },
187            Credentials::NetsBlox { username, .. } => LinkedAccount {
188                // TODO: should this panic?
189                username,
190                strategy: "netsblox".to_owned(),
191            },
192        }
193    }
194}
195
196pub type FriendLinkId = String; // FIXME: switch to newtype
197#[derive(TS, Deserialize, Serialize, Clone, Debug)]
198#[serde(rename_all = "camelCase")]
199#[ts(export)]
200pub struct FriendLink {
201    pub id: FriendLinkId,
202    pub sender: String,
203    pub recipient: String,
204    pub state: FriendLinkState,
205    #[ts(type = "any")] // FIXME
206    pub created_at: SystemTime,
207    #[ts(type = "any")] // FIXME
208    pub updated_at: SystemTime,
209}
210
211#[derive(Deserialize, Serialize, Clone, Debug, TS)]
212#[ts(export)]
213pub enum FriendLinkState {
214    Pending,
215    Approved,
216    Rejected,
217    Blocked,
218}
219
220#[derive(Debug)]
221pub struct ParseFriendLinkStateError;
222
223impl fmt::Display for ParseFriendLinkStateError {
224    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225        write!(f, "invalid friend link state")
226    }
227}
228
229impl FromStr for FriendLinkState {
230    type Err = ParseFriendLinkStateError;
231
232    fn from_str(s: &str) -> Result<Self, Self::Err> {
233        match s {
234            "pending" => Ok(FriendLinkState::Pending),
235            "approved" => Ok(FriendLinkState::Approved),
236            "rejected" => Ok(FriendLinkState::Rejected),
237            "blocked" => Ok(FriendLinkState::Blocked),
238            _ => Err(ParseFriendLinkStateError),
239        }
240    }
241}
242
243#[derive(Serialize, Deserialize, Clone, Debug, TS)]
244#[serde(rename_all = "camelCase")]
245#[ts(export)]
246pub struct FriendInvite {
247    pub id: String,
248    pub sender: String,
249    pub recipient: String,
250    #[ts(type = "any")] // FIXME
251    pub created_at: SystemTime,
252}
253
254#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Display, Hash, TS)]
255#[ts(export)]
256pub struct ProjectId(String);
257
258impl ProjectId {
259    pub fn new(id: String) -> Self {
260        ProjectId(id)
261    }
262}
263
264#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Display, Hash, TS)]
265#[ts(export)]
266pub struct RoleId(String);
267
268impl RoleId {
269    pub fn new(id: String) -> Self {
270        RoleId(id)
271    }
272
273    pub fn as_str(&self) -> &str {
274        &self.0
275    }
276}
277
278#[derive(Deserialize, Serialize, Clone, Debug, TS)]
279#[serde(rename_all = "camelCase")]
280#[ts(export)]
281pub struct ProjectMetadata {
282    pub id: ProjectId,
283    pub owner: String,
284    pub name: String,
285    #[ts(type = "any")] // FIXME
286    pub updated: SystemTime,
287    pub state: PublishState,
288    pub collaborators: std::vec::Vec<String>,
289    pub network_traces: Vec<NetworkTraceMetadata>,
290    #[ts(type = "any")] // FIXME
291    pub origin_time: SystemTime,
292    pub save_state: SaveState,
293    pub roles: HashMap<RoleId, RoleMetadata>,
294}
295
296#[derive(Deserialize, Serialize, Clone, Debug, TS)]
297#[ts(export)]
298pub enum SaveState {
299    Created,
300    Transient,
301    Broken,
302    Saved,
303}
304
305#[derive(Deserialize, Serialize, Clone, Debug, TS)]
306#[ts(export)]
307pub struct RoleMetadata {
308    pub name: String,
309    pub code: String,
310    pub media: String,
311}
312
313#[derive(Deserialize, Serialize, TS)]
314#[serde(rename_all = "camelCase")]
315#[ts(export)]
316pub struct Project {
317    pub id: ProjectId,
318    pub owner: String,
319    pub name: String,
320    #[ts(type = "any")] // FIXME
321    pub updated: SystemTime,
322    pub state: PublishState,
323    pub collaborators: std::vec::Vec<String>,
324    #[ts(type = "any")] // FIXME
325    pub origin_time: SystemTime,
326    pub save_state: SaveState,
327    pub roles: HashMap<RoleId, RoleData>,
328}
329
330impl Project {
331    pub fn to_xml(&self) -> String {
332        let role_str: String = self
333            .roles
334            .values()
335            .map(|role| role.to_xml())
336            .collect::<Vec<_>>()
337            .join(" ");
338        format!(
339            "<room name=\"{}\" app=\"{}\">{}</room>",
340            self.name, APP_NAME, role_str
341        )
342    }
343}
344
345#[derive(Deserialize, Serialize, TS)]
346#[serde(rename_all = "camelCase")]
347#[ts(export)]
348pub struct RoleDataResponse {
349    pub id: Uuid,
350    pub data: RoleData,
351}
352
353#[derive(Deserialize, Serialize, Debug, Clone, TS)]
354#[ts(export)]
355pub struct RoleData {
356    pub name: String,
357    pub code: String,
358    pub media: String,
359}
360
361impl RoleData {
362    pub fn to_xml(&self) -> String {
363        let name = self.name.replace('\"', "\\\"");
364        format!("<role name=\"{}\">{}{}</role>", name, self.code, self.media)
365    }
366}
367
368#[derive(Deserialize, Serialize, TS)]
369#[serde(rename_all = "camelCase")]
370#[ts(export)]
371pub struct ClientStateData {
372    pub state: ClientState,
373}
374
375#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, TS)]
376#[serde(rename_all = "camelCase")]
377#[ts(export)]
378pub enum ClientState {
379    Browser(BrowserClientState),
380    External(ExternalClientState),
381}
382
383#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, TS)]
384#[serde(rename_all = "camelCase")]
385#[ts(export)]
386pub struct BrowserClientState {
387    pub role_id: RoleId,
388    pub project_id: ProjectId,
389}
390
391#[derive(Debug, Serialize, Clone, Hash, Eq, PartialEq, TS)]
392#[ts(export)]
393pub struct AppId(String);
394
395impl AppId {
396    pub fn new(addr: &str) -> Self {
397        Self(addr.to_lowercase())
398    }
399
400    pub fn as_str(&self) -> &str {
401        &self.0
402    }
403}
404
405impl<'de> Deserialize<'de> for AppId {
406    fn deserialize<D>(deserializer: D) -> Result<AppId, D::Error>
407    where
408        D: Deserializer<'de>,
409    {
410        let value = Value::deserialize(deserializer)?;
411        if let Value::String(s) = value {
412            Ok(AppId::new(s.as_str()))
413        } else {
414            Err(de::Error::custom("Invalid App ID expected a string"))
415        }
416    }
417}
418
419struct AppIdVisitor;
420impl<'de> Visitor<'de> for AppIdVisitor {
421    type Value = AppId;
422
423    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
424        formatter.write_str("an App ID string")
425    }
426
427    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
428        println!("deserializing {}", value);
429        Ok(AppId::new(value))
430    }
431
432    fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
433        println!("deserializing {}", value);
434        Ok(AppId::new(value.as_str()))
435    }
436}
437
438#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, TS)]
439#[serde(rename_all = "camelCase")]
440#[ts(export)]
441pub struct ExternalClientState {
442    pub address: String,
443    pub app_id: AppId,
444}
445
446#[derive(Serialize, Deserialize, TS)]
447#[ts(export)]
448pub struct CreateLibraryData {
449    pub name: String,
450    pub notes: String,
451    pub blocks: String,
452}
453
454#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, TS)]
455#[ts(export)]
456pub enum PublishState {
457    Private,
458    ApprovalDenied,
459    PendingApproval,
460    Public,
461}
462
463#[derive(Serialize, Deserialize, Clone, Debug, TS)]
464#[ts(export)]
465pub struct LibraryMetadata {
466    pub owner: String,
467    pub name: String,
468    pub notes: String,
469    pub state: PublishState,
470}
471
472impl LibraryMetadata {
473    pub fn new(
474        owner: String,
475        name: String,
476        state: PublishState,
477        notes: Option<String>,
478    ) -> LibraryMetadata {
479        LibraryMetadata {
480            owner,
481            name,
482            notes: notes.unwrap_or_default(),
483            state,
484        }
485    }
486}
487
488#[derive(Serialize, Deserialize, Clone, TS)]
489#[serde(rename_all = "camelCase")]
490#[ts(export)]
491pub struct CreateGroupData {
492    pub name: String,
493    #[ts(optional)]
494    pub services_hosts: Option<Vec<ServiceHost>>,
495}
496
497#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Display, Hash, FromStr, TS)]
498#[ts(export)]
499pub struct GroupId(String);
500
501impl GroupId {
502    pub fn new(name: String) -> Self {
503        Self(name)
504    }
505
506    pub fn as_str(&self) -> &str {
507        &self.0
508    }
509}
510
511#[derive(Serialize, Deserialize, Clone, Debug, TS)]
512#[serde(rename_all = "camelCase")]
513#[ts(export)]
514pub struct Group {
515    pub id: GroupId,
516    pub owner: String,
517    pub name: String,
518    #[ts(optional)]
519    pub services_hosts: Option<Vec<ServiceHost>>,
520}
521
522#[derive(Serialize, Deserialize, TS)]
523#[ts(export)]
524pub struct UpdateGroupData {
525    pub name: String,
526}
527
528#[derive(Deserialize, Serialize, Clone, Debug, TS)]
529#[ts(export)]
530pub enum InvitationState {
531    Pending,
532    Accepted,
533    Rejected,
534}
535
536pub type InvitationId = String;
537
538#[derive(TS, Deserialize, Serialize, Clone, Debug)]
539#[serde(rename_all = "camelCase")]
540#[ts(export)]
541pub struct CollaborationInvite {
542    pub id: String,
543    pub sender: String,
544    pub receiver: String,
545    pub project_id: ProjectId,
546    pub state: InvitationState,
547    #[ts(type = "any")] // FIXME
548    pub created_at: SystemTime,
549}
550
551impl CollaborationInvite {
552    pub fn new(sender: String, receiver: String, project_id: ProjectId) -> Self {
553        CollaborationInvite {
554            id: Uuid::new_v4().to_string(),
555            sender,
556            receiver,
557            project_id,
558            state: InvitationState::Pending,
559            created_at: SystemTime::now(),
560        }
561    }
562}
563
564#[derive(Deserialize, Serialize, TS)]
565#[serde(rename_all = "camelCase")]
566#[ts(export)]
567pub struct UpdateProjectData {
568    pub name: String,
569    #[ts(optional)]
570    pub client_id: Option<ClientId>,
571}
572
573#[derive(Deserialize, Serialize, Debug, TS)]
574#[serde(rename_all = "camelCase")]
575#[ts(export)]
576pub struct UpdateRoleData {
577    pub name: String,
578    #[ts(optional)]
579    pub client_id: Option<ClientId>,
580}
581
582#[derive(Deserialize, Serialize, TS)]
583#[serde(rename_all = "camelCase")]
584#[ts(export)]
585pub struct CreateProjectData {
586    #[ts(optional)]
587    pub owner: Option<String>,
588    pub name: String,
589    #[ts(optional)]
590    pub roles: Option<Vec<RoleData>>,
591    #[ts(optional)]
592    pub client_id: Option<ClientId>,
593    #[ts(optional)]
594    pub save_state: Option<SaveState>,
595}
596
597// Network debugging data
598#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, TS)]
599#[ts(export)]
600pub struct ClientId(String);
601
602impl ClientId {
603    pub fn new(addr: String) -> Self {
604        Self(addr)
605    }
606
607    pub fn as_str(&self) -> &str {
608        &self.0
609    }
610}
611
612#[derive(Debug, Display, Error, TS)]
613#[display(fmt = "Invalid client ID. Must start with a _")]
614#[ts(export)]
615pub struct ClientIDError;
616
617impl FromStr for ClientId {
618    type Err = ClientIDError;
619    fn from_str(s: &str) -> Result<Self, Self::Err> {
620        if s.starts_with('_') {
621            Ok(ClientId::new(s.to_owned()))
622        } else {
623            Err(ClientIDError)
624        }
625    }
626}
627
628#[derive(Deserialize, Serialize, Debug, TS)]
629#[serde(rename_all = "camelCase")]
630#[ts(export)]
631pub struct ExternalClient {
632    #[ts(optional)]
633    pub username: Option<String>,
634    pub address: String,
635    pub app_id: AppId,
636}
637
638#[derive(Deserialize, Serialize, Clone, Debug, TS)]
639#[ts(export)]
640pub struct RoomState {
641    pub id: ProjectId,
642    pub owner: String,
643    pub name: String,
644    pub roles: HashMap<RoleId, RoleState>,
645    pub collaborators: Vec<String>,
646    pub version: u64,
647}
648
649#[derive(Deserialize, Serialize, Clone, Debug, TS)]
650#[ts(export)]
651pub struct RoleState {
652    pub name: String,
653    pub occupants: Vec<OccupantState>,
654}
655
656#[derive(Deserialize, Serialize, Clone, Debug, TS)]
657#[ts(export)]
658pub struct OccupantState {
659    pub id: ClientId,
660    pub name: String,
661}
662
663#[derive(Deserialize, Serialize, Debug, TS)]
664#[serde(rename_all = "camelCase")]
665#[ts(export)]
666pub struct OccupantInviteData {
667    pub username: String,
668    pub role_id: RoleId,
669    #[ts(optional)]
670    pub sender: Option<String>,
671}
672
673#[derive(Deserialize, Serialize, Debug, Clone, TS)]
674#[serde(rename_all = "camelCase")]
675#[ts(export)]
676pub struct AuthorizedServiceHost {
677    pub url: String,
678    pub id: String,
679    pub visibility: ServiceHostScope,
680}
681
682#[derive(Deserialize, Serialize, Debug, Clone, TS)]
683#[serde(rename_all = "camelCase")]
684#[ts(export)]
685pub enum ServiceHostScope {
686    Public(Vec<String>),
687    Private,
688}
689
690#[derive(Deserialize, Serialize, Debug, Clone, TS)]
691#[serde(rename_all = "camelCase")]
692#[ts(export)]
693pub struct ClientInfo {
694    #[ts(optional)]
695    pub username: Option<String>,
696    #[ts(optional)]
697    pub state: Option<ClientState>,
698}
699
700/// Service settings for a given user categorized by origin
701#[derive(Deserialize, Serialize, Debug, Clone, TS)]
702#[ts(export)]
703pub struct ServiceSettings {
704    /// Service settings owned by the user
705    #[ts(optional)]
706    pub user: Option<String>,
707    /// Service settings owned by a group in which the user is a member
708    #[ts(optional)]
709    pub member: Option<String>,
710    /// Service settings owned by a groups created by the user
711    pub groups: HashMap<GroupId, String>,
712}
713
714/// Send message request (for authorized services)
715#[derive(Deserialize, Serialize, Debug, Clone, TS)]
716#[serde(rename_all = "camelCase")]
717#[ts(export)]
718pub struct SendMessage {
719    pub sender: Option<SendMessageSender>,
720    pub target: SendMessageTarget,
721    // TODO: Should we only allow "message" types or any sort of message?
722    #[ts(type = "object")]
723    pub content: Value,
724}
725
726/// Send message request (for authorized services)
727#[derive(Deserialize, Serialize, Debug, Clone, TS)]
728#[serde(rename_all = "camelCase")]
729#[ts(export)]
730pub struct LogMessage {
731    pub sender: String,
732    pub recipients: Vec<String>,
733    // TODO: Should we only allow "message" types or any sort of message?
734    #[ts(type = "object")]
735    pub content: Value,
736}
737
738#[derive(Deserialize, Serialize, Debug, Clone, TS)]
739#[serde(rename_all = "camelCase")]
740#[ts(export)]
741pub enum SendMessageSender {
742    Username(String),
743    Client(ClientId),
744}
745
746#[derive(Deserialize, Serialize, Debug, Clone, TS)]
747#[serde(rename_all = "camelCase")]
748#[ts(export)]
749pub enum SendMessageTarget {
750    Address {
751        address: String,
752    },
753    #[serde(rename_all = "camelCase")]
754    Room {
755        project_id: ProjectId,
756    },
757    #[serde(rename_all = "camelCase")]
758    Role {
759        project_id: ProjectId,
760        role_id: RoleId,
761    },
762    #[serde(rename_all = "camelCase")]
763    Client {
764        #[ts(optional)]
765        state: Option<ClientState>,
766        client_id: ClientId,
767    },
768}
769
770#[derive(Serialize, Deserialize, Clone, Debug, TS)]
771#[ts(export)]
772pub struct MagicLinkId(String);
773
774impl MagicLinkId {
775    pub fn new(id: String) -> Self {
776        Self(id)
777    }
778
779    pub fn as_str(&self) -> &str {
780        &self.0
781    }
782}
783
784#[derive(Serialize, Deserialize, Clone, Debug, TS)]
785#[serde(rename_all = "camelCase")]
786#[ts(export)]
787pub struct MagicLinkLoginData {
788    pub link_id: MagicLinkId,
789    pub username: String,
790    #[ts(optional)]
791    pub client_id: Option<ClientId>,
792    #[ts(optional)]
793    pub redirect_uri: Option<String>,
794}
795
796#[derive(Serialize, Deserialize, Clone, Debug, TS)]
797#[serde(rename_all = "camelCase")]
798#[ts(export)]
799pub struct CreateMagicLinkData {
800    pub email: String,
801    #[ts(optional)]
802    pub redirect_uri: Option<String>,
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808    use uuid::Uuid;
809
810    #[test]
811    fn deserialize_project_id() {
812        let project_id_str = &format!("\"{}\"", Uuid::new_v4());
813        let _project_id: ProjectId = serde_json::from_str(project_id_str)
814            .unwrap_or_else(|_err| panic!("Unable to parse ProjectId from {}", project_id_str));
815    }
816
817    #[test]
818    fn deserialize_role_id() {
819        let role_id_str = &format!("\"{}\"", Uuid::new_v4());
820        let _role_id: RoleId = serde_json::from_str(role_id_str)
821            .unwrap_or_else(|_err| panic!("Unable to parse RoleId from {}", role_id_str));
822    }
823
824    #[test]
825    fn should_compare_roles() {
826        assert!(UserRole::Teacher > UserRole::User);
827        assert!(UserRole::Moderator > UserRole::User);
828        assert!(UserRole::Admin > UserRole::User);
829
830        assert!(UserRole::Moderator > UserRole::Teacher);
831        assert!(UserRole::Admin > UserRole::Teacher);
832
833        assert!(UserRole::Admin > UserRole::Moderator);
834
835        assert!(UserRole::User == UserRole::User);
836        assert!(UserRole::Teacher == UserRole::Teacher);
837        assert!(UserRole::Moderator == UserRole::Moderator);
838        assert!(UserRole::Admin == UserRole::Admin);
839    }
840
841    #[test]
842    fn serialize_userroles_as_strings() {
843        let role_str = serde_json::to_string(&UserRole::User).unwrap();
844        assert_eq!(&role_str, "\"user\"");
845    }
846
847    #[test]
848    fn deserialize_app_id_lowercase() {
849        let app_id_str = String::from("\"NetsBlox\"");
850        let app_id: AppId = serde_json::from_str(&app_id_str).unwrap();
851        assert_eq!(&app_id.as_str(), &"netsblox");
852        assert_eq!(app_id, AppId::new("netsblox"));
853    }
854
855    #[test]
856    fn publish_state_priv_lt_pending() {
857        assert!(PublishState::Private < PublishState::PendingApproval);
858    }
859
860    #[test]
861    fn publish_state_pending_lt_public() {
862        assert!(PublishState::PendingApproval < PublishState::Public);
863    }
864
865    #[test]
866    fn publish_state_public_eq() {
867        assert!(PublishState::Public == PublishState::Public);
868    }
869}