Skip to main content

ezcapsolver/
response.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
2
3use crate::{Result, Solution};
4
5/// Envelope metadata included in every API response.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct ResponseMeta {
9    /// On success it is 0, on failure it is 1.
10    pub error_id: i32,
11    /// Request Tracking ID
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub request_id: Option<String>,
14    /// Error code
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub error_code: Option<String>,
17    /// Error message
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub error_description: Option<String>,
20}
21
22/// Response returned after an asynchronous task is created.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct CreateTaskResponse {
26    /// Shared response metadata.
27    #[serde(flatten)]
28    pub meta: ResponseMeta,
29    /// Identifier used to query the task result.
30    pub task_id: String,
31}
32
33/// Current state of a task.
34///
35/// The service reports exactly these three states, so this enum is closed and
36/// callers can match it exhaustively without a wildcard arm. A status outside
37/// this set is rejected during deserialization.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum TaskStatus {
40    /// The task is still being processed.
41    Processing,
42    /// The task completed successfully.
43    Ready,
44    /// The task failed.
45    Error,
46}
47
48impl TaskStatus {
49    /// Returns the wire representation of the status.
50    #[must_use]
51    pub const fn as_str(&self) -> &str {
52        match self {
53            Self::Processing => "processing",
54            Self::Ready => "ready",
55            Self::Error => "error",
56        }
57    }
58}
59
60impl Serialize for TaskStatus {
61    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
62    where
63        S: Serializer,
64    {
65        serializer.serialize_str(self.as_str())
66    }
67}
68
69impl<'de> Deserialize<'de> for TaskStatus {
70    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
71    where
72        D: Deserializer<'de>,
73    {
74        let value = String::deserialize(deserializer)?;
75        match value.as_str() {
76            "processing" => Ok(Self::Processing),
77            "ready" => Ok(Self::Ready),
78            "error" => Ok(Self::Error),
79            other => Err(D::Error::unknown_variant(
80                other,
81                &["processing", "ready", "error"],
82            )),
83        }
84    }
85}
86
87/// Response returned by task result queries and synchronous task creation.
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub struct TaskResult {
90    /// Shared response metadata.
91    #[serde(flatten)]
92    pub meta: ResponseMeta,
93    /// Current task status.
94    ///
95    /// This field is required. The service may omit `status` on request-level
96    /// failures such as an invalid key or an unknown task ID, but those carry
97    /// an error envelope that is rejected before this type is decoded. A
98    /// success envelope without a status therefore violates the API contract,
99    /// and reporting it as [`Error::UnexpectedResponse`] with the raw body is
100    /// more useful than silently substituting a placeholder status.
101    ///
102    /// [`Error::UnexpectedResponse`]: crate::Error::UnexpectedResponse
103    pub status: TaskStatus,
104    /// Identifier assigned by the synchronous task endpoint.
105    ///
106    /// `/createSyncTask` returns one on both the success and the failure path;
107    /// `/getTaskResult` does not echo it back, so this is `None` there.
108    ///
109    /// The name is spelled out because this struct has no `rename_all`: the
110    /// other fields already match the wire, and a blanket rule would have to be
111    /// re-checked against every one of them.
112    #[serde(rename = "taskId", default, skip_serializing_if = "Option::is_none")]
113    pub task_id: Option<String>,
114    /// Raw task solution, preserving a missing field separately from JSON null.
115    #[serde(default, skip_serializing_if = "Solution::is_missing")]
116    pub solution: Solution,
117}
118
119impl TaskResult {
120    /// Returns true when the task is still processing.
121    #[must_use]
122    pub fn is_processing(&self) -> bool {
123        self.status == TaskStatus::Processing
124    }
125
126    /// Returns true when the task completed successfully.
127    #[must_use]
128    pub fn is_ready(&self) -> bool {
129        self.status == TaskStatus::Ready
130    }
131
132    /// Deserializes the raw solution into a caller-selected type.
133    pub fn deserialize_solution<T>(&self) -> Result<T>
134    where
135        T: serde::de::DeserializeOwned,
136    {
137        self.solution.deserialize()
138    }
139}
140
141/// Completed task with its identifiers and solution.
142///
143/// This is an SDK-side aggregate rather than a wire type, so it is serializable
144/// for logging and storage but is never decoded from an API response.
145///
146/// Both execution modes return this shape, [`task_id`] included.
147///
148/// [`task_id`]: Solved::task_id
149#[derive(Debug, Clone, PartialEq, Serialize)]
150pub struct Solved<T> {
151    /// Identifier the service assigned to the task.
152    ///
153    /// Both endpoints supply one: the asynchronous path returns it from task
154    /// creation, and the synchronous path returns it alongside the result.
155    /// `None` only when the service omitted it.
156    pub task_id: Option<String>,
157    /// Request identifier returned by task creation.
158    pub request_id: Option<String>,
159    /// Decoded or raw task solution.
160    pub solution: T,
161    /// The untouched solution JSON.
162    ///
163    /// Whatever the typed model in [`solution`] does not declare stays
164    /// recoverable here, so a worker that starts returning a new field never
165    /// costs the caller data.
166    ///
167    /// [`solution`]: Solved::solution
168    pub raw: Solution,
169}
170
171/// Response returned by the balance endpoint.
172///
173/// Internal: the balance is exposed directly as an `f64`, so this envelope
174/// never reaches callers.
175///
176/// Only the two clients use it, so it is gated alongside them: without
177/// that, a build with neither client enabled reports it as dead code.
178#[cfg(any(feature = "async", feature = "blocking"))]
179#[derive(Debug, Clone, PartialEq, Deserialize)]
180pub(crate) struct BalanceResponse {
181    /// Shared response metadata.
182    #[serde(flatten)]
183    #[allow(dead_code, reason = "decoded from the envelope but not read")]
184    pub meta: ResponseMeta,
185    /// Balance returned by the service.
186    ///
187    /// The API sends a JSON number with at most four decimal places, which
188    /// `f64` round-trips exactly. The value is for display only, so exact
189    /// decimal arithmetic is not required here.
190    pub balance: f64,
191}