1use bytes::{Buf, BufMut};
13
14#[cfg(test)]
15use crate::krabka::test_support::sample_tagged_fields;
16use crate::{
17 Decode, Encode, ProtocolError, ProtocolRequest, UnknownTaggedFields,
18 krabka::common::{
19 I8_LEN, I16_LEN, I32_LEN, I64_LEN, UUID_LEN, check_version, read_unknown_tagged_fields,
20 unknown_tagged_fields_len, write_unknown_tagged_fields,
21 },
22 primitives::{
23 fixed::{get_i8, get_i16, get_i32, get_i64, put_i8, put_i16, put_i32, put_i64},
24 string_bytes::{
25 compact_nullable_string_len, compact_string_len, get_compact_nullable_string_owned,
26 get_compact_string_owned, put_compact_nullable_string, put_compact_string,
27 },
28 uuid::{Uuid, get_uuid, put_uuid},
29 },
30};
31
32pub const API_KEY: i16 = 1017;
34pub const MIN_VERSION: i16 = 0;
36pub const MAX_VERSION: i16 = 0;
38pub const FLEXIBLE_MIN: i16 = 0;
40
41#[inline]
46#[must_use]
47pub fn is_flexible(version: i16) -> bool {
48 version >= FLEXIBLE_MIN
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Default)]
53pub struct ProposeBreakGlassRequest {
54 pub action: i8,
57 pub target: String,
60 pub reason: String,
62 pub ttl_ms: i64,
66 pub unknown_tagged_fields: UnknownTaggedFields,
68}
69
70impl Encode for ProposeBreakGlassRequest {
71 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
72 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
73 put_i8(buf, self.action);
74 put_compact_string(buf, &self.target)?;
75 put_compact_string(buf, &self.reason)?;
76 put_i64(buf, self.ttl_ms);
77 write_unknown_tagged_fields(buf, &self.unknown_tagged_fields)?;
78 Ok(())
79 }
80
81 fn encoded_len(&self, _version: i16) -> usize {
82 I8_LEN
83 + compact_string_len(&self.target)
84 + compact_string_len(&self.reason)
85 + I64_LEN
86 + unknown_tagged_fields_len(&self.unknown_tagged_fields)
87 }
88}
89
90impl Decode<'_> for ProposeBreakGlassRequest {
91 fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
92 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
93 Ok(Self {
94 action: get_i8(buf)?,
95 target: get_compact_string_owned(buf)?,
96 reason: get_compact_string_owned(buf)?,
97 ttl_ms: get_i64(buf)?,
98 unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
99 })
100 }
101}
102
103impl ProtocolRequest for ProposeBreakGlassRequest {
104 const API_KEY: i16 = API_KEY;
105 const MIN_VERSION: i16 = MIN_VERSION;
106 const MAX_VERSION: i16 = MAX_VERSION;
107 const LATEST_STABLE_VERSION: i16 = MAX_VERSION;
108 const FLEXIBLE_MIN: i16 = FLEXIBLE_MIN;
109
110 type Response = ProposeBreakGlassResponse;
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Default)]
115pub struct ProposeBreakGlassResponse {
116 pub throttle_time_ms: i32,
118 pub error_code: i16,
120 pub error_message: Option<String>,
123 pub proposal_id: Uuid,
127 pub expires_at_ms: i64,
130 pub unknown_tagged_fields: UnknownTaggedFields,
132}
133
134impl Encode for ProposeBreakGlassResponse {
135 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
136 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
137 put_i32(buf, self.throttle_time_ms);
138 put_i16(buf, self.error_code);
139 put_compact_nullable_string(buf, self.error_message.as_deref())?;
140 put_uuid(buf, self.proposal_id);
141 put_i64(buf, self.expires_at_ms);
142 write_unknown_tagged_fields(buf, &self.unknown_tagged_fields)?;
143 Ok(())
144 }
145
146 fn encoded_len(&self, _version: i16) -> usize {
147 I32_LEN
148 + I16_LEN
149 + compact_nullable_string_len(self.error_message.as_deref())
150 + UUID_LEN
151 + I64_LEN
152 + unknown_tagged_fields_len(&self.unknown_tagged_fields)
153 }
154}
155
156impl Decode<'_> for ProposeBreakGlassResponse {
157 fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
158 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
159 Ok(Self {
160 throttle_time_ms: get_i32(buf)?,
161 error_code: get_i16(buf)?,
162 error_message: get_compact_nullable_string_owned(buf)?,
163 proposal_id: get_uuid(buf)?,
164 expires_at_ms: get_i64(buf)?,
165 unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
166 })
167 }
168}
169
170#[cfg(test)]
171impl ProposeBreakGlassRequest {
172 fn populated() -> Self {
174 Self {
175 action: 6,
176 target: "doomed".to_owned(),
177 reason: "topic holds test data only".to_owned(),
178 ttl_ms: 1_800_000,
179 unknown_tagged_fields: sample_tagged_fields(),
180 }
181 }
182}
183
184#[cfg(test)]
185impl ProposeBreakGlassResponse {
186 fn populated() -> Self {
188 Self {
189 throttle_time_ms: 4,
190 error_code: 0,
191 error_message: None,
192 proposal_id: Uuid([
193 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215,
194 ]),
195 expires_at_ms: 1_770_000_180_000,
196 unknown_tagged_fields: sample_tagged_fields(),
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use assert2::assert;
204
205 use super::*;
206 use crate::krabka::test_support::{reject_truncated, roundtrip_case, unsupported_versions};
207
208 #[test]
209 fn requests_roundtrip() {
210 let cases = [
211 ("default", ProposeBreakGlassRequest::default()),
212 ("delete a topic", ProposeBreakGlassRequest::populated()),
213 (
214 "configured lifetime and no reason",
215 ProposeBreakGlassRequest {
216 action: 1,
217 target: "literal:orders".to_owned(),
218 reason: String::new(),
219 ttl_ms: 0,
220 unknown_tagged_fields: UnknownTaggedFields::default(),
221 },
222 ),
223 ];
224 for (label, request) in &cases {
225 for version in MIN_VERSION..=MAX_VERSION {
226 roundtrip_case(label, request, version);
227 }
228 }
229 }
230
231 #[test]
232 fn responses_roundtrip() {
233 let cases = [
234 ("default", ProposeBreakGlassResponse::default()),
235 ("accepted proposal", ProposeBreakGlassResponse::populated()),
236 (
237 "refusal carries no proposal",
238 ProposeBreakGlassResponse {
239 throttle_time_ms: 0,
240 error_code: 1008,
241 error_message: Some("principal is not an approver".to_owned()),
242 proposal_id: Uuid::ZERO,
243 expires_at_ms: 0,
244 unknown_tagged_fields: UnknownTaggedFields::default(),
245 },
246 ),
247 ];
248 for (label, response) in &cases {
249 for version in MIN_VERSION..=MAX_VERSION {
250 roundtrip_case(label, response, version);
251 }
252 }
253 }
254
255 #[test]
256 fn a_default_request_asks_for_the_configured_lifetime() {
257 assert!(ProposeBreakGlassRequest::default().ttl_ms == 0);
258 }
259
260 #[test]
261 fn truncated_input_is_rejected() {
262 reject_truncated(&ProposeBreakGlassRequest::populated(), MIN_VERSION);
263 reject_truncated(&ProposeBreakGlassResponse::populated(), MIN_VERSION);
264 }
265
266 #[test]
267 fn rejects_unsupported_versions() {
268 for version in unsupported_versions(MIN_VERSION, MAX_VERSION) {
269 let request = ProposeBreakGlassRequest::populated();
270 assert!(matches!(
271 request.encode(&mut Vec::<u8>::new(), version),
272 Err(ProtocolError::UnsupportedVersion {
273 api_key: API_KEY,
274 ..
275 })
276 ));
277 let response = ProposeBreakGlassResponse::populated();
278 assert!(matches!(
279 response.encode(&mut Vec::<u8>::new(), version),
280 Err(ProtocolError::UnsupportedVersion {
281 api_key: API_KEY,
282 ..
283 })
284 ));
285 }
286 }
287}