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/// Matches the `CloudTags` schema, which is a HATEOAS envelope carrying the
253/// owning account ID and links rather than an inline tag array.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct CloudTags {
257 /// Account ID owning the tags.
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub account_id: Option<i32>,
260
261 /// HATEOAS links.
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub links: Option<Vec<Link>>,
264}
265
266/// Traffic state for a database, returned by the `.../traffic` endpoints.
267///
268/// Matches the `DatabaseTrafficStateResponse` schema. Reports whether traffic
269/// to the database is currently stopped and, if so, whether it can be resumed.
270/// Shared by the Pro and Essentials (fixed) database traffic endpoints.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct DatabaseTrafficStateResponse {
274 /// Database (BDB) ID the traffic state applies to.
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub bdb_id: Option<i32>,
277
278 /// Current traffic status (e.g. `"active"`, `"stopped"`).
279 #[serde(skip_serializing_if = "Option::is_none")]
280 pub traffic_status: Option<String>,
281
282 /// Whether traffic can currently be resumed.
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub can_resume: Option<bool>,
285
286 /// Whether a resume operation is already in progress.
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub resume_in_progress: Option<bool>,
289
290 /// Reason traffic was stopped, when applicable.
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub stop_reason: Option<String>,
293
294 /// Timestamp from which traffic becomes eligible to resume.
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub resume_eligible_at: Option<String>,
297}
298
299// ============================================================================
300// Common Response Types
301// ============================================================================
302
303/// Generic paginated-response wrapper.
304///
305/// The `data` field is flattened so the inner shape's fields appear at the
306/// same JSON level as the pagination metadata.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct PaginatedResponse<T> {
309 /// Inner page payload.
310 #[serde(flatten)]
311 pub data: T,
312
313 /// Zero-based offset of the first item in this page.
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub offset: Option<u32>,
316
317 /// Maximum number of items returned per page.
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub limit: Option<u32>,
320
321 /// Total number of items across all pages.
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub total: Option<u32>,
324}
325
326/// HATEOAS link to a related API resource.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct Link {
329 /// Relationship name (e.g. `"self"`, `"databases"`).
330 pub rel: String,
331 /// Absolute URL the link points to.
332 pub href: String,
333
334 /// HTTP method to use when following the link, if specified.
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub method: Option<String>,
337
338 /// Media type the linked resource will return.
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub r#type: Option<String>,
341}
342
343/// Collection wrapper for a list of [`Link`].
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct Links {
346 /// Links in the collection.
347 pub links: Vec<Link>,
348}
349
350// ============================================================================
351// Common Enums used across multiple endpoints
352// ============================================================================
353
354/// Cloud provider hosting a Redis Cloud resource.
355#[derive(Debug, Clone, Serialize, Deserialize)]
356#[serde(rename_all = "UPPERCASE")]
357pub enum CloudProvider {
358 /// Amazon Web Services.
359 Aws,
360 /// Google Cloud Platform.
361 Gcp,
362 /// Microsoft Azure.
363 Azure,
364}
365
366/// Redis protocol exposed by a database endpoint.
367#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(rename_all = "lowercase")]
369pub enum Protocol {
370 /// Standard Redis protocol.
371 Redis,
372 /// Memcached-compatible protocol.
373 Memcached,
374 /// Redis Stack — Redis with bundled modules (JSON, Search, etc.).
375 Stack,
376}
377
378/// Database persistence policy.
379///
380/// Variant wire names follow the Redis Cloud convention (e.g.
381/// `"aof-every-1-sec"`).
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub enum DataPersistence {
384 /// No persistence; data lives only in memory.
385 #[serde(rename = "none")]
386 None,
387 /// Append-only file flushed every second.
388 #[serde(rename = "aof-every-1-sec")]
389 AofEvery1Sec,
390 /// Append-only file flushed on every write.
391 #[serde(rename = "aof-every-write")]
392 AofEveryWrite,
393 /// RDB snapshot every hour.
394 #[serde(rename = "snapshot-every-1-hour")]
395 SnapshotEvery1Hour,
396 /// RDB snapshot every six hours.
397 #[serde(rename = "snapshot-every-6-hours")]
398 SnapshotEvery6Hours,
399 /// RDB snapshot every twelve hours.
400 #[serde(rename = "snapshot-every-12-hours")]
401 SnapshotEvery12Hours,
402}
403
404/// Lifecycle status of a Pro subscription.
405#[derive(Debug, Clone, Serialize, Deserialize)]
406#[serde(rename_all = "lowercase")]
407pub enum SubscriptionStatus {
408 /// Subscription is being created; not yet ready for use.
409 Pending,
410 /// Subscription is operational.
411 Active,
412 /// Subscription is in the process of being deleted.
413 Deleting,
414 /// Subscription is in an error state and may require operator action.
415 Error,
416}
417
418/// Lifecycle status of a database.
419#[derive(Debug, Clone, Serialize, Deserialize)]
420#[serde(rename_all = "lowercase")]
421pub enum DatabaseStatus {
422 /// Database is being created; not yet ready for connections.
423 Pending,
424 /// Database is operational.
425 Active,
426 /// Database is operational but has pending configuration changes.
427 ActiveChangePending,
428 /// Database is being populated from an import source.
429 ImportPending,
430 /// Database is queued for deletion.
431 DeletePending,
432 /// Database is in recovery from a failure.
433 Recovery,
434 /// Database is in an error state.
435 Error,
436}
437
438// ============================================================================
439// Utility Types
440// ============================================================================
441
442/// Empty response body. Returned by operations that succeed without payload.
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct EmptyResponse {}
445
446/// Generic error response body.
447///
448/// The Redis Cloud API uses several different error shapes; this struct
449/// captures the most common keys and leaves all of them optional so callers
450/// can match on whichever fields the server returned.
451#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct ErrorResponse {
453 /// Short error code or category (e.g. `"NOT_FOUND"`).
454 #[serde(skip_serializing_if = "Option::is_none")]
455 pub error: Option<String>,
456
457 /// Human-readable error message.
458 #[serde(skip_serializing_if = "Option::is_none")]
459 pub message: Option<String>,
460
461 /// Additional free-form context about the error.
462 #[serde(skip_serializing_if = "Option::is_none")]
463 pub description: Option<String>,
464
465 /// HTTP status code echoed in the body.
466 #[serde(skip_serializing_if = "Option::is_none")]
467 pub status_code: Option<u16>,
468}