anp 0.7.2

Rust SDK for Agent Network Protocol (ANP)
Documentation
use reqwest::Client;
use serde_json::Value;

use super::errors::{
    HandleGoneError, HandleMovedError, HandleNotFoundError, HandleResolutionError,
};
use super::models::HandleResolutionDocument;
use super::validator::{build_resolution_url, parse_wba_uri, validate_handle};

#[derive(Debug, Clone)]
pub struct ResolveHandleOptions {
    pub timeout_seconds: f64,
    pub verify_ssl: bool,
    pub base_url_override: Option<String>,
}

impl Default for ResolveHandleOptions {
    fn default() -> Self {
        Self {
            timeout_seconds: 10.0,
            verify_ssl: true,
            base_url_override: None,
        }
    }
}

pub async fn resolve_handle(
    handle: &str,
) -> Result<HandleResolutionDocument, HandleResolutionError> {
    resolve_handle_with_options(handle, &ResolveHandleOptions::default()).await
}

pub async fn resolve_handle_with_options(
    handle: &str,
    options: &ResolveHandleOptions,
) -> Result<HandleResolutionDocument, HandleResolutionError> {
    let bare_handle = strip_wba_scheme(handle);
    let (local_part, domain) =
        validate_handle(bare_handle).map_err(|err| HandleResolutionError {
            message: err.message,
            status_code: 400,
        })?;
    let url = options
        .base_url_override
        .as_ref()
        .map(|base| {
            format!(
                "{}/.well-known/handle/{}",
                base.trim_end_matches('/'),
                local_part
            )
        })
        .unwrap_or_else(|| build_resolution_url(&local_part, &domain));
    let normalized = format!("{}.{}", local_part, domain);
    let client = Client::builder()
        .danger_accept_invalid_certs(!options.verify_ssl)
        .timeout(std::time::Duration::from_secs_f64(options.timeout_seconds))
        .build()
        .map_err(|_| HandleResolutionError {
            message: format!("Unexpected error resolving handle '{}'", normalized),
            status_code: 502,
        })?;
    let response = client
        .get(url)
        .header("Accept", "application/json")
        .send()
        .await
        .map_err(|err| HandleResolutionError {
            message: format!("Network error resolving handle '{}': {}", normalized, err),
            status_code: 502,
        })?;

    let status = response.status();
    if status.as_u16() == 301 {
        let redirect_url = response
            .headers()
            .get("Location")
            .and_then(|value| value.to_str().ok())
            .unwrap_or_default()
            .to_string();
        return Err(HandleResolutionError {
            message: HandleMovedError {
                message: format!("Handle '{}' has been migrated", normalized),
                status_code: 301,
                redirect_url,
            }
            .message,
            status_code: 301,
        });
    }
    if status.as_u16() == 404 {
        return Err(HandleResolutionError {
            message: HandleNotFoundError {
                message: format!("Handle '{}' does not exist", normalized),
                status_code: 404,
            }
            .message,
            status_code: 404,
        });
    }
    if status.as_u16() == 410 {
        return Err(HandleResolutionError {
            message: HandleGoneError {
                message: format!("Handle '{}' has been permanently revoked", normalized),
                status_code: 410,
            }
            .message,
            status_code: 410,
        });
    }
    if status.as_u16() != 200 {
        let text = response.text().await.unwrap_or_default();
        return Err(HandleResolutionError {
            message: format!(
                "Unexpected status {} resolving '{}': {}",
                status.as_u16(),
                normalized,
                text
            ),
            status_code: 502,
        });
    }

    let data: Value = response.json().await.map_err(|err| HandleResolutionError {
        message: format!(
            "Unexpected error resolving handle '{}': {}",
            normalized, err
        ),
        status_code: 502,
    })?;
    let document: HandleResolutionDocument =
        serde_json::from_value(data).map_err(|err| HandleResolutionError {
            message: format!(
                "Unexpected error resolving handle '{}': {}",
                normalized, err
            ),
            status_code: 502,
        })?;
    if document.handle.to_ascii_lowercase() != normalized {
        return Err(HandleResolutionError {
            message: format!(
                "Handle mismatch: requested '{}', got '{}'",
                normalized, document.handle
            ),
            status_code: 502,
        });
    }
    Ok(document)
}

pub fn resolve_handle_sync(
    handle: &str,
) -> Result<HandleResolutionDocument, HandleResolutionError> {
    let runtime = tokio::runtime::Runtime::new().map_err(|_| HandleResolutionError {
        message: "Unable to start runtime".to_string(),
        status_code: 502,
    })?;
    runtime.block_on(resolve_handle(handle))
}

pub async fn resolve_handle_from_uri(
    wba_uri: &str,
) -> Result<HandleResolutionDocument, HandleResolutionError> {
    let parsed = parse_wba_uri(wba_uri).map_err(|err| HandleResolutionError {
        message: err.message,
        status_code: 400,
    })?;
    resolve_handle(&parsed.handle).await
}

fn strip_wba_scheme(handle_or_uri: &str) -> &str {
    handle_or_uri
        .strip_prefix("wba://")
        .unwrap_or(handle_or_uri)
}