Skip to main content

headless_lms_credit_registration/registry/
mod.rs

1//! The study registry as the phases and the manual actions see it: what can be asked of it, in the
2//! pipeline's terms. Request ids, wire items, the limiter, the breakers and audit bodies are the
3//! adapter's; splitting a refused batch is the batch runner's.
4
5mod answers;
6mod batch;
7mod ids;
8mod requests;
9
10pub(crate) use answers::{
11    CourseCodeVerdicts, EnrolmentAnswer, EnrolmentReading, FoundPerson, HeldCredit, ImportAnswer,
12    PersonAnswer, PersonReading, RosterListing, RosterSearch, VerificationAnswer,
13    VerificationReading,
14};
15pub use answers::{PersonLookupError, RegistryPerson};
16pub(crate) use batch::{
17    AnsweredRow, BatchEntry, BatchOptions, BatchReply, BatchRequest, ExchangeAudit, RefusedFor,
18    RefusedRow,
19};
20pub(crate) use ids::{AttainmentId, CourseCode, StudentNumber, SubmittedAttainmentRef};
21pub(crate) use requests::{
22    AttainmentSubmission, Credits, EnrolmentLookup, PersonLookup, RosterCode, VerificationRequest,
23};
24
25use headless_lms_models::library::credit_registration::study_registry::RegistryErrorKind;
26pub(crate) use headless_lms_models::library::credit_registration::study_registry::RegistryOperation;
27
28/// The study registry a phase iteration asks, through its limiter and breakers. The seam a
29/// use-case test would fake; only the Suotar adapter implements it. Generic rather than `dyn`.
30///
31/// The batch operations each send one request and hand every row they were given back, answered
32/// or refused.
33pub(crate) trait StudyRegistry {
34    /// How many items the next request of `operation` may carry, or for a roster listing how many
35    /// requests may go out. 0 means send nothing this iteration.
36    fn allowance(&self, operation: RegistryOperation) -> usize;
37
38    /// How many course codes one roster listing request may carry: the registry's batch size, or
39    /// one while a breaker's probe lets a single item through.
40    fn roster_request_size(&self) -> usize;
41
42    async fn resolve_persons<K>(
43        &mut self,
44        entries: Vec<BatchEntry<K, PersonLookup>>,
45        options: BatchOptions,
46    ) -> BatchReply<K, PersonLookup, PersonAnswer>;
47
48    async fn resolve_enrolments<K>(
49        &mut self,
50        entries: Vec<BatchEntry<K, EnrolmentLookup>>,
51        options: BatchOptions,
52    ) -> BatchReply<K, EnrolmentLookup, EnrolmentAnswer>;
53
54    /// The one operation that creates something: a row it leaves unanswered may have landed.
55    async fn import_attainments<K>(
56        &mut self,
57        entries: Vec<BatchEntry<K, AttainmentSubmission>>,
58        options: BatchOptions,
59    ) -> BatchReply<K, AttainmentSubmission, ImportAnswer>;
60
61    async fn verify_attainments<K>(
62        &mut self,
63        entries: Vec<BatchEntry<K, VerificationRequest>>,
64        options: BatchOptions,
65    ) -> BatchReply<K, VerificationRequest, VerificationAnswer>;
66
67    /// One list-by-course request, spending one request of the allowance.
68    async fn list_course_roster(
69        &mut self,
70        request: &[RosterCode],
71    ) -> Result<RosterListing, RegistryError>;
72
73    /// Validates `codes` in requests as large as the allowance lets, until either runs out. On the
74    /// first refused request the verdicts gathered so far are dropped.
75    async fn validate_course_codes(
76        &mut self,
77        codes: &[CourseCode],
78    ) -> Result<CourseCodeVerdicts, RegistryError>;
79}
80
81/// The study registry for someone waiting on the answer: no allowance, and no breaker learns from
82/// its calls, so one click cannot trip the workers'. Not [`StudyRegistry`], whose calls go through
83/// the iteration's gate.
84pub(crate) trait InteractiveStudyRegistry {
85    /// `Ok(None)` when the registry answered `personNotFound`.
86    async fn look_up_person(
87        &self,
88        student_number: &StudentNumber,
89    ) -> Result<Option<RegistryPerson>, PersonLookupError>;
90
91    /// Lists each code's roster in a request of its own, all at once.
92    async fn search_course_rosters(
93        &self,
94        codes: &[CourseCode],
95        student_number: &StudentNumber,
96    ) -> RosterSearch;
97}
98
99/// A study registry request that failed as a whole: a value the rows' outcomes are decided from,
100/// not an error of the iteration.
101pub(crate) struct RegistryError {
102    pub kind: RegistryErrorKind,
103    /// Unscrubbed; scrub it before persisting it.
104    pub message: String,
105}
106
107impl RegistryError {
108    pub(crate) fn new(kind: RegistryErrorKind, message: impl Into<String>) -> Self {
109        Self {
110            kind,
111            message: message.into(),
112        }
113    }
114
115    /// Whether the failure may be down to the items the request carried. A connection that never
116    /// opened, or our own credentials, say nothing about any of them.
117    pub(crate) fn blames_request_items(&self) -> bool {
118        !matches!(
119            self.kind,
120            RegistryErrorKind::NotDelivered | RegistryErrorKind::AuthenticationFailure
121        )
122    }
123}