Skip to main content

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}