Skip to main content

headless_lms_credit_registration/registry/
batch.rs

1//! One batch request to the study registry and what came of it, row by row.
2
3use chrono::{DateTime, Utc};
4use headless_lms_models::credit_registration_events::SuotarAnswer;
5use headless_lms_models::suotar_api_calls::SuotarEndpoint;
6use uuid::Uuid;
7
8use super::ids::StudentNumber;
9use super::{RegistryError, RegistryOperation, StudyRegistry};
10
11/// What one item of a batch operation asks: the item decides the operation it goes out under and the
12/// answer it gets back.
13pub(crate) trait BatchRequest: Sized {
14    const OPERATION: RegistryOperation;
15    type Answer;
16
17    /// Sends `entries` in one request of [`Self::OPERATION`].
18    async fn send<Registry: StudyRegistry, K>(
19        registry: &mut Registry,
20        entries: Vec<BatchEntry<K, Self>>,
21        options: BatchOptions,
22    ) -> BatchReply<K, Self, Self::Answer>;
23}
24
25/// One claimed row of a batch and the item it asks.
26pub(crate) struct BatchEntry<K, R> {
27    pub row: K,
28    pub request: R,
29}
30
31/// How one batch request is sent and accounted for.
32#[derive(Debug, Clone)]
33pub(crate) struct BatchOptions {
34    /// Every row takes the shared request-level refusal, so a malformed-request refusal of a batch
35    /// of several rows comes back as [`BatchReply::RefusedAsMalformed`] for the caller to split.
36    pub may_split: bool,
37    /// A half of a batch refused as malformed, which the limiter lets through past its allowance.
38    pub is_resent_half: bool,
39    /// The iteration's error when every item comes back unavailable.
40    pub all_unavailable_error: &'static str,
41    /// The rows' registrations, which the call is tagged with in the audit log.
42    pub registration_ids: Vec<Uuid>,
43}
44
45/// What came of one batch request.
46pub(crate) enum BatchReply<K, R, A> {
47    /// Every row, in request order; a row the registry did not answer has no answer.
48    Answered(Vec<AnsweredRow<K, A>>),
49    /// The request failed as a whole.
50    Refused {
51        rows: Vec<RefusedRow<K>>,
52        error: RegistryError,
53        refused_for: RefusedFor,
54    },
55    /// A splittable batch of several rows refused as malformed. Suotar validates every item before
56    /// acting on any, so nothing was acted on, and no breaker counts it; the rows come back as
57    /// sent, for the caller to resend in halves.
58    RefusedAsMalformed {
59        entries: Vec<BatchEntry<K, R>>,
60        error: RegistryError,
61    },
62}
63
64/// Whose a whole-request refusal is.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub(crate) enum RefusedFor {
67    WholeBatch,
68    /// A malformed request refused with the row alone in it, which resending cannot fix.
69    RowAlone,
70}
71
72pub(crate) struct AnsweredRow<K, A> {
73    pub row: K,
74    pub answer: Option<A>,
75    pub audit: ExchangeAudit,
76}
77
78pub(crate) struct RefusedRow<K> {
79    pub row: K,
80    pub audit: ExchangeAudit,
81}
82
83/// The exchange behind one row's answer, as its ledger event records it. No `Debug`: the bodies are
84/// unscrubbed, and are scrubbed only on their way into the event row.
85pub(crate) struct ExchangeAudit {
86    /// `suotar_api_calls.id`; `None` on a refusal, or when the call row could not be written.
87    pub call_id: Option<Uuid>,
88    pub endpoint: SuotarEndpoint,
89    /// Taken just before the request left.
90    pub requested_at: DateTime<Utc>,
91    /// Taken when the answer or refusal arrived.
92    pub answered_at: DateTime<Utc>,
93    pub answer: SuotarAnswer,
94    /// What the row's item went out under in that request.
95    pub request_item_id: String,
96    pub request: serde_json::Value,
97    /// The row's item exactly as it arrived.
98    pub response: Option<serde_json::Value>,
99    /// The number the request carried, which may no longer be the linked one.
100    pub sent_student_number: Option<StudentNumber>,
101}