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        .unwrap()
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        .for_each(|f| fs::remove_file(f.path()).unwrap());
274
275    // write docs
276    controllers();
277
278    external();
279
280    doc!((), ex());
281    doc!(i64, 123);
282    doc!(bool, ex());
283    doc!(
284        Vec<bool>,
285        vec![false, true, false, true, false, true, true, true]
286    );
287    doc!(String, ex());
288    doc!(Uuid, ex());
289    doc!(Vec<Uuid>, ex());
290
291    Ok(())
292}
293
294pub fn write_json<T: Serialize>(path: &str, value: T) {
295    let mut file = std::fs::File::create(path).unwrap();
296    let formatter = PrettyFormatter::with_indent(b"    ");
297    let mut serializer = Serializer::with_formatter(&mut file, formatter);
298    serde::Serialize::serialize(&value, &mut serializer).unwrap();
299}
300
301#[cfg(feature = "ts_rs")]
302pub fn write_ts<T: TS>(path: &str, type_name: &str) {
303    let contents = format!("type {} = {}", type_name, T::inline());
304    std::fs::write(path, contents).unwrap();
305}
306
307#[allow(non_local_definitions)]
308fn controllers() {
309    doc!(
310        T,
311        Vec,
312        StudyRegistryCompletion {
313            completion_date: Utc.with_ymd_and_hms(2022, 6, 21, 0, 0, 0).unwrap(),
314            completion_language: "en-US".to_string(),
315            completion_registration_attempt_date: None,
316            email: "student@example.com".to_string(),
317            grade: StudyRegistryGrade::new(true, Some(4)),
318            id: Uuid::parse_str("633852ce-c82a-4d60-8ab5-28745163f6f9").unwrap(),
319            user_id,
320            tier: None
321        }
322    );
323}
324
325// external types, there should only be a couple in here so no macros or other fancy stuff
326fn external() {
327    std::fs::write(doc_path!("Bytes", ".ts"), "type Bytes = Blob").unwrap();
328}