ndaxrs 0.1.0

Rust client library for the NDAX cryptocurrency exchange API
Documentation
// Copyright (C) 2026 ndaxrs Art Morozov
// SPDX-License-Identifier: GPL-3.0-only

//! Configuration types for the ndaxrs crate.

use std::{path::Path, time::Duration};

use crate::{Error, Result};

/// Default WebSocket URL for the NDAX API.
pub const DEFAULT_WS_URL: &str = "wss://api.ndax.io:8443/WSGateway/";

/// Default timeout for API requests.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);

/// Credentials for authenticating with the NDAX API.
#[derive(Clone, Debug, Default)]
pub struct NdaxCredentials {
  /// The API key.
  pub api_key:    String,
  /// The API secret.
  pub api_secret: String,
  /// The user ID.
  pub user_id:    String,
}

impl NdaxCredentials {
  /// Create new credentials with the given values.
  pub fn new(
    api_key: impl Into<String>,
    api_secret: impl Into<String>,
    user_id: impl Into<String>,
  ) -> Self {
    Self {
      api_key:    api_key.into(),
      api_secret: api_secret.into(),
      user_id:    user_id.into(),
    }
  }

  /// Load credentials from environment variables.
  ///
  /// Looks for `NDAX_API_KEY`, `NDAX_API_SECRET`, and `NDAX_USER_ID`.
  /// Automatically loads `.env` file in current directory if present.
  pub fn from_env() -> Result<Self> {
    // Silently ignore if .env doesn't exist
    dotenvy::dotenv().ok();
    Self::read_from_env()
  }

  /// Load credentials from a specific `.env` file, then read env vars.
  pub fn from_env_file<P: AsRef<Path>>(path: P) -> Result<Self> {
    dotenvy::from_path(path.as_ref())
      .map_err(|e| Error::Env(format!("Failed to load env file: {}", e)))?;
    Self::read_from_env()
  }

  fn read_from_env() -> Result<Self> {
    let api_key = std::env::var("NDAX_API_KEY")
      .map_err(|_| Error::Env("NDAX_API_KEY not set".to_string()))?;
    let api_secret = std::env::var("NDAX_API_SECRET")
      .map_err(|_| Error::Env("NDAX_API_SECRET not set".to_string()))?;
    let user_id = std::env::var("NDAX_USER_ID")
      .map_err(|_| Error::Env("NDAX_USER_ID not set".to_string()))?;

    Ok(Self {
      api_key,
      api_secret,
      user_id,
    })
  }
}

/// Configuration for the NDAX client.
#[derive(Clone, Debug)]
pub struct NdaxConfig {
  /// Optional credentials for authenticated API calls.
  pub credentials: Option<NdaxCredentials>,
  /// Timeout for API requests.
  pub timeout:     Duration,
  /// WebSocket URL.
  pub ws_url:      String,
}

impl Default for NdaxConfig {
  fn default() -> Self {
    Self {
      credentials: None,
      timeout:     DEFAULT_TIMEOUT,
      ws_url:      DEFAULT_WS_URL.to_string(),
    }
  }
}

impl NdaxConfig {
  /// Create a new builder for `NdaxConfig`.
  pub fn builder() -> NdaxConfigBuilder { NdaxConfigBuilder::default() }
}

/// Builder for `NdaxConfig`.
#[derive(Clone, Debug, Default)]
pub struct NdaxConfigBuilder {
  credentials: Option<NdaxCredentials>,
  timeout:     Option<Duration>,
  ws_url:      Option<String>,
}

impl NdaxConfigBuilder {
  /// Create a new builder.
  pub fn new() -> Self { Self::default() }

  /// Set the credentials for authenticated API calls.
  pub fn credentials(mut self, credentials: NdaxCredentials) -> Self {
    self.credentials = Some(credentials);
    self
  }

  /// Set the timeout for API requests.
  pub fn timeout(mut self, timeout: Duration) -> Self {
    self.timeout = Some(timeout);
    self
  }

  /// Set the WebSocket URL.
  pub fn ws_url(mut self, ws_url: impl Into<String>) -> Self {
    self.ws_url = Some(ws_url.into());
    self
  }

  /// Build the `NdaxConfig`.
  pub fn build(self) -> Result<NdaxConfig> {
    Ok(NdaxConfig {
      credentials: self.credentials,
      timeout:     self.timeout.unwrap_or(DEFAULT_TIMEOUT),
      ws_url:      self.ws_url.unwrap_or_else(|| DEFAULT_WS_URL.to_string()),
    })
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_credentials_new() {
    let creds = NdaxCredentials::new("key", "secret", "123");
    assert_eq!(creds.api_key, "key");
    assert_eq!(creds.api_secret, "secret");
    assert_eq!(creds.user_id, "123");
  }

  #[test]
  fn test_config_default() {
    let config = NdaxConfig::default();
    assert!(config.credentials.is_none());
    assert_eq!(config.timeout, DEFAULT_TIMEOUT);
    assert_eq!(config.ws_url, DEFAULT_WS_URL);
  }

  #[test]
  fn test_config_builder() {
    let creds = NdaxCredentials::new("key", "secret", "123");
    let config = NdaxConfig::builder()
      .credentials(creds.clone())
      .timeout(Duration::from_secs(60))
      .ws_url("wss://custom.url/")
      .build()
      .unwrap();

    assert!(config.credentials.is_some());
    let config_creds = config.credentials.unwrap();
    assert_eq!(config_creds.api_key, "key");
    assert_eq!(config.timeout, Duration::from_secs(60));
    assert_eq!(config.ws_url, "wss://custom.url/");
  }

  #[test]
  fn test_config_builder_defaults() {
    let config = NdaxConfig::builder().build().unwrap();
    assert!(config.credentials.is_none());
    assert_eq!(config.timeout, DEFAULT_TIMEOUT);
    assert_eq!(config.ws_url, DEFAULT_WS_URL);
  }

  #[test]
  fn test_env_loading_missing_vars() {
    // Clear any existing env vars
    std::env::remove_var("NDAX_API_KEY");
    std::env::remove_var("NDAX_API_SECRET");
    std::env::remove_var("NDAX_USER_ID");

    let result = NdaxCredentials::read_from_env();
    assert!(result.is_err());
  }

  #[test]
  fn test_env_loading_with_vars() {
    std::env::set_var("NDAX_API_KEY", "test_key");
    std::env::set_var("NDAX_API_SECRET", "test_secret");
    std::env::set_var("NDAX_USER_ID", "test_user");

    let result = NdaxCredentials::read_from_env();
    assert!(result.is_ok());
    let creds = result.unwrap();
    assert_eq!(creds.api_key, "test_key");
    assert_eq!(creds.api_secret, "test_secret");
    assert_eq!(creds.user_id, "test_user");

    // Clean up
    std::env::remove_var("NDAX_API_KEY");
    std::env::remove_var("NDAX_API_SECRET");
    std::env::remove_var("NDAX_USER_ID");
  }
}