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