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/// 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}