headless_lms_models/
lib.rs

1/*!
2Functions and structs for interacting with the database.
3
4Each submodule corresponds to a database table.
5*/
6// we always use --document-private-items, so this warning is moot
7#![allow(rustdoc::private_intra_doc_links)]
8pub mod certificate_configuration_to_requirements;
9pub mod certificate_configurations;
10pub mod certificate_fonts;
11pub mod chapters;
12pub mod chatbot_configurations;
13pub mod chatbot_configurations_models;
14pub mod chatbot_conversation_message_tool_calls;
15pub mod chatbot_conversation_message_tool_outputs;
16pub mod chatbot_conversation_messages;
17pub mod chatbot_conversation_messages_citations;
18pub mod chatbot_conversations;
19pub mod chatbot_page_sync_statuses;
20pub mod code_giveaway_codes;
21pub mod code_giveaways;
22pub mod course_background_question_answers;
23pub mod course_background_questions;
24pub mod course_custom_privacy_policy_checkbox_texts;
25pub mod course_exams;
26pub mod course_instance_enrollments;
27pub mod course_instances;
28pub mod course_language_groups;
29pub mod course_module_completion_registered_to_study_registries;
30pub mod course_module_completions;
31pub mod course_modules;
32pub mod courses;
33pub mod email_deliveries;
34pub mod email_templates;
35pub mod email_verification_tokens;
36pub mod ended_processed_exams;
37pub mod error;
38pub mod exams;
39pub mod exercise_language_groups;
40pub mod exercise_repositories;
41pub mod exercise_reset_logs;
42pub mod exercise_service_info;
43pub mod exercise_services;
44pub mod exercise_slide_submissions;
45pub mod exercise_slides;
46pub mod exercise_task_gradings;
47pub mod exercise_task_regrading_submissions;
48pub mod exercise_task_submissions;
49pub mod exercise_tasks;
50pub mod exercises;
51pub mod feedback;
52pub mod file_uploads;
53pub mod flagged_answers;
54pub mod generated_certificates;
55pub mod glossary;
56pub mod join_code_uses;
57pub mod library;
58pub mod marketing_consents;
59pub mod material_references;
60pub mod oauth_access_token;
61pub mod oauth_auth_code;
62pub mod oauth_client;
63pub mod oauth_dpop_proofs;
64pub mod oauth_refresh_tokens;
65pub mod oauth_user_client_scopes;
66pub mod offered_answers_to_peer_review_temporary;
67pub mod open_university_registration_links;
68pub mod organizations;
69pub mod other_domain_to_course_redirections;
70pub mod page_audio_files;
71pub mod page_history;
72pub mod page_language_groups;
73pub mod page_visit_datum;
74pub mod page_visit_datum_daily_visit_hashing_keys;
75pub mod page_visit_datum_summary_by_courses;
76pub mod page_visit_datum_summary_by_courses_countries;
77pub mod page_visit_datum_summary_by_courses_device_types;
78pub mod page_visit_datum_summary_by_pages;
79pub mod pages;
80pub mod partner_block;
81pub mod peer_or_self_review_configs;
82pub mod peer_or_self_review_question_submissions;
83pub mod peer_or_self_review_questions;
84pub mod peer_or_self_review_submissions;
85pub mod peer_review_queue_entries;
86pub mod pending_roles;
87pub mod playground_examples;
88pub mod privacy_link;
89pub mod proposed_block_edits;
90pub mod proposed_page_edits;
91pub mod regradings;
92pub mod rejected_exercise_slide_submissions;
93pub mod repository_exercises;
94pub mod research_forms;
95pub mod roles;
96pub mod student_countries;
97pub mod study_registry_registrars;
98pub mod suspected_cheaters;
99pub mod teacher_grading_decisions;
100pub mod url_redirections;
101pub mod user_chapter_locking_statuses;
102pub mod user_course_exercise_service_variables;
103pub mod user_course_settings;
104pub mod user_details;
105pub mod user_email_codes;
106pub mod user_exercise_slide_states;
107pub mod user_exercise_states;
108pub mod user_exercise_task_states;
109pub mod user_passwords;
110pub mod user_research_consents;
111pub mod users;
112
113pub mod prelude;
114#[cfg(test)]
115pub mod test_helper;
116
117use exercises::Exercise;
118use futures::future::BoxFuture;
119use url::Url;
120use user_exercise_states::UserExerciseState;
121use uuid::Uuid;
122
123pub use self::error::{HttpErrorType, ModelError, ModelErrorType, ModelResult};
124use crate::prelude::*;
125
126#[macro_use]
127extern crate tracing;
128
129/**
130Helper struct to use with functions that insert data into the database.
131
132## Examples
133
134### Usage when inserting to a database
135
136By calling `.into_uuid()` function implemented by `PKeyPolicy<Uuid>`, this enum can be used with
137SQLX queries while letting the caller dictate how the primary key should be decided.
138
139```no_check
140# use headless_lms_models::{ModelResult, PKeyPolicy};
141# use uuid::Uuid;
142# use sqlx::PgConnection;
143async fn insert(
144    conn: &mut PgConnection,
145    pkey_policy: PKeyPolicy<Uuid>,
146) -> ModelResult<Uuid> {
147    let res = sqlx::query!(
148        "INSERT INTO organizations (id) VALUES ($1) RETURNING id",
149        pkey_policy.into_uuid(),
150    )
151    .fetch_one(conn)
152    .await?;
153    Ok(res.id)
154}
155
156# async fn random_function(conn: &mut PgConnection) -> ModelResult<()> {
157// Insert using generated id.
158let foo_1_id = insert(conn, PKeyPolicy::Generate).await.unwrap();
159
160// Insert using fixed id.
161let uuid = Uuid::parse_str("8fce44cf-738e-4fc9-8d8e-47c350fd3a7f").unwrap();
162let foo_2_id = insert(conn, PKeyPolicy::Fixed(uuid)).await.unwrap();
163assert_eq!(foo_2_id, uuid);
164# Ok(())
165# }
166```
167
168### Usage in a higher-order function.
169
170When `PKeyPolicy` is used with a higher-order function, an arbitrary struct can be provided
171instead. The data can be mapped further by calling the `.map()` or `.map_ref()` methods.
172
173```no_run
174# use headless_lms_models::{ModelResult, PKeyPolicy};
175# use uuid::Uuid;
176# use sqlx::PgConnection;
177# mod foos {
178#   use headless_lms_models::{ModelResult, PKeyPolicy};
179#   use uuid::Uuid;
180#   use sqlx::PgConnection;
181#   pub async fn insert(conn: &mut PgConnection, pkey_policy: PKeyPolicy<Uuid>) -> ModelResult<()> {
182#       Ok(())
183#   }
184# }
185# mod bars {
186#   use headless_lms_models::{ModelResult, PKeyPolicy};
187#   use uuid::Uuid;
188#   use sqlx::PgConnection;
189#   pub async fn insert(conn: &mut PgConnection, pkey_policy: PKeyPolicy<Uuid>) -> ModelResult<()> {
190#       Ok(())
191#   }
192# }
193
194struct FooBar {
195    foo: Uuid,
196    bar: Uuid,
197}
198
199async fn multiple_inserts(
200    conn: &mut PgConnection,
201    pkey_policy: PKeyPolicy<FooBar>,
202) -> ModelResult<()> {
203    foos::insert(conn, pkey_policy.map_ref(|x| x.foo)).await?;
204    bars::insert(conn, pkey_policy.map_ref(|x| x.bar)).await?;
205    Ok(())
206}
207
208# async fn some_function(conn: &mut PgConnection) {
209// Insert using generated ids.
210assert!(multiple_inserts(conn, PKeyPolicy::Generate).await.is_ok());
211
212// Insert using fixed ids.
213let foobar = FooBar {
214    foo: Uuid::parse_str("52760668-cc9d-4144-9226-d2aacb83bea9").unwrap(),
215    bar: Uuid::parse_str("ce9bd0cd-0e66-4522-a1b4-52a9347a115c").unwrap(),
216};
217assert!(multiple_inserts(conn, PKeyPolicy::Fixed(foobar)).await.is_ok());
218# }
219```
220*/
221pub enum PKeyPolicy<T> {
222    /// Ids will be generated based on the associated data. Usually only used in
223    /// local test environments where reproducible database states are desired.
224    Fixed(T),
225    /// Ids will be generated on the database level. This should be the default
226    /// behavior.
227    Generate,
228}
229
230impl<T> PKeyPolicy<T> {
231    /// Gets reference to the fixed data, if there are any.
232    pub fn fixed(&self) -> Option<&T> {
233        match self {
234            PKeyPolicy::Fixed(t) => Some(t),
235            PKeyPolicy::Generate => None,
236        }
237    }
238
239    /// Maps `PKeyPolicy<T>` to `PKeyPolicy<U>` by applying a function to the contained value.
240    pub fn map<U, F>(self, f: F) -> PKeyPolicy<U>
241    where
242        F: FnOnce(T) -> U,
243    {
244        match self {
245            PKeyPolicy::Fixed(x) => PKeyPolicy::Fixed(f(x)),
246            PKeyPolicy::Generate => PKeyPolicy::Generate,
247        }
248    }
249
250    /// Maps a reference of contained data in `Fixed(T)` to `PKeyPolicy<U>` by applying a function
251    /// to the contained value. This is useful whenever a referenced value can be used instead of
252    /// having to consume the original value.
253    pub fn map_ref<U, F>(&self, f: F) -> PKeyPolicy<U>
254    where
255        F: FnOnce(&T) -> U,
256    {
257        match self {
258            PKeyPolicy::Fixed(x) => PKeyPolicy::Fixed(f(x)),
259            PKeyPolicy::Generate => PKeyPolicy::Generate,
260        }
261    }
262}
263
264impl PKeyPolicy<Uuid> {
265    /// Maps into the contained `Uuid` value or generates a new one.
266    pub fn into_uuid(self) -> Uuid {
267        match self {
268            PKeyPolicy::Fixed(uuid) => uuid,
269            PKeyPolicy::Generate => Uuid::new_v4(),
270        }
271    }
272}
273
274/// A "trait alias" so this `for<'a>` ... string doesn't need to be repeated everywhere
275/// Arguments:
276///   `Url`: The URL that the request is sent to (the exercise service's endpoint)
277///   `&str`: Exercise type/service slug
278///   `Option<Value>`: The Json for the request, for example the private spec in a public spec request
279pub trait SpecFetcher:
280    for<'a> Fn(
281    Url,
282    &'a str,
283    Option<&'a serde_json::Value>,
284) -> BoxFuture<'a, ModelResult<serde_json::Value>>
285{
286}
287
288impl<
289    T: for<'a> Fn(
290        Url,
291        &'a str,
292        Option<&'a serde_json::Value>,
293    ) -> BoxFuture<'a, ModelResult<serde_json::Value>>,
294> SpecFetcher for T
295{
296}
297
298/// Either a course or exam id.
299///
300/// Exercises can either be part of courses or exams. Many user-related actions need to differentiate
301/// between two, so `CourseOrExamId` helps when handling these separate scenarios.
302#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash)]
303pub enum CourseOrExamId {
304    Course(Uuid),
305    Exam(Uuid),
306}
307
308impl CourseOrExamId {
309    pub fn from_course_and_exam_ids(
310        course_id: Option<Uuid>,
311        exam_id: Option<Uuid>,
312    ) -> ModelResult<Self> {
313        match (course_id, exam_id) {
314            (None, None) => Err(ModelError::new(
315                ModelErrorType::Generic,
316                "Expected either course or exam id, but neither were provided.",
317                None,
318            )),
319            (Some(course_id), None) => Ok(Self::Course(course_id)),
320            (None, Some(exam_id)) => Ok(Self::Exam(exam_id)),
321            (Some(_), Some(_)) => Err(ModelError::new(
322                ModelErrorType::Generic,
323                "Expected either course or exam id, but both were provided.",
324                None,
325            )),
326        }
327    }
328
329    pub fn to_course_and_exam_ids(&self) -> (Option<Uuid>, Option<Uuid>) {
330        match self {
331            CourseOrExamId::Course(course_id) => (Some(*course_id), None),
332            CourseOrExamId::Exam(exam_id) => (None, Some(*exam_id)),
333        }
334    }
335}
336
337impl TryFrom<UserExerciseState> for CourseOrExamId {
338    type Error = ModelError;
339
340    fn try_from(user_exercise_state: UserExerciseState) -> Result<Self, Self::Error> {
341        Self::from_course_and_exam_ids(user_exercise_state.course_id, user_exercise_state.exam_id)
342    }
343}
344
345impl TryFrom<&UserExerciseState> for CourseOrExamId {
346    type Error = ModelError;
347
348    fn try_from(user_exercise_state: &UserExerciseState) -> Result<Self, Self::Error> {
349        Self::from_course_and_exam_ids(user_exercise_state.course_id, user_exercise_state.exam_id)
350    }
351}
352
353impl TryFrom<Exercise> for CourseOrExamId {
354    type Error = ModelError;
355
356    fn try_from(exercise: Exercise) -> Result<Self, Self::Error> {
357        Self::from_course_and_exam_ids(exercise.course_id, exercise.exam_id)
358    }
359}
360
361impl TryFrom<&Exercise> for CourseOrExamId {
362    type Error = ModelError;
363
364    fn try_from(exercise: &Exercise) -> Result<Self, Self::Error> {
365        Self::from_course_and_exam_ids(exercise.course_id, exercise.exam_id)
366    }
367}