headless_lms_credit_registration/runtime/suotar/
rosters.rs1use futures::future::join_all;
5use headless_lms_utils::services::suotar::{
6 ListByCourseRequestItem, SuotarEndpoint, SuotarItemStatus, endpoints, new_request_item_id,
7};
8use secrecy::ExposeSecret;
9use tracing::Instrument;
10
11use super::decode::{registry_error, roster, roster_person};
12use super::encode::roster_item;
13use super::gate::Exchange;
14use super::{InteractiveSuotar, SuotarStudyRegistry};
15use crate::registry::{
16 CourseCode, RegistryError, RosterCode, RosterListing, RosterSearch, StudentNumber,
17};
18
19const ENDPOINT: SuotarEndpoint = SuotarEndpoint::ListByCourse;
20
21pub(super) async fn list(
22 registry: &mut SuotarStudyRegistry<'_>,
23 request: &[RosterCode],
24) -> Result<RosterListing, RegistryError> {
25 registry.gate.spend(ENDPOINT, 1);
26 let items: Vec<ListByCourseRequestItem> = request
27 .iter()
28 .map(|code| roster_item(&code.course_code, new_request_item_id()))
29 .collect();
30 let request_item_ids: Vec<String> = items
31 .iter()
32 .map(|item| item.request_item_id.clone())
33 .collect();
34 let span = super::request_span(ENDPOINT, items.len());
35 let response = registry
36 .client
37 .post::<endpoints::ListByCourse>(registry.call_context(Vec::new()), items)
38 .instrument(span)
39 .await;
40 let response = match response {
41 Ok(response) => response,
42 Err(error) => {
43 let refusal = registry_error(&error);
44 let is_known_bad_code = matches!(request, [only] if only.is_fetched_alone)
46 && refusal.blames_request_items()
47 && !refusal.kind.is_outage();
48 registry.gate.record(
49 ENDPOINT,
50 if is_known_bad_code {
51 Exchange::RefusedAlone(&error)
52 } else {
53 Exchange::Refused(&error)
54 },
55 );
56 return Err(refusal);
57 }
58 };
59 registry.gate.record(
60 ENDPOINT,
61 Exchange::answered(
62 &response,
63 "Every course code of the batch came back unavailable.",
64 ),
65 );
66 Ok(RosterListing {
67 duration: response.duration,
68 rosters: request
69 .iter()
70 .zip(&request_item_ids)
71 .map(|(code, request_item_id)| roster(&response, request_item_id, &code.course_code))
72 .collect(),
73 })
74}
75
76pub(super) async fn search(
77 registry: &InteractiveSuotar<'_>,
78 codes: &[CourseCode],
79 student_number: &StudentNumber,
80) -> RosterSearch {
81 let responses = join_all(codes.iter().map(|course_code| {
85 let span = super::request_span(ENDPOINT, 1);
86 registry
87 .client
88 .post::<endpoints::ListByCourse>(
89 registry.call_context(),
90 vec![roster_item(course_code, new_request_item_id())],
91 )
92 .instrument(span)
93 }))
94 .await;
95 let mut has_unanswered_code = false;
96 let mut person = None;
97 for response in responses {
98 let response = match response {
99 Ok(response) => response,
100 Err(error) => {
101 warn!(
102 error = %error,
103 "Could not list a course code's roster for a linking mail resend"
104 );
105 has_unanswered_code = true;
106 continue;
107 }
108 };
109 person = person.or_else(|| {
110 response
111 .items
112 .iter()
113 .filter(|item| item.status == SuotarItemStatus::Ok)
114 .filter_map(|item| item.result.as_ref())
115 .flat_map(|result| result.people.iter())
116 .find(|candidate| {
117 candidate.student_number.expose_secret() == student_number.expose()
118 })
119 .map(roster_person)
120 });
121 }
122 RosterSearch {
123 person,
124 has_unanswered_code,
125 }
126}