headless_lms_server/programs/doc_file_generator/
mod.rs

1/*! The doc file generator is used to write example JSON and TypeScript definitions for the docs of return values of API endpoints.
2
3This is done by writing .json and .ts files that can be discovered by the #[generated_doc] attribute macro that is used by API endpoints.
4
5To make this process more convenient, two macros are provided:
6- example! (proc macro defined in the doc_macros crate)
7- doc! (declarative macro defined in this file)
8
9## example!
10Accepts a struct or enum literal, such as
11```no_run
12# use headless_lms_server::doc;
13# use headless_lms_server::programs::doc_file_generator::example::Example;
14# use doc_macro::example;
15# struct SomeStruct { first_field: u32, second_field: u32 }
16example!(SomeStruct {
17    first_field,
18    second_field: 1234,
19});
20```
21and implements the Example trait for the given type:
22```
23# use headless_lms_server::programs::doc_file_generator::example::Example;
24# struct SomeStruct { first_field: u32, second_field: u32 }
25impl Example for SomeStruct {
26    fn example() -> Self {
27        Self {
28            first_field: Example::example(),
29            second_field: 1234,
30        }
31    }
32}
33```
34As can be seen in the code above, you can leave out the value of any field to use its Example implementation.
35
36The Example trait is used for the doc! macro as explained below.
37
38## doc!
39Writes the JSON and TypeScript files used for API endpoint docs.
40
41This macro can be used in two primary ways:
42- With a struct/enum literal
43- With a type and expression
44
45With a struct/enum literal, the doc! macro generates an Example implementation for the type using the example! macro
46and then uses it to write the JSON docs. (The TypeScript definition is generated with the `ts_rs::TS` trait)
47```no_run
48# use headless_lms_server::doc;
49# use headless_lms_server::programs::doc_file_generator::example::Example;
50# #[derive(serde::Serialize)]
51# struct SomeStruct { first_field: u32, second_field: u32 }
52doc!(SomeStruct {
53    first_field,
54    second_field: 1234,
55});
56```
57In addition to the `impl Example for SomeStruct`, it will serialize `<SomeStruct as Example>::example()` to JSON and write it to generated-docs/SomeStruct.json,
58as well as `<SomeStruct as TS>::inline()` to generated-docs/SomeStruct.ts.
59
60Note that because it uses the example! macro, you can leave out values for fields the same way.
61
62The struct/enum literal can be prepended with T, Option, or Vec, in order to generate docs for the given type (T), Option of the given type (Opt), or Vec of the given type (Vec).
63Note that they must be in the order T, Option, Vec, though you can leave any (or all) of them out.
64
65For example,
66```no_run
67# use headless_lms_server::doc;
68# use headless_lms_server::programs::doc_file_generator::example::Example;
69# #[derive(serde::Serialize)]
70# struct SomeStruct { first_field: u32, second_field: u32 }
71doc!(
72    T,
73    Vec,
74    SomeStruct {
75        first_field,
76        second_field: 1234,
77    }
78);
79```
80will create docs for `SomeStruct` and `Vec<SomeStruct>`.
81
82With a type and expression, the doc! macro simply uses the expression to write the JSON docs for the given type without involving the Example trait or example! macro.
83```no_run
84# use headless_lms_server::doc;
85# use headless_lms_server::programs::doc_file_generator::example::Example;
86# #[derive(serde::Serialize)]
87# struct SomeStruct { first_field: u32, second_field: u32 }
88doc!(
89    Vec<SomeStruct>,
90    vec![
91        SomeStruct {
92            first_field: Example::example(),
93            second_field: 2,
94        },
95        SomeStruct {
96            first_field: 3,
97            second_field: 4,
98        },
99    ]
100);
101```
102Note that since this method doesn't use the example! macro, leaving out field values is an error. If we want to use the Example trait here, we need to explicitly call Example::example()
103or the ex() function which is just a shortcut for Example::example().
104
105This method is mainly useful for external/std types. For example, we cannot write Uuid as a struct literal because it has private fields, or bool because it's a primitive type.
106*/
107
108#![allow(clippy::redundant_clone)]
109#![allow(unused_imports)]
110
111pub mod example;
112
113use chrono::{TimeZone, Utc};
114use example::Example;
115use headless_lms_models::{
116    course_background_question_answers::CourseBackgroundQuestionAnswer,
117    course_background_questions::{
118        CourseBackgroundQuestion, CourseBackgroundQuestionType, CourseBackgroundQuestionsAndAnswers,
119    },
120    course_instance_enrollments::CourseInstanceEnrollmentsInfo,
121    course_module_completions::{
122        CourseModuleCompletion, CourseModuleCompletionWithRegistrationInfo,
123        StudyRegistryCompletion, StudyRegistryGrade,
124    },
125    courses::CourseBreadcrumbInfo,
126    exercise_task_submissions::PeerOrSelfReviewsReceived,
127    exercises::ExerciseStatusSummaryForUser,
128    library::global_stats::{GlobalCourseModuleStatEntry, GlobalStatEntry},
129    page_audio_files::PageAudioFile,
130    page_visit_datum_summary_by_courses::PageVisitDatumSummaryByCourse,
131    page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries,
132    page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes,
133    page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages,
134    peer_or_self_review_configs::CourseMaterialPeerOrSelfReviewConfig,
135    peer_or_self_review_question_submissions::{
136        PeerOrSelfReviewAnswer, PeerOrSelfReviewQuestionAndAnswer,
137        PeerOrSelfReviewQuestionSubmission,
138    },
139    peer_or_self_review_submissions::PeerOrSelfReviewSubmission,
140    peer_review_queue_entries::PeerReviewQueueEntry,
141    proposed_block_edits::EditedBlockStillExistsData,
142    research_forms::{ResearchForm, ResearchFormQuestion, ResearchFormQuestionAnswer},
143    student_countries::StudentCountry,
144    teacher_grading_decisions::{TeacherDecisionType, TeacherGradingDecision},
145    user_details::UserDetail,
146    user_research_consents::UserResearchConsent,
147};
148use serde::Serialize;
149use serde_json::{Serializer, Value, json, ser::PrettyFormatter};
150use std::{collections::HashMap, fs};
151#[cfg(feature = "ts_rs")]
152use ts_rs::TS;
153use uuid::Uuid;
154
155use crate::controllers::course_material::exercises::CourseMaterialPeerOrSelfReviewDataWithToken;
156
157// Helper function to avoid typing out Example::example()
158fn ex<T: Example>() -> T {
159    Example::example()
160}
161
162#[macro_export]
163macro_rules! doc_path {
164    ($filename:expr_2021, $extension:expr_2021) => {{
165        let windows_safe_filename = $filename
166            .replace('<', "(")
167            .replace('>', ")")
168            .replace(" ", "");
169
170        let mut s = String::new();
171        s.push_str(env!("CARGO_MANIFEST_DIR"));
172        s.push_str("/generated-docs/");
173        s.push_str(windows_safe_filename.as_str());
174        s.push_str($extension);
175        s
176    }};
177}
178
179// Writes doc files. See the module documentation for more info.
180// macro_export mainly for the docs that use it
181#[macro_export]
182macro_rules! doc {
183    (T, Option, Vec, $($t:tt)*) => {
184        ::doc_macro::example!($($t)*);
185        doc!(@inner T, $($t)*);
186        doc!(@inner Option, $($t)*);
187        doc!(@inner Vec, $($t)*);
188    };
189    (Option, Vec, $($t:tt)*) => {
190        ::doc_macro::example!($($t)*);
191        doc!(@inner Option, $($t)*);
192        doc!(@inner Vec, $($t)*);
193    };
194    (T, Option, $($t:tt)*) => {
195        ::doc_macro::example!($($t)*);
196        doc!(@inner T, $($t)*);
197        doc!(@inner Option, $($t)*);
198    };
199    (T, Vec, $($t:tt)*) => {
200        ::doc_macro::example!($($t)*);
201        doc!(@inner T, $($t)*);
202        doc!(@inner Vec, $($t)*);
203    };
204    (T, $($t:tt)*) => {
205        ::doc_macro::example!($($t)*);
206        doc!(@inner T, $($t)*);
207    };
208    (Option, $($t:tt)*) => {
209        ::doc_macro::example!($($t)*);
210        doc!(@inner Option, $($t)*);
211    };
212    (Vec, $($t:tt)*) => {
213        ::doc_macro::example!($($t)*);
214        doc!(@inner Vec, $($t)*);
215    };
216    // enum literal
217    (@inner T, $i:ident :: $($t:tt)*) => {
218        doc!($i, Example::example());
219    };
220    (@inner Option, $i:ident :: $($t:tt)*) => {
221        doc!(Option<$i>, Example::example());
222    };
223    (@inner Vec, $i:ident :: $($t:tt)*) => {
224        doc!(Vec<$i>, Example::example());
225    };
226    // struct literal
227    (@inner T, $i:ident $($t:tt)*) => {
228        doc!($i, Example::example());
229    };
230    (@inner Option, $i:ident $($t:tt)*) => {
231        doc!(Option<$i>, Example::example());
232    };
233    (@inner Vec, $i:ident $($t:tt)*) => {
234        doc!(Vec<$i>, Example::example());
235    };
236    // writes the actual docs
237    ($t:ty, $e:expr_2021) => {{
238        let expr: $t = $e;
239
240        let json_path = $crate::doc_path!(
241            stringify!($t),
242            ".json"
243        );
244        $crate::programs::doc_file_generator::write_json(&json_path, expr);
245
246        #[cfg(feature = "ts_rs")]
247        {
248            let ts_path = $crate::doc_path!(
249                stringify!($t),
250                ".ts"
251            );
252
253            $crate::programs::doc_file_generator::write_ts::<$t>(&ts_path, stringify!($t));
254        }
255    }};
256    // shortcut for doc!(T, ...)
257    ($($t:tt)*) => {
258        doc!(T, $($t)*);
259    };
260}
261
262pub async fn main() -> anyhow::Result<()> {
263    // clear previous results
264    fs::read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/generated-docs/"))
265        .map_err(|e| anyhow::anyhow!("Failed to read generated-docs directory: {}", e))?
266        .filter_map(|file| {
267            file.ok().filter(|f| {
268                f.file_name()
269                    .to_str()
270                    .is_some_and(|n| n.ends_with(".json") || n.ends_with(".ts"))
271            })
272        })
273        .try_for_each(|f| -> anyhow::Result<()> {
274            fs::remove_file(f.path())
275                .map_err(|e| anyhow::anyhow!("Failed to remove file {:?}: {}", f.path(), e))?;
276            Ok(())
277        })?;
278
279    // write docs
280    controllers();
281
282    external();
283
284    doc!((), ex());
285    doc!(i64, 123);
286    doc!(bool, ex());
287    doc!(
288        Vec<bool>,
289        vec![false, true, false, true, false, true, true, true]
290    );
291    doc!(String, ex());
292    doc!(Uuid, ex());
293    doc!(Vec<Uuid>, ex());
294
295    Ok(())
296}
297
298pub fn write_json<T: Serialize>(path: &str, value: T) {
299    let mut file =
300        std::fs::File::create(path).expect("Failed to create file for JSON serialization");
301    let formatter = PrettyFormatter::with_indent(b"    ");
302    let mut serializer = Serializer::with_formatter(&mut file, formatter);
303    serde::Serialize::serialize(&value, &mut serializer)
304        .expect("Failed to serialize value to JSON");
305}
306
307#[cfg(feature = "ts_rs")]
308pub fn write_ts<T: TS>(path: &str, type_name: &str) {
309    let contents = format!("type {} = {}", type_name, T::inline());
310    std::fs::write(path, contents).expect("Failed to write TypeScript type definition file");
311}
312
313#[allow(non_local_definitions)]
314fn controllers() {
315    doc!(
316        T,
317        Vec,
318        StudyRegistryCompletion {
319            completion_date: Utc.with_ymd_and_hms(2022, 6, 21, 0, 0, 0).unwrap(),
320            completion_language: "en-US".to_string(),
321            completion_registration_attempt_date: None,
322            email: "student@example.com".to_string(),
323            grade: StudyRegistryGrade::new(true, Some(4)),
324            id: Uuid::parse_str("633852ce-c82a-4d60-8ab5-28745163f6f9")
325                .expect("Invalid UUID constant in doc generator"),
326            user_id,
327            tier: None
328        }
329    );
330}
331
332// external types, there should only be a couple in here so no macros or other fancy stuff
333fn external() {
334    std::fs::write(doc_path!("Bytes", ".ts"), "type Bytes = Blob")
335        .expect("Failed to write Bytes type definition file");
336}