use std::collections::HashMap;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::order::OrderStatus;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OrderResponse {
pub order_id: String,
pub filled_price: Option<Decimal>,
pub filled_size: Decimal,
pub requested_size: Decimal,
pub status: OrderStatus,
}
impl OrderResponse {
pub fn new(
order_id: String,
filled_price: Option<Decimal>,
filled_size: Decimal,
requested_size: Decimal,
status: OrderStatus,
) -> Self {
Self {
order_id,
filled_price,
filled_size,
requested_size,
status,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HlActionResponse {
pub status: String,
#[serde(default)]
pub response: Option<serde_json::Value>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl HlActionResponse {
pub fn is_ok(&self) -> bool {
self.status == "ok"
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn order_response_serde_roundtrip() {
let resp = OrderResponse {
order_id: "abc123".into(),
filled_price: Some(Decimal::from_str("50000.0").unwrap()),
filled_size: Decimal::from_str("0.1").unwrap(),
requested_size: Decimal::from_str("0.1").unwrap(),
status: OrderStatus::Filled,
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: OrderResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.order_id, "abc123");
assert_eq!(
parsed.filled_price,
Some(Decimal::from_str("50000.0").unwrap())
);
assert_eq!(parsed.filled_size, Decimal::from_str("0.1").unwrap());
assert_eq!(parsed.requested_size, Decimal::from_str("0.1").unwrap());
assert_eq!(parsed.status, OrderStatus::Filled);
}
#[test]
fn order_response_no_fill_price_roundtrip() {
let resp = OrderResponse {
order_id: "xyz".into(),
filled_price: None,
filled_size: Decimal::ZERO,
requested_size: Decimal::ONE,
status: OrderStatus::Open,
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: OrderResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.filled_price.is_none());
assert_eq!(parsed.status, OrderStatus::Open);
}
#[test]
fn order_response_camel_case_keys() {
let resp = OrderResponse {
order_id: "x".into(),
filled_price: None,
filled_size: Decimal::ZERO,
requested_size: Decimal::ZERO,
status: OrderStatus::Open,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("orderId"));
assert!(json.contains("filledPrice"));
assert!(json.contains("filledSize"));
assert!(json.contains("requestedSize"));
}
#[test]
fn action_response_ok_roundtrip() {
let json = serde_json::json!({
"status": "ok",
"response": {"type": "cancel", "data": {"statuses": ["success"]}}
});
let parsed: HlActionResponse = serde_json::from_value(json).unwrap();
assert_eq!(parsed.status, "ok");
assert!(parsed.is_ok());
assert!(parsed.response.is_some());
}
#[test]
fn action_response_error() {
let json = serde_json::json!({
"status": "err",
"response": "Order not found"
});
let parsed: HlActionResponse = serde_json::from_value(json).unwrap();
assert_eq!(parsed.status, "err");
assert!(!parsed.is_ok());
}
#[test]
fn action_response_no_response_field() {
let json = serde_json::json!({ "status": "ok" });
let parsed: HlActionResponse = serde_json::from_value(json).unwrap();
assert!(parsed.is_ok());
assert!(parsed.response.is_none());
}
#[test]
fn action_response_extra_fields_captured() {
let json = serde_json::json!({
"status": "ok",
"timestamp": 1700000000
});
let parsed: HlActionResponse = serde_json::from_value(json).unwrap();
assert!(parsed.extra.contains_key("timestamp"));
}
#[test]
fn action_response_serde_roundtrip() {
let resp = HlActionResponse {
status: "ok".into(),
response: Some(serde_json::json!({"data": "test"})),
extra: HashMap::new(),
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: HlActionResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.status, "ok");
assert!(parsed.response.is_some());
}
}