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}