redis_cloud/types.rs
1//! Shared types for the Redis Cloud REST API.
2//!
3//! Types in this module are referenced from many endpoints and modules in the
4//! crate. They model the cross-cutting concepts the API itself reuses:
5//!
6//! - **Task tracking** — [`TaskStateUpdate`], [`TaskStatus`],
7//! [`TasksStateUpdate`], and [`ProcessorResponse`] surface the async
8//! workflow the Cloud API uses for most mutating operations: a request
9//! returns a task ID; the caller polls `GET /tasks/{taskId}` until
10//! completion.
11//! - **HATEOAS navigation** — [`Link`] and [`Links`] capture the `links`
12//! arrays the API returns alongside most resources.
13//! - **Tagging** — [`Tag`] is the key/value pair used in request bodies;
14//! [`CloudTag`] and [`CloudTags`] model the richer shapes the database tag
15//! endpoints return.
16//! - **Common enums** — [`CloudProvider`], [`Protocol`],
17//! [`DataPersistence`], [`SubscriptionStatus`], [`DatabaseStatus`] are
18//! shared across the database, subscription, and connectivity modules.
19//! - **Generic wrappers** — [`PaginatedResponse`], [`EmptyResponse`],
20//! [`ErrorResponse`] for cross-cutting response shapes.
21//!
22//! These models are the single canonical location for shared shapes (#64):
23//! endpoint modules import them rather than redefining their own copies of
24//! `TaskStateUpdate` and the tag types.
25
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28
29// ============================================================================
30// Task Types (Most common - appears in 37 endpoints)
31// ============================================================================
32
33/// State of an asynchronous Redis Cloud task.
34///
35/// Returned by most mutating operations in the API. The `task_id` can be
36/// polled via [`TasksHandler::get_task_by_id`] until `status` reaches a
37/// terminal value (see [`TaskStatus`]).
38///
39/// [`TasksHandler::get_task_by_id`]: crate::tasks::TasksHandler::get_task_by_id
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct TaskStateUpdate {
43 /// UUID of the task. Use with `TasksHandler::get_task_by_id` to poll.
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub task_id: Option<String>,
46
47 /// Type of command being executed (e.g. `"CREATE_DATABASE"`).
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub command_type: Option<String>,
50
51 /// Current task status. See [`TaskStatus`].
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub status: Option<TaskStatus>,
54
55 /// Human-readable description of the task.
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub description: Option<String>,
58
59 /// Timestamp of the last task update (ISO-8601 string).
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub timestamp: Option<String>,
62
63 /// Task completion percentage (0-100), when the processor reports it.
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub progress: Option<f64>,
66
67 /// Result of the task once it has completed. See [`ProcessorResponse`].
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub response: Option<ProcessorResponse>,
70
71 /// HATEOAS links for related resources (e.g. the resource the task
72 /// produced).
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub links: Option<Vec<Link>>,
75}
76
77/// Terminal and intermediate states of a Redis Cloud task.
78///
79/// Status values use kebab-case on the wire (e.g. `"processing-completed"`).
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81#[serde(rename_all = "kebab-case")]
82pub enum TaskStatus {
83 /// Task has been created but not yet picked up by the processor.
84 Initialized,
85 /// Task has been received by the processor.
86 Received,
87 /// Task is currently executing. Poll again later.
88 ProcessingInProgress,
89 /// Task completed successfully. See `response.resource_id` for the
90 /// resulting resource ID.
91 ProcessingCompleted,
92 /// Task failed during processing. See `response.error` for details.
93 ProcessingError,
94 /// A status value not recognized by this client.
95 ///
96 /// The wire format is a free-form string and the server may introduce
97 /// new states; unknown values deserialize here rather than failing the
98 /// whole response. Note this variant does not round-trip — serializing it
99 /// emits `"unknown"`, not the original wire value.
100 #[serde(other)]
101 Unknown,
102}
103
104/// Result payload included on a completed [`TaskStateUpdate`].
105///
106/// On success, `resource_id` points at the created/modified resource.
107/// On failure, `error` carries a message and the optional `additional_info`
108/// may carry more context.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct ProcessorResponse {
112 /// ID of the primary resource created or modified by the task.
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub resource_id: Option<i32>,
115
116 /// ID of an additional resource, if the operation produced one.
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub additional_resource_id: Option<i32>,
119
120 /// Free-form resource details. Shape varies by operation type.
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub resource: Option<std::collections::HashMap<String, Value>>,
123
124 /// Error detail, populated only when the task failed.
125 ///
126 /// The Redis Cloud API returns this in two shapes depending on the
127 /// failure: sometimes a plain string, and sometimes a structured object
128 /// (e.g. `{"type": ..., "status": ..., "description": ...}`). It is kept
129 /// as a [`Value`] so both deserialize cleanly; use
130 /// [`ProcessorResponse::error_message`] to extract a human-readable string.
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub error: Option<Value>,
133
134 /// Free-form additional context attached by the processor.
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub additional_info: Option<String>,
137}
138
139impl ProcessorResponse {
140 /// Extract a human-readable error message from [`Self::error`], regardless
141 /// of whether the server returned a plain string or a structured object.
142 ///
143 /// For the object shape, prefers the `description`, then `message`, then
144 /// `error` keys; falls back to the compact JSON encoding if none are
145 /// present. Returns `None` when no error is set.
146 pub fn error_message(&self) -> Option<String> {
147 let error = self.error.as_ref()?;
148 match error {
149 Value::String(s) => Some(s.clone()),
150 Value::Object(map) => {
151 let field = ["description", "message", "error"]
152 .iter()
153 .find_map(|key| map.get(*key).and_then(Value::as_str));
154 Some(field.map_or_else(|| error.to_string(), str::to_string))
155 }
156 other => Some(other.to_string()),
157 }
158 }
159}
160
161/// Coarse subset of Redis Cloud's processor error codes.
162///
163/// The full server-side enumeration has 600+ values; this type captures the
164/// few that callers typically branch on. Unknown codes fall through to
165/// [`Self::Other`].
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub enum ProcessorError {
168 /// Authentication failed for the requested operation.
169 #[serde(rename = "UNAUTHORIZED")]
170 Unauthorized,
171 /// The target resource was not found.
172 #[serde(rename = "NOT_FOUND")]
173 NotFound,
174 /// The request was malformed or missing required fields.
175 #[serde(rename = "BAD_REQUEST")]
176 BadRequest,
177 /// Unspecified processor failure.
178 #[serde(rename = "GENERAL_ERROR")]
179 GeneralError,
180 /// Catch-all for codes not enumerated here.
181 #[serde(other)]
182 Other,
183}
184
185/// Wrapper for the `GET /tasks` response (`{tasks: [...]}`).
186///
187/// See [`TasksHandler::get_all_tasks`] for the public ergonomic API that
188/// unwraps to `Vec<TaskStateUpdate>`.
189///
190/// [`TasksHandler::get_all_tasks`]: crate::tasks::TasksHandler::get_all_tasks
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct TasksStateUpdate {
193 /// Tasks returned by the server.
194 #[serde(default)]
195 pub tasks: Vec<TaskStateUpdate>,
196}
197
198// ============================================================================
199// Tag Types (Used in database and subscription endpoints)
200// ============================================================================
201
202/// Key-value tag used in create/update request bodies and embedded tag lists.
203///
204/// Matches the `Tag` schema (`key`/`value` required). `command_type` is a
205/// server-populated read-only field that appears on some responses; it is
206/// skipped on serialization when absent.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct Tag {
210 /// Tag key.
211 pub key: String,
212
213 /// Tag value.
214 pub value: String,
215
216 /// Read-only on the response; populated by the server with the
217 /// operation type (e.g. `"CREATE_DATABASE_TAG"`).
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub command_type: Option<String>,
220}
221
222/// A single tag as returned by the database tag endpoints.
223///
224/// Matches the `CloudTag` schema: all fields optional, with creation/update
225/// timestamps and HATEOAS links alongside the key/value pair.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct CloudTag {
229 /// Tag key.
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub key: Option<String>,
232
233 /// Tag value.
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub value: Option<String>,
236
237 /// Timestamp when the tag was created.
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub created_at: Option<String>,
240
241 /// Timestamp when the tag was last updated.
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub updated_at: Option<String>,
244
245 /// HATEOAS links.
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub links: Option<Vec<Link>>,
248}
249
250/// Collection wrapper returned by the database tags listing endpoints.
251///
252/// The OpenAPI `CloudTags` schema describes only `accountId`/`links`, but the
253/// live `GET .../tags` response includes an inline `tags` array when the
254/// database has tags — so the array is captured here too (see #130).
255#[derive(Debug, Clone, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct CloudTags {
258 /// Account ID owning the tags.
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub account_id: Option<i32>,
261
262 /// Tags on the database. Present (possibly empty) on real responses; the
263 /// OpenAPI schema omits it.
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub tags: Option<Vec<CloudTag>>,
266
267 /// HATEOAS links.
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub links: Option<Vec<Link>>,
270}
271
272/// Traffic state for a database, returned by the `.../traffic` endpoints.
273///
274/// Matches the `DatabaseTrafficStateResponse` schema. Reports whether traffic
275/// to the database is currently stopped and, if so, whether it can be resumed.
276/// Shared by the Pro and Essentials (fixed) database traffic endpoints.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct DatabaseTrafficStateResponse {
280 /// Database (BDB) ID the traffic state applies to.
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub bdb_id: Option<i32>,
283
284 /// Current traffic status (e.g. `"active"`, `"stopped"`).
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub traffic_status: Option<String>,
287
288 /// Whether traffic can currently be resumed.
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub can_resume: Option<bool>,
291
292 /// Whether a resume operation is already in progress.
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub resume_in_progress: Option<bool>,
295
296 /// Reason traffic was stopped, when applicable.
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub stop_reason: Option<String>,
299
300 /// Timestamp from which traffic becomes eligible to resume.
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub resume_eligible_at: Option<String>,
303}
304
305// ============================================================================
306// Common Response Types
307// ============================================================================
308
309/// Generic paginated-response wrapper.
310///
311/// The `data` field is flattened so the inner shape's fields appear at the
312/// same JSON level as the pagination metadata.
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct PaginatedResponse<T> {
315 /// Inner page payload.
316 #[serde(flatten)]
317 pub data: T,
318
319 /// Zero-based offset of the first item in this page.
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub offset: Option<u32>,
322
323 /// Maximum number of items returned per page.
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub limit: Option<u32>,
326
327 /// Total number of items across all pages.
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub total: Option<u32>,
330}
331
332/// HATEOAS link to a related API resource.
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct Link {
335 /// Relationship name (e.g. `"self"`, `"databases"`).
336 pub rel: String,
337 /// Absolute URL the link points to.
338 pub href: String,
339
340 /// HTTP method to use when following the link, if specified.
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub method: Option<String>,
343
344 /// Media type the linked resource will return.
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub r#type: Option<String>,
347}
348
349/// Collection wrapper for a list of [`Link`].
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct Links {
352 /// Links in the collection.
353 pub links: Vec<Link>,
354}
355
356// ============================================================================
357// Common Enums used across multiple endpoints
358// ============================================================================
359
360/// Cloud provider hosting a Redis Cloud resource.
361#[derive(Debug, Clone, Serialize, Deserialize)]
362#[serde(rename_all = "UPPERCASE")]
363pub enum CloudProvider {
364 /// Amazon Web Services.
365 Aws,
366 /// Google Cloud Platform.
367 Gcp,
368 /// Microsoft Azure.
369 Azure,
370}
371
372/// Redis protocol exposed by a database endpoint.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(rename_all = "lowercase")]
375pub enum Protocol {
376 /// Standard Redis protocol.
377 Redis,
378 /// Memcached-compatible protocol.
379 Memcached,
380 /// Redis Stack — Redis with bundled modules (JSON, Search, etc.).
381 Stack,
382}
383
384/// Database persistence policy.
385///
386/// Variant wire names follow the Redis Cloud convention (e.g.
387/// `"aof-every-1-sec"`).
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub enum DataPersistence {
390 /// No persistence; data lives only in memory.
391 #[serde(rename = "none")]
392 None,
393 /// Append-only file flushed every second.
394 #[serde(rename = "aof-every-1-sec")]
395 AofEvery1Sec,
396 /// Append-only file flushed on every write.
397 #[serde(rename = "aof-every-write")]
398 AofEveryWrite,
399 /// RDB snapshot every hour.
400 #[serde(rename = "snapshot-every-1-hour")]
401 SnapshotEvery1Hour,
402 /// RDB snapshot every six hours.
403 #[serde(rename = "snapshot-every-6-hours")]
404 SnapshotEvery6Hours,
405 /// RDB snapshot every twelve hours.
406 #[serde(rename = "snapshot-every-12-hours")]
407 SnapshotEvery12Hours,
408}
409
410/// Lifecycle status of a Pro subscription.
411#[derive(Debug, Clone, Serialize, Deserialize)]
412#[serde(rename_all = "lowercase")]
413pub enum SubscriptionStatus {
414 /// Subscription is being created; not yet ready for use.
415 Pending,
416 /// Subscription is operational.
417 Active,
418 /// Subscription is in the process of being deleted.
419 Deleting,
420 /// Subscription is in an error state and may require operator action.
421 Error,
422}
423
424/// Lifecycle status of a database.
425#[derive(Debug, Clone, Serialize, Deserialize)]
426#[serde(rename_all = "lowercase")]
427pub enum DatabaseStatus {
428 /// Database is being created; not yet ready for connections.
429 Pending,
430 /// Database is operational.
431 Active,
432 /// Database is operational but has pending configuration changes.
433 ActiveChangePending,
434 /// Database is being populated from an import source.
435 ImportPending,
436 /// Database is queued for deletion.
437 DeletePending,
438 /// Database is in recovery from a failure.
439 Recovery,
440 /// Database is in an error state.
441 Error,
442}
443
444// ============================================================================
445// Utility Types
446// ============================================================================
447
448/// Empty response body. Returned by operations that succeed without payload.
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct EmptyResponse {}
451
452/// Generic error response body.
453///
454/// The Redis Cloud API uses several different error shapes; this struct
455/// captures the most common keys and leaves all of them optional so callers
456/// can match on whichever fields the server returned.
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct ErrorResponse {
459 /// Short error code or category (e.g. `"NOT_FOUND"`).
460 #[serde(skip_serializing_if = "Option::is_none")]
461 pub error: Option<String>,
462
463 /// Human-readable error message.
464 #[serde(skip_serializing_if = "Option::is_none")]
465 pub message: Option<String>,
466
467 /// Additional free-form context about the error.
468 #[serde(skip_serializing_if = "Option::is_none")]
469 pub description: Option<String>,
470
471 /// HTTP status code echoed in the body.
472 #[serde(skip_serializing_if = "Option::is_none")]
473 pub status_code: Option<u16>,
474}