1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
/*!
Functions and structs for interacting with the database.

Each submodule corresponds to a database table.
*/
// we always use --document-private-items, so this warning is moot
#![allow(rustdoc::private_intra_doc_links)]
pub mod certificate_configuration_to_requirements;
pub mod certificate_configurations;
pub mod certificate_fonts;
pub mod chapters;
pub mod course_background_question_answers;
pub mod course_background_questions;
pub mod course_exams;
pub mod course_instance_enrollments;
pub mod course_instances;
pub mod course_language_groups;
pub mod course_module_completion_registered_to_study_registries;
pub mod course_module_completions;
pub mod course_modules;
pub mod courses;
pub mod email_deliveries;
pub mod email_templates;
pub mod ended_processed_exams;
pub mod exams;
pub mod exercise_language_groups;
pub mod exercise_repositories;
pub mod exercise_service_info;
pub mod exercise_services;
pub mod exercise_slide_submissions;
pub mod exercise_slides;
pub mod exercise_task_gradings;
pub mod exercise_task_regrading_submissions;
pub mod exercise_task_submissions;
pub mod exercise_tasks;
pub mod exercises;
pub mod feedback;
pub mod file_uploads;
pub mod generated_certificates;
pub mod glossary;
pub mod library;
pub mod material_references;
pub mod offered_answers_to_peer_review_temporary;
pub mod open_university_registration_links;
pub mod organizations;
pub mod other_domain_to_course_redirections;
pub mod page_audio_files;
pub mod page_history;
pub mod page_language_groups;
pub mod page_visit_datum;
pub mod page_visit_datum_daily_visit_hashing_keys;
pub mod page_visit_datum_summary_by_courses;
pub mod page_visit_datum_summary_by_courses_countries;
pub mod page_visit_datum_summary_by_courses_device_types;
pub mod page_visit_datum_summary_by_pages;
pub mod pages;
pub mod peer_or_self_review_configs;
pub mod peer_or_self_review_question_submissions;
pub mod peer_or_self_review_questions;
pub mod peer_or_self_review_submissions;
pub mod peer_review_queue_entries;
pub mod pending_roles;
pub mod playground_examples;
pub mod proposed_block_edits;
pub mod proposed_page_edits;
pub mod regradings;
pub mod rejected_exercise_slide_submissions;
pub mod repository_exercises;
pub mod research_forms;
pub mod roles;
pub mod student_countries;
pub mod study_registry_registrars;
pub mod suspected_cheaters;
pub mod teacher_grading_decisions;
pub mod url_redirections;
pub mod user_course_instance_exercise_service_variables;
pub mod user_course_settings;
pub mod user_details;
pub mod user_exercise_slide_states;
pub mod user_exercise_states;
pub mod user_exercise_task_states;
pub mod user_research_consents;
pub mod users;

pub mod error;

pub mod prelude;
#[cfg(test)]
pub mod test_helper;

use futures::future::BoxFuture;
use url::Url;
use uuid::Uuid;

pub use self::error::{ModelError, ModelErrorType, ModelResult};
use crate::prelude::*;

#[macro_use]
extern crate tracing;

/**
Helper struct to use with functions that insert data into the database.

## Examples

### Usage when inserting to a database

By calling `.into_uuid()` function implemented by `PKeyPolicy<Uuid>`, this enum can be used with
SQLX queries while letting the caller dictate how the primary key should be decided.

```no_run
# use headless_lms_models::{ModelResult, PKeyPolicy};
# use uuid::Uuid;
# use sqlx::PgConnection;
async fn insert(
    conn: &mut PgConnection,
    pkey_policy: PKeyPolicy<Uuid>,
) -> ModelResult<Uuid> {
    let res = sqlx::query!(
        "INSERT INTO organizations (id) VALUES ($1) RETURNING id",
        pkey_policy.into_uuid(),
    )
    .fetch_one(conn)
    .await?;
    Ok(res.id)
}

# async fn random_function(conn: &mut PgConnection) -> ModelResult<()> {
// Insert using generated id.
let foo_1_id = insert(conn, PKeyPolicy::Generate).await.unwrap();

// Insert using fixed id.
let uuid = Uuid::parse_str("8fce44cf-738e-4fc9-8d8e-47c350fd3a7f").unwrap();
let foo_2_id = insert(conn, PKeyPolicy::Fixed(uuid)).await.unwrap();
assert_eq!(foo_2_id, uuid);
# Ok(())
# }
```

### Usage in a higher-order function.

When `PKeyPolicy` is used with a higher-order function, an arbitrary struct can be provided
instead. The data can be mapped further by calling the `.map()` or `.map_ref()` methods.

```no_run
# use headless_lms_models::{ModelResult, PKeyPolicy};
# use uuid::Uuid;
# use sqlx::PgConnection;
# mod foos {
#   use headless_lms_models::{ModelResult, PKeyPolicy};
#   use uuid::Uuid;
#   use sqlx::PgConnection;
#   pub async fn insert(conn: &mut PgConnection, pkey_policy: PKeyPolicy<Uuid>) -> ModelResult<()> {
#       Ok(())
#   }
# }
# mod bars {
#   use headless_lms_models::{ModelResult, PKeyPolicy};
#   use uuid::Uuid;
#   use sqlx::PgConnection;
#   pub async fn insert(conn: &mut PgConnection, pkey_policy: PKeyPolicy<Uuid>) -> ModelResult<()> {
#       Ok(())
#   }
# }

struct FooBar {
    foo: Uuid,
    bar: Uuid,
}

async fn multiple_inserts(
    conn: &mut PgConnection,
    pkey_policy: PKeyPolicy<FooBar>,
) -> ModelResult<()> {
    foos::insert(conn, pkey_policy.map_ref(|x| x.foo)).await?;
    bars::insert(conn, pkey_policy.map_ref(|x| x.bar)).await?;
    Ok(())
}

# async fn some_function(conn: &mut PgConnection) {
// Insert using generated ids.
assert!(multiple_inserts(conn, PKeyPolicy::Generate).await.is_ok());

// Insert using fixed ids.
let foobar = FooBar {
    foo: Uuid::parse_str("52760668-cc9d-4144-9226-d2aacb83bea9").unwrap(),
    bar: Uuid::parse_str("ce9bd0cd-0e66-4522-a1b4-52a9347a115c").unwrap(),
};
assert!(multiple_inserts(conn, PKeyPolicy::Fixed(foobar)).await.is_ok());
# }
```
*/
pub enum PKeyPolicy<T> {
    /// Ids will be generated based on the associated data. Usually only used in
    /// local test environments where reproducible database states are desired.
    Fixed(T),
    /// Ids will be generated on the database level. This should be the default
    /// behavior.
    Generate,
}

