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

//! NDAX WebSocket message frame types.
//!
//! NDAX uses a specific message frame format where the payload (`o` field)
//! is a stringified JSON that needs to be parsed separately.

use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};

use crate::{Error, Result};

/// NDAX message type enum.
///
/// Indicates the type of message being sent or received.
#[derive(
  Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum NdaxMessageType {
  /// Request message sent to the server
  Request     = 0,
  /// Reply message received from the server
  Reply       = 1,
  /// Subscribe request for events
  Subscribe   = 2,
  /// Event notification from the server
  Event       = 3,
  /// Unsubscribe request to stop receiving events
  Unsubscribe = 4,
  /// Error response from the server
  Error       = 5,
}

/// NDAX message frame wrapper.
///
/// All NDAX WebSocket messages are wrapped in this frame structure.
/// The `o` field contains a JSON string that needs to be parsed separately.
///
/// # Example
///
/// ```
/// use ndaxrs::messages::{NdaxFrame, NdaxMessageType};
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct Payload {
///   value: i32,
/// }
///
/// let frame = NdaxFrame::request(
///   1,
///   "GetInstruments",
///   &Payload {
///     value: 42
///   },
/// )
/// .unwrap();
/// assert_eq!(frame.message_type(), NdaxMessageType::Request);
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NdaxFrame {
  /// Message type: 0=Request, 1=Reply, 2=Subscribe, 3=Event, 4=Unsubscribe,
  /// 5=Error
  pub m: u8,
  /// Sequence number / request ID for matching requests to replies
  pub i: u64,
  /// Function name (e.g., "SubscribeLevel2", "Level2UpdateEvent",
  /// "AuthenticateUser")
  pub n: String,
  /// Payload as a JSON string (requires double-parsing)
  pub o: String,
}

impl NdaxFrame {
  /// Create a new request frame (m=0).
  ///
  /// # Arguments
  ///
  /// * `id` - Sequence number for request/response matching
  /// * `name` - Function name (e.g., "GetInstruments")
  /// * `payload` - Request payload to serialize
  ///
  /// # Errors
  ///
  /// Returns an error if the payload cannot be serialized to JSON.
  pub fn request(
    id: u64,
    name: &str,
    payload: &impl Serialize,
  ) -> Result<Self> {
    Ok(Self {
      m: NdaxMessageType::Request as u8,
      i: id,
      n: name.to_string(),
      o: serde_json::to_string(payload)?,
    })
  }

  /// Create a new request frame with a pre-serialized payload (m=0).
  ///
  /// # Arguments
  ///
  /// * `id` - Sequence number for request/response matching
  /// * `name` - Function name (e.g., "GetInstruments")
  /// * `payload_json` - Pre-serialized JSON payload string
  pub fn request_raw(id: u64, name: &str, payload_json: &str) -> Self {
    Self {
      m: NdaxMessageType::Request as u8,
      i: id,
      n: name.to_string(),
      o: payload_json.to_string(),
    }
  }

  /// Create a new subscribe frame (m=2).
  ///
  /// # Arguments
  ///
  /// * `id` - Sequence number for request/response matching
  /// * `name` - Subscription function name (e.g., "SubscribeLevel2")
  /// * `payload` - Subscription payload to serialize
  ///
  /// # Errors
  ///
  /// Returns an error if the payload cannot be serialized to JSON.
  pub fn subscribe(
    id: u64,
    name: &str,
    payload: &impl Serialize,
  ) -> Result<Self> {
    Ok(Self {
      m: NdaxMessageType::Subscribe as u8,
      i: id,
      n: name.to_string(),
      o: serde_json::to_string(payload)?,
    })
  }

  /// Parse the payload field as a specific type.
  ///
  /// The `o` field contains a JSON string that must be deserialized
  /// into the target type.
  ///
  /// # Type Parameters
  ///
  /// * `T` - The type to deserialize the payload into
  ///
  /// # Errors
  ///
  /// Returns an error if the payload cannot be deserialized.
  pub fn parse_payload<T: DeserializeOwned>(&self) -> Result<T> {
    serde_json::from_str(&self.o).map_err(Error::from)
  }

  /// Get the message type as an enum.
  pub fn message_type(&self) -> NdaxMessageType {
    match self.m {
      0 => NdaxMessageType::Request,
      1 => NdaxMessageType::Reply,
      2 => NdaxMessageType::Subscribe,
      3 => NdaxMessageType::Event,
      4 => NdaxMessageType::Unsubscribe,
      _ => NdaxMessageType::Error,
    }
  }

  /// Check if this is a reply message (m=1).
  pub fn is_reply(&self) -> bool { self.m == NdaxMessageType::Reply as u8 }

  /// Check if this is an event message (m=3).
  pub fn is_event(&self) -> bool { self.m == NdaxMessageType::Event as u8 }

  /// Check if this is an error message (m=5).
  pub fn is_error(&self) -> bool { self.m == NdaxMessageType::Error as u8 }
}

#[cfg(test)]
mod tests {
  use serde::Deserialize;
  use serde_json::json;

  use super::*;

  #[derive(Serialize, Deserialize, Debug, PartialEq)]
  struct TestPayload {
    value: i32,
    name:  String,
  }

  #[test]
  fn test_frame_serialize_request() {
    let payload = TestPayload {
      value: 42,
      name:  "test".to_string(),
    };
    let frame = NdaxFrame::request(1, "TestFunc", &payload).unwrap();

    let json = serde_json::to_value(&frame).unwrap();
    assert_eq!(json["m"], 0);
    assert_eq!(json["i"], 1);
    assert_eq!(json["n"], "TestFunc");
  }

  #[test]
  fn test_frame_deserialize_reply() {
    let json_str = r#"{"m":1,"i":5,"n":"GetInstruments","o":"{\"value\":123,\"name\":\"test\"}"}"#;
    let frame: NdaxFrame = serde_json::from_str(json_str).unwrap();

    assert_eq!(frame.m, 1);
    assert_eq!(frame.i, 5);
    assert_eq!(frame.n, "GetInstruments");
    assert!(frame.is_reply());
  }

  #[test]
  fn test_frame_parse_payload() {
    let inner = TestPayload {
      value: 99,
      name:  "parsed".to_string(),
    };
    let frame = NdaxFrame {
      m: 1,
      i: 1,
      n: "Test".to_string(),
      o: serde_json::to_string(&inner).unwrap(),
    };

    let parsed: TestPayload = frame.parse_payload().unwrap();
    assert_eq!(parsed, inner);
  }

  #[test]
  fn test_message_type_values() {
    assert_eq!(NdaxMessageType::Request as u8, 0);
    assert_eq!(NdaxMessageType::Reply as u8, 1);
    assert_eq!(NdaxMessageType::Subscribe as u8, 2);
    assert_eq!(NdaxMessageType::Event as u8, 3);
    assert_eq!(NdaxMessageType::Unsubscribe as u8, 4);
    assert_eq!(NdaxMessageType::Error as u8, 5);
  }

  #[test]
  fn test_message_type_serialization() {
    let msg_type = NdaxMessageType::Reply;
    let serialized = serde_json::to_string(&msg_type).unwrap();
    assert_eq!(serialized, "1");

    let deserialized: NdaxMessageType = serde_json::from_str("3").unwrap();
    assert_eq!(deserialized, NdaxMessageType::Event);
  }

  #[test]
  fn test_frame_helper_methods() {
    let reply_frame = NdaxFrame {
      m: 1,
      i: 1,
      n: "Test".to_string(),
      o: "{}".to_string(),
    };
    assert!(reply_frame.is_reply());
    assert!(!reply_frame.is_event());
    assert!(!reply_frame.is_error());
    assert_eq!(reply_frame.message_type(), NdaxMessageType::Reply);

    let event_frame = NdaxFrame {
      m: 3,
      i: 0,
      n: "Event".to_string(),
      o: "{}".to_string(),
    };
    assert!(!event_frame.is_reply());
    assert!(event_frame.is_event());
    assert!(!event_frame.is_error());
    assert_eq!(event_frame.message_type(), NdaxMessageType::Event);

    let error_frame = NdaxFrame {
      m: 5,
      i: 1,
      n: "Error".to_string(),
      o: "{}".to_string(),
    };
    assert!(!error_frame.is_reply());
    assert!(!error_frame.is_event());
    assert!(error_frame.is_error());
    assert_eq!(error_frame.message_type(), NdaxMessageType::Error);
  }

  #[test]
  fn test_subscribe_frame() {
    let payload = json!({"OMSId": 1, "InstrumentId": 1});
    let frame = NdaxFrame::subscribe(10, "SubscribeLevel2", &payload).unwrap();

    assert_eq!(frame.m, 2);
    assert_eq!(frame.i, 10);
    assert_eq!(frame.n, "SubscribeLevel2");
    assert_eq!(frame.message_type(), NdaxMessageType::Subscribe);
  }
}