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