api/routes/modules/assignments/config/get.rs
1use crate::response::ApiResponse;
2use axum::{
3 extract::{Path, State},
4 http::StatusCode,
5 response::IntoResponse,
6 Json,
7};
8use db::models::assignment::{Column as AssignmentColumn, Entity as AssignmentEntity};
9use db::models::assignment_file::{Entity as AssignmentFile, Column as AssignmentFileColumn, FileType, Model as AssignmentFileModel};
10use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
11use serde_json::to_value;
12use util::{execution_config::ExecutionConfig, state::AppState};
13
14/// GET /api/modules/{module_id}/assignments/{assignment_id}/config
15///
16/// Retrieve the JSON configuration object associated with a specific assignment. Accessible to users
17/// assigned to the module with appropriate permissions.
18///
19/// The configuration object is loaded from disk using the [`ExecutionConfig`] schema. If no configuration
20/// file is present on disk, an empty config is returned instead.
21///
22/// ### Path Parameters
23/// - `module_id` (i64): The ID of the module containing the assignment
24/// - `assignment_id` (i64): The ID of the assignment to retrieve configuration for
25///
26/// ### Example Request
27/// ```bash
28/// curl -X GET http://localhost:3000/api/modules/1/assignments/2/config \
29/// -H "Authorization: Bearer <token>"
30/// ```
31///
32/// ### Success Response (200 OK) - With Configuration
33/// ```json
34/// {
35/// "success": true,
36/// "message": "Assignment configuration retrieved successfully",
37/// "data": {
38/// "execution": {
39/// "timeout_secs": 10,
40/// "max_memory": 8589934592,
41/// "max_cpus": 2,
42/// "max_uncompressed_size": 100000000,
43/// "max_processes": 256
44/// },
45/// "marking": {
46/// "marking_scheme": "exact",
47/// "feedback_scheme": "auto",
48/// "deliminator": "&-=-&"
49/// }
50/// }
51/// }
52/// ```
53///
54/// ### Success Response (200 OK) - No Configuration File
55/// ```json
56/// {
57/// "success": true,
58/// "message": "No configuration set for this assignment",
59/// "data": {}
60/// }
61/// ```
62///
63/// ### Error Responses
64/// - **404** – Assignment not found
65/// - **500** – Failed to load configuration from disk
66///
67/// ### Notes
68/// - Configurations are stored on disk under `ASSIGNMENT_STORAGE_ROOT/module_{id}/assignment_{id}/config/config.json`
69/// - Config format uses [`ExecutionConfig`] as the schema
70/// - This is an example schema and will evolve over time
71pub async fn get_assignment_config(
72 State(app_state): State<AppState>,
73 Path((module_id, assignment_id)): Path<(i64, i64)>,
74) -> impl IntoResponse {
75 let db = app_state.db();
76
77 // Verify the assignment exists
78 match AssignmentEntity::find()
79 .filter(AssignmentColumn::Id.eq(assignment_id as i32))
80 .filter(AssignmentColumn::ModuleId.eq(module_id as i32))
81 .one(db)
82 .await
83 {
84 Ok(Some(_)) => {}
85 Ok(None) => {
86 return (
87 StatusCode::NOT_FOUND,
88 Json(ApiResponse::<()>::error("Assignment or module not found")),
89 )
90 .into_response();
91 }
92 Err(e) => {
93 eprintln!("DB error: {:?}", e);
94 return (
95 StatusCode::INTERNAL_SERVER_ERROR,
96 Json(ApiResponse::<()>::error("Database error")),
97 )
98 .into_response();
99 }
100 }
101
102 // Look up the latest config assignment file
103 let config_file: Option<AssignmentFileModel> = match AssignmentFile::find()
104 .filter(AssignmentFileColumn::AssignmentId.eq(assignment_id))
105 .filter(AssignmentFileColumn::FileType.eq(FileType::Config))
106 .order_by_desc(AssignmentFileColumn::UpdatedAt)
107 .one(db)
108 .await
109 {
110 Ok(opt) => opt,
111 Err(e) => {
112 eprintln!("DB error while fetching config file: {:?}", e);
113 return (
114 StatusCode::INTERNAL_SERVER_ERROR,
115 Json(ApiResponse::<()>::error("Database error")),
116 )
117 .into_response();
118 }
119 };
120
121 // Load the config from the file model
122 match config_file {
123 Some(file_model) => match file_model.load_execution_config(module_id) {
124 Ok(cfg) => {
125 let json = to_value(cfg).unwrap_or_else(|_| serde_json::json!({}));
126 (
127 StatusCode::OK,
128 Json(ApiResponse::success(json, "Assignment configuration retrieved successfully")),
129 )
130 .into_response()
131 }
132 Err(err) => {
133 eprintln!("Failed to load config from disk: {}", err);
134 (
135 StatusCode::OK,
136 Json(ApiResponse::success(
137 serde_json::json!({}),
138 "No configuration set for this assignment",
139 )),
140 )
141 .into_response()
142 }
143 },
144 None => (
145 StatusCode::OK,
146 Json(ApiResponse::success(
147 serde_json::json!({}),
148 "No configuration set for this assignment",
149 )),
150 )
151 .into_response(),
152 }
153}
154
155/// GET /api/modules/{module_id}/assignments/{assignment_id}/config/default
156///
157/// Returns the default execution configuration used when no custom config file is present.
158/// This helps clients pre-fill configuration forms or understand system defaults.
159///
160/// ### Success Response (200 OK)
161/// ```json
162/// {
163/// "success": true,
164/// "message": "Default execution config retrieved successfully",
165/// "data":
166/// {
167// "execution": {
168// "timeout_secs": 10,
169// "max_memory": 1000000,
170// "max_cpus": 2,
171// "max_uncompressed_size": 1000000,
172// "max_processes": 256
173// },
174// "marking": {
175// "marking_scheme": "exact",
176// "feedback_scheme": "auto",
177// "deliminator": "&-=-&"
178// }
179// }
180
181/// }
182/// ```
183pub async fn get_default_assignment_config(
184 Path((_module_id, _assignment_id)): Path<(i64, i64)>,
185) -> impl IntoResponse {
186 let default_config = ExecutionConfig::default_config();
187 (
188 StatusCode::OK,
189 Json(ApiResponse::success(
190 default_config,
191 "Default execution config retrieved successfully",
192 )),
193 )
194}