Skip to main content

ezcapsolver/
task_type.rs

1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
4
5/// Static task execution mode declared by the service.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum TaskMode {
8    /// The task is created and queried through the asynchronous API.
9    Async,
10    /// The task returns its result from the synchronous API.
11    Sync,
12}
13
14/// Known EzCaptchaSolver task type.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
16#[non_exhaustive]
17pub enum TaskType {
18    /// ReCaptcha V2 proxyless task.
19    #[default]
20    RecaptchaV2TaskProxyless,
21    /// ReCaptcha V2 high-score proxyless task.
22    RecaptchaV2TaskProxylessS9,
23    /// ReCaptcha V2 task carrying the `s` parameter.
24    RecaptchaV2STaskProxyless,
25    /// ReCaptcha V2 Enterprise proxyless task.
26    RecaptchaV2EnterpriseTaskProxyless,
27    /// ReCaptcha V2 Enterprise task carrying the `s` parameter.
28    RecaptchaV2SEnterpriseTaskProxyless,
29    /// ReCaptcha V2 image classification task.
30    RecaptchaV2Classification,
31    /// ReCaptcha V3 proxyless task.
32    RecaptchaV3TaskProxyless,
33    /// ReCaptcha V3 high-score proxyless task.
34    RecaptchaV3TaskProxylessS9,
35    /// ReCaptcha V3 Enterprise proxyless task.
36    RecaptchaV3EnterpriseTaskProxyless,
37    /// ReCaptcha V3 Enterprise high-score proxyless task.
38    RecaptchaV3EnterpriseTaskProxylessS9,
39    /// FunCaptcha proxyless task.
40    FuncaptchaTaskProxyless,
41    /// FunCaptcha image classification task.
42    FuncaptchaClassification,
43    /// PerimeterX task.
44    PerimeterX,
45    /// HCaptcha token task.
46    Hcaptcha,
47    /// HCaptcha image classification task.
48    HcaptchaClassification,
49    /// Akamai Web task.
50    AkamaiWebTaskProxyless,
51    /// Akamai SBSD task.
52    AkamaiSbsdTaskProxyless,
53    /// TLS forwarding task.
54    TlsTask,
55    /// Cloudflare five-second challenge task.
56    Cloudflare5sTask,
57    /// Cloudflare Turnstile task.
58    CloudflareTurnstileTask,
59    /// DataDome challenge task.
60    DataDomeTaskProxyless,
61    /// DataDome tags task.
62    DataDomeTagsTaskProxyless,
63    /// Incapsula Reese84 task.
64    IncapsulaTaskProxyless,
65}
66
67impl TaskType {
68    /// Task types declared by the current EzCaptchaSolver service contract.
69    pub const KNOWN: &'static [Self] = &[
70        Self::RecaptchaV2TaskProxyless,
71        Self::RecaptchaV2TaskProxylessS9,
72        Self::RecaptchaV2STaskProxyless,
73        Self::RecaptchaV2EnterpriseTaskProxyless,
74        Self::RecaptchaV2SEnterpriseTaskProxyless,
75        Self::RecaptchaV2Classification,
76        Self::RecaptchaV3TaskProxyless,
77        Self::RecaptchaV3TaskProxylessS9,
78        Self::RecaptchaV3EnterpriseTaskProxyless,
79        Self::RecaptchaV3EnterpriseTaskProxylessS9,
80        Self::FuncaptchaTaskProxyless,
81        Self::FuncaptchaClassification,
82        Self::PerimeterX,
83        Self::Hcaptcha,
84        Self::HcaptchaClassification,
85        Self::AkamaiWebTaskProxyless,
86        Self::AkamaiSbsdTaskProxyless,
87        Self::TlsTask,
88        Self::Cloudflare5sTask,
89        Self::CloudflareTurnstileTask,
90        Self::DataDomeTaskProxyless,
91        Self::DataDomeTagsTaskProxyless,
92        Self::IncapsulaTaskProxyless,
93    ];
94
95    /// Returns the canonical task type string.
96    #[must_use]
97    pub fn as_str(&self) -> &str {
98        match self {
99            Self::RecaptchaV2TaskProxyless => "ReCaptchaV2TaskProxyless",
100            Self::RecaptchaV2TaskProxylessS9 => "ReCaptchaV2TaskProxylessS9",
101            Self::RecaptchaV2STaskProxyless => "ReCaptchaV2STaskProxyless",
102            Self::RecaptchaV2EnterpriseTaskProxyless => "ReCaptchaV2EnterpriseTaskProxyless",
103            Self::RecaptchaV2SEnterpriseTaskProxyless => "ReCaptchaV2SEnterpriseTaskProxyless",
104            Self::RecaptchaV2Classification => "ReCaptchaV2Classification",
105            Self::RecaptchaV3TaskProxyless => "ReCaptchaV3TaskProxyless",
106            Self::RecaptchaV3TaskProxylessS9 => "ReCaptchaV3TaskProxylessS9",
107            Self::RecaptchaV3EnterpriseTaskProxyless => "ReCaptchaV3EnterpriseTaskProxyless",
108            // The catalog writes this one with a lowercase c. Every language SDK
109            // normalises it so one capitalisation runs through the whole ReCaptcha
110            // family; the service matches task types case-insensitively, so it
111            // reaches the same worker. Do not "fix" it back.
112            Self::RecaptchaV3EnterpriseTaskProxylessS9 => "ReCaptchaV3EnterpriseTaskProxylessS9",
113            Self::FuncaptchaTaskProxyless => "FuncaptchaTaskProxyless",
114            Self::FuncaptchaClassification => "FunCaptchaClassification",
115            Self::PerimeterX => "PerimeterX",
116            Self::Hcaptcha => "HCaptcha",
117            Self::HcaptchaClassification => "HCaptchaClassification",
118            Self::AkamaiWebTaskProxyless => "AkamaiWEBTaskProxyless",
119            Self::AkamaiSbsdTaskProxyless => "AkamaiSBSDTaskProxyless",
120            Self::TlsTask => "TlsTask",
121            Self::Cloudflare5sTask => "CloudFlare5STask",
122            Self::CloudflareTurnstileTask => "CloudFlareTurnstileTask",
123            Self::DataDomeTaskProxyless => "DataDomeTaskProxyless",
124            Self::DataDomeTagsTaskProxyless => "DataDomeTagsTaskProxyless",
125            Self::IncapsulaTaskProxyless => "IncapsulaTaskProxyless",
126        }
127    }
128
129    /// Returns the execution mode the service documents for this type.
130    ///
131    /// This is **informational**. Every type has both a `solve_*` method that
132    /// polls and a `sync_solve_*` method that uses the synchronous endpoint, and
133    /// nothing in the SDK consults this value to pick between them.
134    ///
135    /// It matters because the service can reject a type on the endpoint it does
136    /// not serve — so this is the mode to follow when you have no reason to
137    /// prefer the other. Such a rejection is refused before billing, so it
138    /// costs a round trip rather than a task.
139    #[must_use]
140    pub const fn mode(&self) -> Option<TaskMode> {
141        match self {
142            Self::RecaptchaV2TaskProxyless
143            | Self::RecaptchaV2TaskProxylessS9
144            | Self::RecaptchaV2STaskProxyless
145            | Self::RecaptchaV2EnterpriseTaskProxyless
146            | Self::RecaptchaV2SEnterpriseTaskProxyless
147            | Self::RecaptchaV3TaskProxyless
148            | Self::RecaptchaV3TaskProxylessS9
149            | Self::RecaptchaV3EnterpriseTaskProxyless
150            | Self::RecaptchaV3EnterpriseTaskProxylessS9
151            | Self::FuncaptchaTaskProxyless
152            | Self::PerimeterX
153            | Self::Hcaptcha
154            | Self::Cloudflare5sTask
155            | Self::CloudflareTurnstileTask => Some(TaskMode::Async),
156            Self::RecaptchaV2Classification
157            | Self::FuncaptchaClassification
158            | Self::HcaptchaClassification
159            | Self::AkamaiWebTaskProxyless
160            | Self::AkamaiSbsdTaskProxyless
161            | Self::TlsTask
162            | Self::DataDomeTaskProxyless
163            | Self::DataDomeTagsTaskProxyless
164            | Self::IncapsulaTaskProxyless => Some(TaskMode::Sync),
165        }
166    }
167
168    fn from_wire(value: &str) -> Option<Self> {
169        Self::KNOWN
170            .iter()
171            .find(|task_type| task_type.as_str().eq_ignore_ascii_case(value))
172            .copied()
173    }
174}
175
176/// Source of the `type` value sent with a task.
177///
178/// Implemented for [`TaskType`] and for plain strings, so a task type the
179/// service adds after this release can still be used without waiting for an
180/// SDK update: pass the type name directly.
181///
182/// ```no_run
183/// # use ezcapsolver::{AsyncEzCapSolverClient, TaskType};
184/// # async fn run(client: &AsyncEzCapSolverClient) -> ezcapsolver::Result<()> {
185/// # let task = serde_json::json!({});
186/// // A modelled type
187/// client.create_task(TaskType::RecaptchaV2TaskProxyless, &task).await?;
188/// // The escape hatch: a type the service added that the SDK has yet to model
189/// client.create_task("BrandNewTaskType", &task).await?;
190/// # Ok(())
191/// # }
192/// ```
193pub trait TaskTypeName {
194    /// Returns the wire value written to the `type` field.
195    fn task_type_name(&self) -> &str;
196}
197
198impl TaskTypeName for TaskType {
199    fn task_type_name(&self) -> &str {
200        self.as_str()
201    }
202}
203
204impl TaskTypeName for str {
205    fn task_type_name(&self) -> &str {
206        self
207    }
208}
209
210impl TaskTypeName for String {
211    fn task_type_name(&self) -> &str {
212        self
213    }
214}
215
216impl<T> TaskTypeName for &T
217where
218    T: TaskTypeName + ?Sized,
219{
220    fn task_type_name(&self) -> &str {
221        (*self).task_type_name()
222    }
223}
224
225impl fmt::Display for TaskType {
226    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
227        formatter.write_str(self.as_str())
228    }
229}
230
231impl Serialize for TaskType {
232    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
233    where
234        S: Serializer,
235    {
236        serializer.serialize_str(self.as_str())
237    }
238}
239
240impl<'de> Deserialize<'de> for TaskType {
241    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
242    where
243        D: Deserializer<'de>,
244    {
245        let value = String::deserialize(deserializer)?;
246        Self::from_wire(&value)
247            .ok_or_else(|| D::Error::custom(format!("unknown task type `{value}`")))
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn known_type_deserialization_is_case_insensitive() -> Result<(), serde_json::Error> {
257        let task_type: TaskType = serde_json::from_str(r#""recaptchav2taskproxyless""#)?;
258        assert_eq!(task_type, TaskType::RecaptchaV2TaskProxyless);
259        assert_eq!(
260            serde_json::to_string(&task_type)?,
261            r#""ReCaptchaV2TaskProxyless""#
262        );
263        Ok(())
264    }
265
266    #[test]
267    fn a_plain_string_can_stand_in_for_a_task_type() {
268        // The escape hatch: a new service type needs no SDK release.
269        assert_eq!("BrandNewTaskType".task_type_name(), "BrandNewTaskType");
270        assert_eq!(
271            TaskType::RecaptchaV2TaskProxyless.task_type_name(),
272            "ReCaptchaV2TaskProxyless"
273        );
274    }
275
276    #[test]
277    fn unknown_type_is_rejected() {
278        let error = serde_json::from_str::<TaskType>(r#""FutureTask""#).unwrap_err();
279        assert!(error.to_string().contains("unknown task type `FutureTask`"));
280    }
281}