enigma-node-client 0.0.1

HTTP client for Enigma node services (registry/resolve/announce/sync/nodes) using canonical enigma-node-types.
Documentation
use std::time::Duration;

use enigma_node_types::{CheckUserResponse, NodesPayload, Presence, RegisterRequest, RegisterResponse, ResolveResponse, SyncRequest, SyncResponse, UserId};
use reqwest::Response;
use serde::de::DeserializeOwned;
use serde::Serialize;

use crate::config::NodeClientConfig;
use crate::error::{EnigmaNodeClientError, Result};
use crate::urls;

pub struct NodeClient {
    base_url: String,
    http: reqwest::Client,
    cfg: NodeClientConfig,
}

impl NodeClient {
    pub fn new(base_url: impl Into<String>, cfg: NodeClientConfig) -> Result<NodeClient> {
        if cfg.timeout_ms == 0 {
            return Err(EnigmaNodeClientError::InvalidInput("timeout_ms"));
        }
        if cfg.connect_timeout_ms == 0 {
            return Err(EnigmaNodeClientError::InvalidInput("connect_timeout_ms"));
        }
        if cfg.max_response_bytes == 0 {
            return Err(EnigmaNodeClientError::InvalidInput("max_response_bytes"));
        }
        if cfg.user_agent.trim().is_empty() {
            return Err(EnigmaNodeClientError::InvalidInput("user_agent"));
        }
        let base_raw: String = base_url.into();
        let base = urls::validated_base(base_raw.as_str())?;
        let http = reqwest::Client::builder()
            .user_agent(cfg.user_agent.clone())
            .timeout(Duration::from_millis(cfg.timeout_ms))
            .connect_timeout(Duration::from_millis(cfg.connect_timeout_ms))
            .build()?;
        Ok(NodeClient {
            base_url: base,
            http,
            cfg,
        })
    }

    pub fn base_url(&self) -> &str {
        &self.base_url
    }

    pub async fn register(&self, req: RegisterRequest) -> Result<RegisterResponse> {
        req.identity
            .validate()
            .map_err(|_| EnigmaNodeClientError::InvalidInput("identity"))?;
        let url = urls::register(&self.base_url)?;
        self.post_json(url, req).await
    }

    pub async fn resolve(&self, user_id_hex: &str) -> Result<ResolveResponse> {
        let validated_hex = validate_user_id_hex(user_id_hex)?;
        let url = urls::resolve(&self.base_url, &validated_hex)?;
        self.get_json(url).await
    }

    pub async fn check_user(&self, user_id_hex: &str) -> Result<CheckUserResponse> {
        let validated_hex = validate_user_id_hex(user_id_hex)?;
        let url = urls::check_user(&self.base_url, &validated_hex)?;
        self.get_json(url).await
    }

    pub async fn announce(&self, presence: Presence) -> Result<serde_json::Value> {
        presence
            .validate()
            .map_err(|_| EnigmaNodeClientError::InvalidInput("presence"))?;
        let url = urls::announce(&self.base_url)?;
        self.post_value(url, presence).await
    }

    pub async fn sync(&self, req: SyncRequest) -> Result<SyncResponse> {
        for identity in &req.identities {
            identity
                .validate()
                .map_err(|_| EnigmaNodeClientError::InvalidInput("identities"))?;
        }
        let url = urls::sync(&self.base_url)?;
        self.post_json(url, req).await
    }

    pub async fn nodes(&self) -> Result<NodesPayload> {
        let url = urls::nodes_get(&self.base_url)?;
        self.get_json(url).await
    }

    pub async fn add_nodes(&self, payload: NodesPayload) -> Result<serde_json::Value> {
        payload
            .validate()
            .map_err(|_| EnigmaNodeClientError::InvalidInput("nodes"))?;
        let url = urls::nodes_post(&self.base_url)?;
        self.post_value(url, payload).await
    }

    async fn get_json<T: DeserializeOwned>(&self, url: String) -> Result<T> {
        let resp = self.http.get(url).send().await?;
        self.handle_json_response(resp).await
    }

    async fn post_json<TReq: Serialize, TResp: DeserializeOwned>(
        &self,
        url: String,
        payload: TReq,
    ) -> Result<TResp> {
        let resp = self.http.post(url).json(&payload).send().await?;
        self.handle_json_response(resp).await
    }

    async fn post_value<TReq: Serialize>(&self, url: String, payload: TReq) -> Result<serde_json::Value> {
        let resp = self.http.post(url).json(&payload).send().await?;
        self.handle_value_response(resp).await
    }

    async fn handle_json_response<T: DeserializeOwned>(&self, resp: Response) -> Result<T> {
        let status = resp.status();
        if !status.is_success() {
            return Err(EnigmaNodeClientError::Status(status.as_u16()));
        }
        let body = resp.bytes().await?;
        if body.len() > self.cfg.max_response_bytes {
            return Err(EnigmaNodeClientError::ResponseTooLarge);
        }
        Ok(serde_json::from_slice(&body)?)
    }

    async fn handle_value_response(&self, resp: Response) -> Result<serde_json::Value> {
        let status = resp.status();
        if !status.is_success() {
            return Err(EnigmaNodeClientError::Status(status.as_u16()));
        }
        let body = resp.bytes().await?;
        if body.len() > self.cfg.max_response_bytes {
            return Err(EnigmaNodeClientError::ResponseTooLarge);
        }
        Ok(serde_json::from_slice(&body)?)
    }
}

fn validate_user_id_hex(user_id_hex: &str) -> Result<String> {
    let trimmed = user_id_hex.trim();
    if trimmed.len() != 64 {
        return Err(EnigmaNodeClientError::InvalidUserIdHex);
    }
    UserId::from_hex(trimmed)
        .map(|id| id.to_hex())
        .map_err(|_| EnigmaNodeClientError::InvalidUserIdHex)
}