impl<T> PKeyPolicy<T> {
    /// Gets reference to the fixed data, if there are any.
    pub fn fixed(&self) -> Option<&T> {
        match self {
            PKeyPolicy::Fixed(t) => Some(t),
            PKeyPolicy::Generate => None,
        }
    }

    /// Maps `PKeyPolicy<T>` to `PKeyPolicy<U>` by applying a function to the contained value.
    pub fn map<U, F>(self, f: F) -> PKeyPolicy<U>
    where
        F: FnOnce(T) -> U,
    {
        match self {
            PKeyPolicy::Fixed(x) => PKeyPolicy::Fixed(f(x)),
            PKeyPolicy::Generate => PKeyPolicy::Generate,
        }
    }

    /// Maps a reference of contained data in `Fixed(T)` to `PKeyPolicy<U>` by applying a function
    /// to the contained value. This is useful whenever a referenced value can be used instead of
    /// having to consume the original value.
    pub fn map_ref<U, F>(&self, f: F) -> PKeyPolicy<U>
    where
        F: FnOnce(&T) -> U,
    {
        match self {
            PKeyPolicy::Fixed(x) => PKeyPolicy::Fixed(f(x)),
            PKeyPolicy::Generate => PKeyPolicy::Generate,
        }
    }
}

impl PKeyPolicy<Uuid> {
    /// Maps into the contained `Uuid` value or generates a new one.
    pub fn into_uuid(self) -> Uuid {
        match self {
            PKeyPolicy::Fixed(uuid) => uuid,
            PKeyPolicy::Generate => Uuid::new_v4(),
        }
    }
}

/// Many database tables are related to either a course or an exam
#[derive(Clone, Copy)]
pub enum CourseOrExamId {
    Course(Uuid),
    Exam(Uuid),
}

impl CourseOrExamId {
    pub fn from(course_id: Option<Uuid>, exam_id: Option<Uuid>) -> ModelResult<Self> {
        match (course_id, exam_id) {
            (Some(course_id), None) => Ok(Self::Course(course_id)),
            (None, Some(exam_id)) => Ok(Self::Exam(exam_id)),
            (Some(_), Some(_)) => Err(ModelError::new(
                ModelErrorType::Generic,
                "Database row had both a course id and an exam id".to_string(),
                None,
            )),
            (None, None) => Err(ModelError::new(
                ModelErrorType::Generic,
                "Database row did not have a course id or an exam id".to_string(),
                None,
            )),
        }
    }

    pub fn to_course_and_exam_ids(self) -> (Option<Uuid>, Option<Uuid>) {
        match self {
            Self::Course(instance_id) => (Some(instance_id), None),
            Self::Exam(exam_id) => (None, Some(exam_id)),
        }
    }
    pub fn exam_id(self) -> Option<Uuid> {
        if let CourseOrExamId::Exam(id) = self {
            Some(id)
        } else {
            None
        }
    }
}

/// A "trait alias" so this `for<'a>` ... string doesn't need to be repeated everywhere
/// Arguments:
///   `Url`: The URL that the request is sent to (the exercise service's endpoint)
///   `&str`: Exercise type/service slug
///   `Option<Value>`: The Json for the request, for example the private spec in a public spec request
pub trait SpecFetcher:
    for<'a> Fn(
    Url,
    &'a str,
    Option<&'a serde_json::Value>,
) -> BoxFuture<'a, ModelResult<serde_json::Value>>
{
}

impl<
        T: for<'a> Fn(
            Url,
            &'a str,
            Option<&'a serde_json::Value>,
        ) -> BoxFuture<'a, ModelResult<serde_json::Value>>,
    > SpecFetcher for T
{
}