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}