1use crate::{errors::AssignmentGradeServicesError, request::send_request};
2use chrono::{DateTime, Utc};
3use reqwest::header;
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6use std::time::SystemTime;
7
8#[derive(Debug, Serialize, Deserialize)]
9pub struct LineItem {
10 pub id: String,
11 #[serde(rename = "scoreMaximum")]
12 pub score_maximum: f32,
13 pub label: String,
14 pub tag: String,
15 #[serde(rename = "resourceId")]
16 pub resource_id: String,
17 #[serde(rename = "resourceLinkId")]
18 pub resource_link_id: String,
19 #[serde(rename = "https://canvas.instructure.com/lti/submission_type")]
20 pub submission_type: Option<SubmissionType>,
21 #[serde(rename = "https://canvas.instructure.com/lti/launch_url")]
22 pub launch_url: Option<String>,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26pub struct NewLineItem {
27 #[serde(rename = "scoreMaximum")]
28 pub score_maximum: f32,
29 pub label: String,
30 #[serde(rename = "resourceId")]
31 pub resource_id: String,
32 pub tag: String,
33 #[serde(rename = "resourceLinkId")]
34 pub resource_link_id: String,
35 #[serde(rename = "endDateTime")]
36 pub end_date_time: String, #[serde(rename = "https://canvas.instructure.com/lti/submission_type")]
38 pub submission_type: Option<SubmissionType>,
39}
40
41impl NewLineItem {
42 pub fn new(
43 score_maximum: f32,
44 label: String,
45 resource_id: String,
46 tag: String,
47 resource_link_id: String,
48 submission_type: Option<SubmissionType>,
49 ) -> Self {
50 let now = SystemTime::now();
51 let datetime: DateTime<Utc> = now.into();
52 let iso_string = datetime.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
53 Self {
54 score_maximum,
55 label,
56 resource_id,
57 tag,
58 resource_link_id,
59 end_date_time: iso_string,
60 submission_type,
61 }
62 }
63}
64
65#[derive(Debug, Serialize, Deserialize)]
66pub struct UpdateLineItem {
67 #[serde(rename = "scoreMaximum")]
68 pub score_maximum: f32,
69 pub label: String,
70 #[serde(rename = "resourceId")]
71 pub resource_id: String,
72 pub tag: String,
73 #[serde(rename = "resourceLinkId")]
74 pub resource_link_id: String,
75 #[serde(rename = "endDateTime")]
76 pub end_date_time: String, }
78
79impl UpdateLineItem {
80 pub fn new(
81 score_maximum: f32,
82 label: String,
83 resource_id: String,
84 tag: String,
85 resource_link_id: String,
86 ) -> Self {
87 let now = SystemTime::now();
88 let datetime: DateTime<Utc> = now.into();
89 let iso_string = datetime.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
90 Self {
91 score_maximum,
92 label,
93 resource_id,
94 tag,
95 resource_link_id,
96 end_date_time: iso_string,
97 }
98 }
99}
100
101#[derive(Debug, Serialize, Deserialize)]
102pub struct SubmissionType {
103 pub type_: String,
104 #[serde(alias = "externalToolUrl")]
105 pub external_tool_url: String,
106}
107
108#[derive(Debug, Serialize)]
113#[skip_serializing_none]
114pub struct ListParams {
115 pub tag: Option<String>,
116 pub resource_id: Option<String>,
117 pub resource_link_id: Option<String>,
118 pub limit: Option<usize>,
119 pub include: Option<Vec<String>>,
120}
121
122pub async fn list(
123 api_token: &str,
124 line_items_url: &str,
125 params: &ListParams,
126) -> Result<Vec<LineItem>, AssignmentGradeServicesError> {
127 let client = reqwest::Client::new();
128 let request = client
129 .get(line_items_url)
130 .header(header::AUTHORIZATION, format!("Bearer {}", api_token))
131 .header(
132 header::ACCEPT,
133 "application/vnd.ims.lis.v2.lineitemcontainer+json",
134 )
135 .query(¶ms);
136 let body = send_request(request).await?;
137
138 let response: Vec<LineItem> = serde_json::from_str(&body)
139 .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
140
141 Ok(response)
142}
143
144pub async fn show(
145 api_token: &str,
146 line_item_url: &str,
147) -> Result<LineItem, AssignmentGradeServicesError> {
148 let client = reqwest::Client::new();
149 let request = client
150 .get(line_item_url)
151 .header(header::AUTHORIZATION, format!("Bearer {}", api_token))
152 .header(header::ACCEPT, "application/vnd.ims.lis.v2.lineitem+json");
153 let body = send_request(request).await?;
154
155 let response: LineItem = serde_json::from_str(&body)
156 .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
157
158 Ok(response)
159}
160
161pub async fn create(
166 api_token: &str,
167 line_items_url: &str,
168 new_line_item: &NewLineItem,
169) -> Result<LineItem, AssignmentGradeServicesError> {
170 let client = reqwest::Client::new();
171 let json = serde_json::to_string(new_line_item)
172 .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
173 let request = client
174 .post(line_items_url)
175 .header(header::AUTHORIZATION, format!("Bearer {}", api_token))
176 .header(header::ACCEPT, "application/vnd.ims.lis.v2.lineitem+json")
177 .body(json);
178 let body = send_request(request).await?;
179
180 let response: LineItem = serde_json::from_str(&body)
181 .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
182
183 Ok(response)
184}
185
186pub async fn update(
187 api_token: &str,
188 line_item_url: &str,
189 update_line_item: &UpdateLineItem,
190) -> Result<LineItem, AssignmentGradeServicesError> {
191 let client = reqwest::Client::new();
192 let json = serde_json::to_string(update_line_item)
193 .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
194 let request = client
195 .put(line_item_url)
196 .header(header::AUTHORIZATION, format!("Bearer {}", api_token))
197 .header(header::ACCEPT, "application/vnd.ims.lis.v2.lineitem+json")
198 .body(json);
199 let body = send_request(request).await?;
200
201 let response: LineItem = serde_json::from_str(&body)
202 .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
203
204 Ok(response)
205}
206
207pub async fn delete(
208 api_token: &str,
209 line_item_url: &str,
210) -> Result<LineItem, AssignmentGradeServicesError> {
211 let client = reqwest::Client::new();
212
213 let request = client
214 .delete(line_item_url)
215 .header(header::AUTHORIZATION, format!("Bearer {}", api_token));
216 let body = send_request(request).await?;
217
218 let response: LineItem = serde_json::from_str(&body)
219 .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
220
221 Ok(response)
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use mockito;
228
229 #[tokio::test]
230 async fn test_list() {
231 let mut server = mockito::Server::new_async().await;
232 let server_url = server.url();
233 let mock = server
234 .mock("GET", "/line_items")
235 .with_status(200)
236 .with_header(
237 "content-type",
238 "application/vnd.ims.lis.v2.lineitemcontainer+json",
239 )
240 .with_body(
241 r#"[
242 {
243 "id": "line_item_1",
244 "scoreMaximum": 100.0,
245 "label": "Test Item",
246 "tag": "test",
247 "resourceId": "res1",
248 "resourceLinkId": "link1",
249 "submission_type": null,
250 "launch_url": null
251 }
252 ]"#,
253 )
254 .create();
255
256 let api_token = "not a real token";
257 let params = ListParams {
258 tag: None,
259 resource_id: None,
260 resource_link_id: None,
261 limit: None,
262 include: None,
263 };
264 let url = format!("{}/line_items", &server_url);
265 let result = list(api_token, &url, ¶ms).await;
266
267 mock.assert();
268 assert!(result.is_ok());
269 let response = result.unwrap();
270 assert_eq!(response.len(), 1);
271 assert_eq!(response[0].id, "line_item_1");
272 }
273
274 #[tokio::test]
275 async fn test_show() {
276 let mut server = mockito::Server::new_async().await;
277 let server_url = server.url();
278 let mock = server
279 .mock("GET", "/line_item_1")
280 .with_status(200)
281 .with_header("content-type", "application/vnd.ims.lis.v2.lineitem+json")
282 .with_body(
283 r#"{
284 "id": "line_item_1",
285 "scoreMaximum": 100.0,
286 "label": "Test Item",
287 "tag": "test",
288 "resourceId": "res1",
289 "resourceLinkId": "link1",
290 "submission_type": null,
291 "launch_url": null
292 }"#,
293 )
294 .create();
295
296 let api_token = "not a real token";
297 let result = show(api_token, &format!("{}/line_item_1", server_url)).await;
298
299 mock.assert();
300 assert!(result.is_ok());
301 let response = result.unwrap();
302 assert_eq!(response.id, "line_item_1");
303 }
304
305 #[tokio::test]
306 async fn test_create() {
307 let mut server = mockito::Server::new_async().await;
308 let server_url = server.url();
309 let mock = server
310 .mock("POST", "/line_items")
311 .with_status(201)
312 .with_header("content-type", "application/vnd.ims.lis.v2.lineitem+json")
313 .with_body(
314 r#"{
315 "id": "new_line_item",
316 "scoreMaximum": 100.0,
317 "label": "New Item",
318 "tag": "new",
319 "resourceId": "res_new",
320 "resourceLinkId": "link_new",
321 "submission_type": null,
322 "launch_url": null
323 }"#,
324 )
325 .create();
326
327 let api_token = "not a real token";
328 let new_line_item = NewLineItem::new(
329 100.0,
330 "New Item".to_string(),
331 "res_new".to_string(),
332 "new".to_string(),
333 "link_new".to_string(),
334 None,
335 );
336 let url = format!("{}/line_items", &server_url);
337 let result = create(api_token, &url, &new_line_item).await;
338
339 mock.assert();
340 assert!(result.is_ok());
341 let response = result.unwrap();
342 assert_eq!(response.id, "new_line_item");
343 }
344
345 #[tokio::test]
346 async fn test_update() {
347 let mut server = mockito::Server::new_async().await;
348 let server_url = server.url();
349 let mock = server
350 .mock("PUT", "/line_item_1")
351 .with_status(200)
352 .with_header("content-type", "application/vnd.ims.lis.v2.lineitem+json")
353 .with_body(
354 r#"{
355 "id": "line_item_1",
356 "scoreMaximum": 95.0,
357 "label": "Updated Item",
358 "tag": "updated",
359 "resourceId": "res1",
360 "resourceLinkId": "link1",
361 "submission_type": null,
362 "launch_url": null
363 }"#,
364 )
365 .create();
366
367 let api_token = "not a real token";
368 let update_line_item = UpdateLineItem::new(
369 95.0,
370 "Updated Item".to_string(),
371 "res1".to_string(),
372 "updated".to_string(),
373 "link1".to_string(),
374 );
375 let result = update(
376 api_token,
377 &format!("{}/line_item_1", server_url),
378 &update_line_item,
379 )
380 .await;
381
382 mock.assert();
383 assert!(result.is_ok());
384 let response = result.unwrap();
385 assert_eq!(response.label, "Updated Item");
386 }
387
388 #[tokio::test]
389 async fn test_delete() {
390 let mut server = mockito::Server::new_async().await;
391 let server_url = server.url();
392 let mock = server
393 .mock("DELETE", "/line_item_1")
394 .with_status(200)
395 .with_header("content-type", "application/vnd.ims.lis.v2.lineitem+json")
396 .with_body(
397 r#"{
398 "id": "line_item_1",
399 "scoreMaximum": 100.0,
400 "label": "Deleted Item",
401 "tag": "deleted",
402 "resourceId": "res1",
403 "resourceLinkId": "link1",
404 "submission_type": null,
405 "launch_url": null
406 }"#,
407 )
408 .create();
409
410 let api_token = "not a real token";
411 let result = delete(api_token, &format!("{}/line_item_1", server_url)).await;
412
413 mock.assert();
414 assert!(result.is_ok());
415 let response = result.unwrap();
416 assert_eq!(response.label, "Deleted Item");
417 }
418}