anp 0.8.9

Rust SDK for Agent Network Protocol (ANP)
Documentation
#[cfg(feature = "network")]
use reqwest::Client;
#[cfg(feature = "network")]
use serde_json::Value;

use super::errors::HandleResolutionError;
#[cfg(feature = "network")]
use super::errors::{HandleGoneError, HandleMovedError, HandleNotFoundError};
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);

    #[cfg(not(feature = "network"))]
    {
        let _ = url;
        return Err(HandleResolutionError {
            message: format!("Network support is disabled for handle '{}'", normalized),
            status_code: 502,
        });
    }

    #[cfg(feature = "network")]
    {
        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 mut 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,
            });
        }
        document.drop_invalid_profile_projection();
        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)
}