headless_lms_server/controllers/mock_suotar/
commands.rs1use serde_json::json;
4use sqlx::PgPool;
5
6use crate::mock_suotar::commands::{
7 CommandResult, MockSuotarCommand, WORLD_DUMP_CALL_LIMIT, execute,
8};
9use crate::mock_suotar::default_world;
10use crate::mock_suotar::store::{EntityHash, MockSuotarStore};
11use crate::mock_suotar::world::{
12 MockAttainment, MockCourseUnit, MockEnrolment, MockPerson, MockSubmission,
13};
14use crate::prelude::*;
15
16pub async fn health(
17 app_conf: web::Data<ApplicationConfiguration>,
18 store: web::Data<MockSuotarStore>,
19 pool: web::Data<PgPool>,
20) -> ControllerResult<HttpResponse> {
21 super::assert_enabled(&app_conf);
22 let token = skip_authorize();
23 let db_generation = default_world::db_generation_marker(&pool).await;
24 let generation = match store.live_generation().await {
25 Ok(generation) => generation,
26 Err(error) => return token.authorized_ok(internal_error(&error)),
27 };
28 let body = match &generation {
29 Some(generation) => {
30 let (counts, preamble) = match (
31 store.counts(generation).await,
32 store.preamble(generation).await,
33 ) {
34 (Ok(counts), Ok(preamble)) => (counts, preamble),
35 (Err(error), _) | (_, Err(error)) => {
36 return token.authorized_ok(internal_error(&error));
37 }
38 };
39 json!({
40 "enabled": true,
41 "generation": generation,
42 "dbGeneration": db_generation,
43 "worldDbGeneration": preamble.db_generation,
44 "generationMatches": preamble.db_generation.is_some()
45 && preamble.db_generation == db_generation,
46 "counts": counts,
47 "defaults": preamble.defaults,
48 })
49 }
50 None => json!({
52 "enabled": true,
53 "generation": serde_json::Value::Null,
54 "dbGeneration": db_generation,
55 "worldDbGeneration": serde_json::Value::Null,
56 "generationMatches": false,
57 "counts": serde_json::Value::Null,
58 "defaults": serde_json::Value::Null,
59 }),
60 };
61 token.authorized_ok(HttpResponse::Ok().json(body))
62}
63
64pub async fn world(
65 app_conf: web::Data<ApplicationConfiguration>,
66 store: web::Data<MockSuotarStore>,
67) -> ControllerResult<HttpResponse> {
68 super::assert_enabled(&app_conf);
69 let token = skip_authorize();
70 let Some(generation) = (match store.live_generation().await {
71 Ok(generation) => generation,
72 Err(error) => return token.authorized_ok(internal_error(&error)),
73 }) else {
74 return token.authorized_ok(HttpResponse::Ok().json(json!({ "generation": null })));
75 };
76 match dump(&store, &generation).await {
77 Ok(body) => token.authorized_ok(HttpResponse::Ok().json(body)),
78 Err(error) => token.authorized_ok(internal_error(&error)),
79 }
80}
81
82async fn dump(store: &MockSuotarStore, generation: &str) -> anyhow::Result<serde_json::Value> {
83 let preamble = store.preamble(generation).await?;
84 let counts = store.counts(generation).await?;
85 Ok(json!({
86 "generation": generation,
87 "defaults": preamble.defaults,
88 "persons": store.all_json::<MockPerson>(generation, EntityHash::Persons).await?,
89 "courseUnits": store.all_json::<MockCourseUnit>(generation, EntityHash::CourseUnits).await?,
90 "enrolments": store.all_json::<MockEnrolment>(generation, EntityHash::Enrolments).await?,
91 "attainments": store.all_json::<MockAttainment>(generation, EntityHash::Attainments).await?,
92 "submissions": store.all_json::<MockSubmission>(generation, EntityHash::Submissions).await?,
93 "sisuViolations": store.all_json::<Vec<String>>(generation, EntityHash::SisuViolations).await?,
94 "faults": store.faults(generation).await?,
95 "calls": store.recent_calls(generation, WORLD_DUMP_CALL_LIMIT).await?,
96 "callLogLen": counts.call_log_len,
97 }))
98}
99
100pub async fn command(
101 app_conf: web::Data<ApplicationConfiguration>,
102 store: web::Data<MockSuotarStore>,
103 pool: web::Data<PgPool>,
104 body: web::Bytes,
105) -> ControllerResult<HttpResponse> {
106 super::assert_enabled(&app_conf);
107 let token = skip_authorize();
108 let parsed: MockSuotarCommand = match serde_json::from_slice(&body) {
109 Ok(parsed) => parsed,
110 Err(error) => {
111 return token.authorized_ok(HttpResponse::BadRequest().json(CommandResult::Error {
112 command: None,
113 code: "unknownCommand".to_string(),
114 message: error.to_string(),
115 }));
116 }
117 };
118 let result = execute(&store, &pool, parsed).await;
119 token.authorized_ok(match &result {
120 CommandResult::Ok { .. } => HttpResponse::Ok().json(&result),
121 CommandResult::NotImplemented { .. } => HttpResponse::NotImplemented().json(&result),
122 CommandResult::Error { code, .. } if code == "internalError" => {
123 HttpResponse::InternalServerError().json(&result)
124 }
125 CommandResult::Error { .. } => HttpResponse::BadRequest().json(&result),
126 })
127}
128
129fn internal_error(error: &anyhow::Error) -> HttpResponse {
130 error!("mock Suotar control failure: {error:?}");
131 HttpResponse::InternalServerError().json(CommandResult::Error {
132 command: None,
133 code: "internalError".to_string(),
134 message: error.to_string(),
135 })
136}
137
138pub fn _add_routes(cfg: &mut ServiceConfig) {
140 cfg.route("/command", web::post().to(command))
141 .route("/health", web::get().to(health))
142 .route("/world", web::get().to(world));
143}