cortexkit_usage_steps/ops.rs
1use serde::{Deserialize, Serialize};
2
3use crate::{codes, Cursor, StepRow};
4
5/// Request of `usage.steps.subscribe`: `{}` for every notice, or
6/// `{harness, session}` for the notices of one session. The provider refuses
7/// an unknown member `malformed`, and so does this type.
8///
9/// `harness` and `session` are both optional and also accept `null`; each
10/// must be given together with the other or not at all. This type does not
11/// check that pairing: the provider refuses a request naming only one
12/// `invalid_request`, as it does for `limit`. See
13/// [`SubscribeRequest::check_pairing`].
14#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16#[non_exhaustive]
17pub struct SubscribeRequest {
18 /// The producer to follow; see [`StepRow::harness`](crate::StepRow::harness).
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub harness: Option<String>,
21 /// The session to follow, matched exactly.
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub session: Option<String>,
24}
25
26impl SubscribeRequest {
27 /// Notices for every session.
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 /// Notices only when a row of this harness and session is committed.
33 pub fn for_session(mut self, harness: impl Into<String>, session: impl Into<String>) -> Self {
34 self.harness = Some(harness.into());
35 self.session = Some(session.into());
36 self
37 }
38
39 /// Reports a request naming only one of `harness` and `session`. The
40 /// pair comes together or not at all; the provider refuses a request that
41 /// names one alone `invalid_request`. Both together and neither are fine.
42 pub fn check_pairing(&self) -> Result<(), &'static str> {
43 pairing::check(self.harness.is_some(), self.session.is_some())
44 }
45}
46
47/// The `harness`-with-`session` rule both requests share. The request types
48/// accept either member alone, as they accept any `limit`. The pairing helper
49/// returns an error naming the missing member, and the provider refuses such a
50/// request `invalid_request`.
51pub(crate) mod pairing {
52 pub(crate) fn check(harness: bool, session: bool) -> Result<(), &'static str> {
53 match (harness, session) {
54 (true, true) | (false, false) => Ok(()),
55 (true, false) => Err("request names `harness` without `session`"),
56 (false, true) => Err("request names `session` without `harness`"),
57 }
58 }
59}
60
61/// One StreamData frame of an open subscription: new steps may exist up to
62/// `through`, the newest committed cursor.
63///
64/// Notices are coalesced (at most four a second), and one may cover many
65/// steps or none the caller may see. It carries no rows; read them with
66/// `usage.steps.read`. Unknown members are ignored.
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68#[non_exhaustive]
69pub struct Notice {
70 pub through: Cursor,
71}
72
73impl Notice {
74 pub fn new(through: impl Into<Cursor>) -> Self {
75 Self {
76 through: through.into(),
77 }
78 }
79
80 /// Reports a member outside the published v1 notice list, by name. For
81 /// the provider's own tests; readers ignore unknown members instead.
82 pub fn check_closed(value: &serde_json::Value) -> Result<(), crate::NotClosed> {
83 crate::row::check_members("notice", value, &["through"])
84 }
85}
86
87/// Request of `usage.steps.read`. Both members are optional and also accept
88/// `null`; any other member is refused `malformed`, and so does this type.
89///
90/// Without `after`, the read starts at the oldest row the feed still holds
91/// (the rows committed in the last 24 hours). `limit` defaults to 200; 0 is
92/// refused `invalid_request` and above 1,000 `range_too_large`. This type
93/// does not check `limit`: the provider does.
94///
95/// `harness` and `session` narrow the read to one session's rows. Without
96/// `after`, such a read also starts at the oldest row the feed holds. They
97/// come together or not at all: this type accepts either alone, and the
98/// provider refuses one alone `invalid_request`, as it refuses a bad
99/// `limit`. See [`ReadRequest::check_pairing`].
100#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102#[non_exhaustive]
103pub struct ReadRequest {
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub after: Option<Cursor>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub limit: Option<u32>,
108 /// The producer to read rows of; see [`StepRow::harness`](crate::StepRow::harness).
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub harness: Option<String>,
111 /// The session to read rows of, matched exactly.
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub session: Option<String>,
114}
115
116impl ReadRequest {
117 /// A read from the oldest row the feed holds, with the default limit.
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 /// Reads the rows after `cursor`.
123 pub fn after(mut self, cursor: impl Into<Cursor>) -> Self {
124 self.after = Some(cursor.into());
125 self
126 }
127
128 /// Asks for at most `limit` rows.
129 pub fn with_limit(mut self, limit: u32) -> Self {
130 self.limit = Some(limit);
131 self
132 }
133
134 /// Reads only the rows whose `harness` and `session` are these.
135 pub fn for_session(mut self, harness: impl Into<String>, session: impl Into<String>) -> Self {
136 self.harness = Some(harness.into());
137 self.session = Some(session.into());
138 self
139 }
140
141 /// Reports a request naming only one of `harness` and `session`. The
142 /// pair comes together or not at all; the provider refuses a request that
143 /// names one alone `invalid_request`. Both together and neither are fine.
144 pub fn check_pairing(&self) -> Result<(), &'static str> {
145 pairing::check(self.harness.is_some(), self.session.is_some())
146 }
147}
148
149/// Reply of `usage.steps.read`. Unknown members are ignored.
150#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
151#[non_exhaustive]
152pub struct ReadReply {
153 /// Oldest first. Several rows may share an identity; the last one wins.
154 pub rows: Vec<StepRow>,
155 /// The cursor to pass as `after` for the following page, or null when
156 /// this reply reached `through`. Always sent, as null when absent.
157 #[serde(default)]
158 pub next: Option<Cursor>,
159 /// The newest committed cursor.
160 pub through: Cursor,
161}
162
163impl ReadReply {
164 pub fn new(rows: Vec<StepRow>, next: Option<Cursor>, through: impl Into<Cursor>) -> Self {
165 Self {
166 rows,
167 next,
168 through: through.into(),
169 }
170 }
171
172 /// The cursor the next read passes as `after`: `next` while more pages
173 /// remain, otherwise `through`.
174 pub fn resume_after(&self) -> &Cursor {
175 self.next.as_ref().unwrap_or(&self.through)
176 }
177
178 /// Reports a reply member, or a member of any row, outside the published
179 /// v1 lists. For the provider's own tests: whatever it emits must stay
180 /// within the published lists. Readers ignore unknown members instead.
181 pub fn check_closed(value: &serde_json::Value) -> Result<(), crate::NotClosed> {
182 crate::row::check_members("reply", value, &["rows", "next", "through"])?;
183 if let Some(serde_json::Value::Array(rows)) = value.get("rows") {
184 for row in rows {
185 StepRow::check_closed(row)?;
186 }
187 }
188 Ok(())
189 }
190}
191
192/// The body of an Error frame: a refusal of either operation, or the `lagged`
193/// or `cancelled` ending of a subscription.
194///
195/// `code` is one of [`codes`] (an unknown code is kept as received);
196/// `message` is for people and is not a stable interface. `lagged` and
197/// `cursor_expired` carry `detail` `{"through": <cursor>}`. Unknown members
198/// are ignored.
199#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
200#[non_exhaustive]
201pub struct Refusal {
202 pub code: String,
203 #[serde(default)]
204 pub message: String,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub detail: Option<serde_json::Value>,
207}
208
209impl Refusal {
210 /// A refusal without detail.
211 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
212 Self {
213 code: code.into(),
214 message: message.into(),
215 detail: None,
216 }
217 }
218
219 /// The `lagged` ending: `through` is the last notified cursor.
220 pub fn lagged(through: impl Into<Cursor>, message: impl Into<String>) -> Self {
221 Self::new(codes::LAGGED, message).with_through(through)
222 }
223
224 /// The `cancelled` ending a provider may send after the caller's own
225 /// Cancel. A reader treats it, after its own Cancel, as the normal end.
226 pub fn cancelled(message: impl Into<String>) -> Self {
227 Self::new(codes::CANCELLED, message)
228 }
229
230 /// The `cursor_expired` refusal of a read: `through` is the newest cursor.
231 pub fn cursor_expired(through: impl Into<Cursor>, message: impl Into<String>) -> Self {
232 Self::new(codes::CURSOR_EXPIRED, message).with_through(through)
233 }
234
235 fn with_through(mut self, through: impl Into<Cursor>) -> Self {
236 self.detail = Some(serde_json::json!({ "through": through.into() }));
237 self
238 }
239
240 /// The cursor in `detail.through`, when the refusal carries one.
241 pub fn through(&self) -> Option<Cursor> {
242 self.detail
243 .as_ref()?
244 .get("through")?
245 .as_str()
246 .map(Cursor::new)
247 }
248}