headless_lms_server/programs/doc_file_generator/
mod.rs

1/*! The doc file generator is used to write example JSON docs for return values of API endpoints.
2
3This is done by writing .json 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 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.
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.
58
59Note that because it uses the example! macro, you can leave out values for fields the same way.
60
61The 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).
62Note that they must be in the order T, Option, Vec, though you can leave any (or all) of them out.
63
64For example,
65```no_run
66# use headless_lms_server::doc;
67# use headless_lms_server::programs::doc_file_generator::example::Example;
68# #[derive(serde::Serialize)]
69# struct SomeStruct { first_field: u32, second_field: u32 }
70doc!(
71    T,
72    Vec,
73    SomeStruct {
74        first_field,
75        second_field: 1234,
76    }
77);
78```
79will create docs for `SomeStruct` and `Vec<SomeStruct>`.
80
81With 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.
82```no_run
83# use headless_lms_server::doc;
84# use headless_lms_server::programs::doc_file_generator::example::Example;
85# #[derive(serde::Serialize)]
86# struct SomeStruct { first_field: u32, second_field: u32 }
87doc!(
88    Vec<SomeStruct>,
89    vec![
90        SomeStruct {
91            first_field: Example::example(),
92            second_field: 2,
93        },
94        SomeStruct {
95            first_field: 3,
96            second_field: 4,
97        },
98    ]
99);
100```
101Note 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()
102or the ex() function which is just a shortcut for Example::example().
103
104This 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.
105*/
106
107#![allow(clippy::redundant_clone)]
108#![allow(unused_imports)]
109
110pub mod example;
111
112use chrono::{TimeZone, Utc};
113use example::Example;
114use headless_lms_models::{
115    course_background_question_answers::CourseBackgroundQuestionAnswer,
116    course_background_questions::{
117        CourseBackgroundQuestion, CourseBackgroundQuestionType, CourseBackgroundQuestionsAndAnswers,
118    },
119    course_instance_enrollments::CourseInstanceEnrollmentsInfo,
120    course_module_completions::{
121        CourseModuleCompletion, CourseModuleCompletionWithRegistrationInfo,
122        StudyRegistryCompletion, StudyRegistryGrade,
123    },
124    courses::CourseBreadcrumbInfo,
125    exercise_task_submissions::PeerOrSelfReviewsReceived,
126    exercises::ExerciseStatusSummaryForUser,
127    library::global_stats::{GlobalCourseModuleStatEntry, GlobalStatEntry},
128    page_audio_files::PageAudioFile,
129    page_visit_datum_summary_by_courses::PageVisitDatumSummaryByCourse,
130    page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries,
131    page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes,
132    page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages,
133    peer_or_self_review_configs::CourseMaterialPeerOrSelfReviewConfig,
134    peer_or_self_review_question_submissions::{
135        PeerOrSelfReviewAnswer, PeerOrSelfReviewQuestionAndAnswer,
136        PeerOrSelfReviewQuestionSubmission,
137    },
138    peer_or_self_review_submissions::PeerOrSelfReviewSubmission,
139    peer_review_queue_entries::PeerReviewQueueEntry,
140    proposed_block_edits::EditedBlockStillExistsData,
141    research_forms::{ResearchForm, ResearchFormQuestion, ResearchFormQuestionAnswer},
142    student_countries::StudentCountry,
143    teacher_grading_decisions::{TeacherDecisionType, TeacherGradingDecision},
144    user_details::UserDetail,
145    user_research_consents::UserResearchConsent,
146};
147use serde::Serialize;
148use serde_json::{Serializer, Value, json, ser::PrettyFormatter};
149use std::{collections::HashMap, fs};
150
151use uuid::Uuid;
152
153use crate::controllers::course_material::exercises::CourseMaterialPeerOrSelfReviewDataWithToken;
154
155// Helper function to avoid typing out Example::example()
156fn ex<T: Example>() -> T {
157    Example::example()
158}
159
160#[macro_export]
161macro_rules! doc_path {
162    ($filename:expr_2021, $extension:expr_2021) => {{
163        let windows_safe_filename = $filename
164            .replace('<', "(")
165            .replace('>', ")")
166            .replace(" ", "");
167
168        let mut s = String::new();
169        s.push_str(env!("CARGO_MANIFEST_DIR"));
170        s.push_str("/generated-docs/");
171        s.push_str(windows_safe_filename.as_str());
172        s.push_str($extension);
173        s
174    }};
175}
176
177// Writes doc files. See the module documentation for more info.
178// macro_export mainly for the docs that use it
179#[macro_export]
180macro_rules! doc {
181    (T, Option, Vec, $($t:tt)*) => {
182        ::doc_macro::example!($($t)*);
183        doc!(@inner T, $($t)*);
184        doc!(@inner Option, $($t)*);
185        doc!(@inner Vec, $($t)*);
186    };
187    (Option, Vec, $($t:tt)*) => {
188        ::doc_macro::example!($($t)*);
189        doc!(@inner Option, $($t)*);
190        doc!(@inner Vec, $($t)*);
191    };
192    (T, Option, $($t:tt)*) => {
193        ::doc_macro::example!($($t)*);
194        doc!(@inner T, $($t)*);
195        doc!(@inner Option, $($t)*);
196    };
197    (T, Vec, $($t:tt)*) => {
198        ::doc_macro::example!($($t)*);
199        doc!(@inner T, $($t)*);
200        doc!(@inner Vec, $($t)*);
201    };
202    (T, $($t:tt)*) => {
203        ::doc_macro::example!($($t)*);
204        doc!(@inner T, $($t)*);
205    };
206    (Option, $($t:tt)*) => {
207        ::doc_macro::example!($($t)*);
208        doc!(@inner Option, $($t)*);
209    };
210    (Vec, $($t:tt)*) => {
211        ::doc_macro::example!($($t)*);
212        doc!(@inner Vec, $($t)*);
213    };
214    // enum literal
215    (@inner T, $i:ident :: $($t:tt)*) => {
216        doc!($i, Example::example());
217    };
218    (@inner Option, $i:ident :: $($t:tt)*) => {
219        doc!(Option<$i>, Example::example());
220    };
221    (@inner Vec, $i:ident :: $($t:tt)*) => {
222        doc!(Vec<$i>, Example::example());
223    };
224    // struct literal
225    (@inner T, $i:ident $($t:tt)*) => {
226        doc!($i, Example::example());
227    };
228    (@inner Option, $i:ident $($t:tt)*) => {
229        doc!(Option<$i>, Example::example());
230    };
231    (@inner Vec, $i:ident $($t:tt)*) => {
232        doc!(Vec<$i>, Example::example());
233    };
234    // writes the actual docs
235    ($t:ty, $e:expr_2021) => {{
236        let expr: $t = $e;
237
238        let json_path = $crate::doc_path!(
239            stringify!($t),
240            ".json"
241        );
242        $crate::programs::doc_file_generator::write_json(&json_path, expr);
243    }};
244    // shortcut for doc!(T, ...)
245    ($($t:tt)*) => {
246        doc!(T, $($t)*);
247    };
248}
249
250pub async fn main() -> anyhow::Result<()> {
251    // clear previous results
252    fs::read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/generated-docs/"))
253        .map_err(|e| anyhow::anyhow!("Failed to read generated-docs directory: {}", e))?
254        .filter_map(|file| {
255            file.ok().filter(|f| {
256                f.file_name()
257                    .to_str()
258                    .is_some_and(|n| n.ends_with(".json") || n.ends_with(".ts"))
259            })
260        })
261        .try_for_each(|f| -> anyhow::Result<()> {
262            fs::remove_file(f.path())
263                .map_err(|e| anyhow::anyhow!("Failed to remove file {:?}: {}", f.path(), e))?;
264            Ok(())
265        })?;
266
267    // write docs
268    controllers();
269
270    external();
271
272    doc!((), ex());
273    doc!(i64, 123);
274    doc!(bool, ex());
275    doc!(
276        Vec<bool>,
277        vec![false, true, false, true, false, true, true, true]
278    );
279    doc!(String, ex());
280    doc!(Uuid, ex());
281    doc!(Vec<Uuid>, ex());
282
283    Ok(())
284}
285
286pub fn write_json<T: Serialize>(path: &str, value: T) {
287    let mut file =
288        std::fs::File::create(path).expect("Failed to create file for JSON serialization");
289    let formatter = PrettyFormatter::with_indent(b"    ");
290    let mut serializer = Serializer::with_formatter(&mut file, formatter);
291    serde::Serialize::serialize(&value, &mut serializer)
292        .expect("Failed to serialize value to JSON");
293}
294
295#[allow(non_local_definitions)]
296fn controllers() {
297    doc!(
298        T,
299        Vec,
300        StudyRegistryCompletion {
301            completion_date: Utc.with_ymd_and_hms(2022, 6, 21, 0, 0, 0).unwrap(),
302            completion_language: "en-US".to_string(),
303            completion_registration_attempt_date: None,
304            email: "student@example.com".to_string(),
305            grade: StudyRegistryGrade::new(true, Some(4)),
306            id: Uuid::parse_str("633852ce-c82a-4d60-8ab5-28745163f6f9")
307                .expect("Invalid UUID constant in doc generator"),
308            user_id,
309            tier: None
310        }
311    );
312}
313
314// External types can be added here if they need JSON example docs.
315fn external() {}