eoka-protocol 0.2.5

Protocol and tool catalog definitions for eoka browser automation
Documentation
use serde::{Deserialize, Serialize};

pub const DATADOME_DEFAULT_ATTEMPTS: u32 = 1;
pub const DATADOME_MAX_ATTEMPTS: u32 = 4;
pub const DATADOME_DEFAULT_TIMEOUT_MS: u64 = 60_000;
pub const DATADOME_MIN_TIMEOUT_MS: u64 = 5_000;
pub const DATADOME_MAX_TIMEOUT_MS: u64 = 120_000;

#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct CaptchaDatadomeArgs {
    #[serde(deserialize_with = "deserialize_attempts")]
    #[schemars(range(min = 1, max = DATADOME_MAX_ATTEMPTS))]
    pub max_attempts: u32,
    #[serde(deserialize_with = "deserialize_timeout")]
    #[schemars(range(min = DATADOME_MIN_TIMEOUT_MS, max = DATADOME_MAX_TIMEOUT_MS))]
    pub timeout_ms: u64,
}

fn deserialize_attempts<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
    let value = u32::deserialize(d)?;
    validate_attempts(value).map_err(serde::de::Error::custom)?;
    Ok(value)
}

fn deserialize_timeout<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
    let value = u64::deserialize(d)?;
    validate_timeout(value).map_err(serde::de::Error::custom)?;
    Ok(value)
}

fn validate_attempts(value: u32) -> Result<(), &'static str> {
    if !(1..=DATADOME_MAX_ATTEMPTS).contains(&value) {
        return Err("max_attempts must be between 1 and 4");
    }
    Ok(())
}

fn validate_timeout(value: u64) -> Result<(), &'static str> {
    if !(DATADOME_MIN_TIMEOUT_MS..=DATADOME_MAX_TIMEOUT_MS).contains(&value) {
        return Err("timeout_ms must be between 5000 and 120000");
    }
    Ok(())
}

impl Default for CaptchaDatadomeArgs {
    fn default() -> Self {
        Self {
            max_attempts: DATADOME_DEFAULT_ATTEMPTS,
            timeout_ms: DATADOME_DEFAULT_TIMEOUT_MS,
        }
    }
}

impl CaptchaDatadomeArgs {
    pub fn validate(&self) -> Result<(), &'static str> {
        validate_attempts(self.max_attempts)?;
        validate_timeout(self.timeout_ms)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DataDomeOutcome {
    ChallengeCleared,
    NotPresent,
    Blocked,
    IpBanned,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CaptchaDatadomeResult {
    pub outcome: DataDomeOutcome,
    pub tab_id: String,
    pub elapsed_ms: u64,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{input_schema_for_cmd, request_from_cmd, Request};
    use serde_json::json;

    #[test]
    fn defaults_ranges_and_roundtrip_are_canonical() {
        let request = request_from_cmd("captcha_datadome", json!({})).unwrap();
        assert_eq!(
            request.args_json(),
            json!({"max_attempts":1,"timeout_ms":60000})
        );
        let decoded: Request =
            serde_json::from_value(serde_json::to_value(&request).unwrap()).unwrap();
        assert_eq!(decoded.cmd(), "captcha_datadome");
        assert_eq!(decoded.args_json(), request.args_json());
        for input in [
            json!({"max_attempts":0}),
            json!({"max_attempts":5}),
            json!({"timeout_ms":4999}),
            json!({"timeout_ms":120001}),
            json!({"api_key":"not-used"}),
            json!({"max_attempts":null}),
            json!({"max_attempts":-1}),
            json!({"max_attempts":1.5}),
            json!({"timeout_ms":"60000"}),
            json!({"timeout_ms":null}),
        ] {
            assert!(request_from_cmd("captcha_datadome", input).is_err());
        }
        for (input, expected) in [
            (
                json!({"max_attempts":2}),
                json!({"max_attempts":2,"timeout_ms":60000}),
            ),
            (
                json!({"timeout_ms":5000}),
                json!({"max_attempts":1,"timeout_ms":5000}),
            ),
        ] {
            assert_eq!(
                request_from_cmd("captcha_datadome", input)
                    .unwrap()
                    .args_json(),
                expected
            );
        }
        let schema = input_schema_for_cmd("captcha_datadome");
        assert_eq!(
            schema["properties"]["max_attempts"]["default"],
            DATADOME_DEFAULT_ATTEMPTS
        );
        assert_eq!(
            schema["properties"]["timeout_ms"]["default"],
            DATADOME_DEFAULT_TIMEOUT_MS
        );
        assert_eq!(schema["properties"]["max_attempts"]["minimum"], 1);
        assert_eq!(
            schema["properties"]["max_attempts"]["maximum"],
            DATADOME_MAX_ATTEMPTS
        );
        assert_eq!(
            schema["properties"]["timeout_ms"]["minimum"],
            DATADOME_MIN_TIMEOUT_MS
        );
        assert_eq!(
            schema["properties"]["timeout_ms"]["maximum"],
            DATADOME_MAX_TIMEOUT_MS
        );
        for timeout_ms in [DATADOME_MIN_TIMEOUT_MS, DATADOME_MAX_TIMEOUT_MS] {
            let args = CaptchaDatadomeArgs {
                max_attempts: DATADOME_MAX_ATTEMPTS,
                timeout_ms,
            };
            assert!(args.validate().is_ok());
            assert!(
                request_from_cmd("captcha_datadome", serde_json::to_value(args).unwrap()).is_ok()
            );
        }
        assert_eq!(schema["additionalProperties"], false);
        assert!(CaptchaDatadomeArgs {
            max_attempts: 0,
            ..Default::default()
        }
        .validate()
        .is_err());
    }
}