use rmcp::ErrorData;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FpSource {
Generative,
Pool,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct GenerateInput {
pub source: FpSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct GenerateOutput {
pub persona: serde_json::Value,
}
const POOL_URL: &str =
"https://github.com/TurtIeSocks/zendriver-rs/releases/latest/download/fingerprint-pool.json";
fn network_url() -> String {
std::env::var("ZENDRIVER_FP_NETWORK_URL")
.unwrap_or_else(|_| zendriver_fingerprints::generative::DEFAULT_NETWORK_URL.to_string())
}
pub async fn generate(input: GenerateInput) -> Result<GenerateOutput, ErrorData> {
generate_from(input, &network_url()).await
}
async fn generate_from(
input: GenerateInput,
network_url: &str,
) -> Result<GenerateOutput, ErrorData> {
use zendriver::Seed;
let seed = input.seed.map_or_else(Seed::random, Seed::from_u64);
let persona = match input.source {
FpSource::Generative => {
zendriver_fingerprints::generative::Generator::load_or_download(network_url)
.await
.map_err(|e| {
ErrorData::internal_error(format!("generative network load failed: {e}"), None)
})?
.generate(seed)
}
FpSource::Pool => {
let set = zendriver_fingerprints::pool::load_or_download(POOL_URL)
.await
.map_err(|e| {
ErrorData::internal_error(
format!(
"pool load failed (the pool asset may not be published yet — see issue #25): {e}"
),
None,
)
})?;
set.sample(seed)
}
};
let value = serde_json::to_value(&persona)
.map_err(|e| ErrorData::internal_error(format!("persona serialize: {e}"), None))?;
Ok(GenerateOutput { persona: value })
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn generative_via_mock_is_non_null_and_deterministic() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(zendriver_fingerprints::generative::TEST_NETWORK_ZIP),
)
.mount(&server)
.await;
let a = generate_from(
GenerateInput {
source: FpSource::Generative,
seed: Some(7),
},
&server.uri(),
)
.await
.expect("a");
assert!(a.persona.is_object());
let b = generate_from(
GenerateInput {
source: FpSource::Generative,
seed: Some(7),
},
&server.uri(),
)
.await
.expect("b");
assert_eq!(a.persona, b.persona);
}
}