use super::Bitget;
use ccxt_core::{Error, ParseError, Result};
use reqwest::header::HeaderMap;
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HttpMethod {
#[default]
Get,
Post,
Delete,
}
pub struct BitgetSignedRequestBuilder<'a> {
bitget: &'a Bitget,
params: BTreeMap<String, String>,
body: Option<Value>,
endpoint: String,
method: HttpMethod,
}
impl<'a> BitgetSignedRequestBuilder<'a> {
pub fn new(bitget: &'a Bitget, endpoint: impl Into<String>) -> Self {
Self {
bitget,
params: BTreeMap::new(),
body: None,
endpoint: endpoint.into(),
method: HttpMethod::default(),
}
}
pub fn method(mut self, method: HttpMethod) -> Self {
self.method = method;
self
}
pub fn param(mut self, key: impl Into<String>, value: impl ToString) -> Self {
self.params.insert(key.into(), value.to_string());
self
}
pub fn optional_param<T: ToString>(mut self, key: impl Into<String>, value: Option<T>) -> Self {
if let Some(v) = value {
self.params.insert(key.into(), v.to_string());
}
self
}
pub fn params(mut self, params: BTreeMap<String, String>) -> Self {
self.params.extend(params);
self
}
pub fn body(mut self, body: Value) -> Self {
self.body = Some(body);
self
}
pub async fn execute(self) -> Result<Value> {
self.bitget.check_required_credentials()?;
let timestamp = (chrono::Utc::now().timestamp_millis()).to_string();
let auth = self.bitget.get_auth()?;
let body_str = if let Some(ref body) = self.body {
body.to_string()
} else if self.method == HttpMethod::Get {
String::new()
} else {
String::new()
};
let path = if self.method == HttpMethod::Get && !self.params.is_empty() {
format!("{}?{}", self.endpoint, build_query_string(&self.params))
} else {
self.endpoint.clone()
};
let signature = auth.sign(×tamp, &method_to_string(self.method), &path, &body_str);
let mut headers = HeaderMap::new();
auth.add_auth_headers(&mut headers, ×tamp, &signature);
let urls = self.bitget.urls();
let full_url = format!("{}{}", urls.rest, self.endpoint);
match self.method {
HttpMethod::Get => {
let query_string = build_query_string(&self.params);
let url = if query_string.is_empty() {
full_url
} else {
format!("{}?{}", full_url, query_string)
};
self.bitget
.base()
.http_client
.get(&url, Some(headers))
.await
}
HttpMethod::Post => {
let body = if let Some(b) = self.body {
b
} else {
serde_json::to_value(&self.params).map_err(|e| {
Error::from(ParseError::invalid_format(
"data",
format!("Failed to serialize request body: {}", e),
))
})?
};
self.bitget
.base()
.http_client
.post(&full_url, Some(headers), Some(body))
.await
}
HttpMethod::Delete => {
let body = if let Some(b) = self.body {
b
} else {
serde_json::to_value(&self.params).map_err(|e| {
Error::from(ParseError::invalid_format(
"data",
format!("Failed to serialize request body: {}", e),
))
})?
};
self.bitget
.base()
.http_client
.delete(&full_url, Some(headers), Some(body))
.await
}
}
}
}
fn method_to_string(method: HttpMethod) -> String {
match method {
HttpMethod::Get => "GET".to_string(),
HttpMethod::Post => "POST".to_string(),
HttpMethod::Delete => "DELETE".to_string(),
}
}
fn build_query_string(params: &BTreeMap<String, String>) -> String {
params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&")
}
#[cfg(test)]
mod tests {
#![allow(clippy::disallowed_methods)]
use super::*;
use ccxt_core::ExchangeConfig;
#[test]
fn test_http_method_default() {
assert_eq!(HttpMethod::default(), HttpMethod::Get);
}
#[test]
fn test_builder_construction() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let builder = BitgetSignedRequestBuilder::new(&bitget, "/api/v2/spot/account/assets");
assert_eq!(builder.endpoint, "/api/v2/spot/account/assets");
assert_eq!(builder.method, HttpMethod::Get);
assert!(builder.params.is_empty());
assert!(builder.body.is_none());
}
#[test]
fn test_builder_method_chaining() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let builder = BitgetSignedRequestBuilder::new(&bitget, "/api/v2/spot/trade/place-order")
.method(HttpMethod::Post)
.param("symbol", "BTCUSDT")
.param("side", "buy");
assert_eq!(builder.method, HttpMethod::Post);
assert_eq!(builder.params.get("symbol"), Some(&"BTCUSDT".to_string()));
assert_eq!(builder.params.get("side"), Some(&"buy".to_string()));
}
#[test]
fn test_builder_param() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
.param("string_param", "value")
.param("int_param", 123)
.param("float_param", 45.67);
assert_eq!(
builder.params.get("string_param"),
Some(&"value".to_string())
);
assert_eq!(builder.params.get("int_param"), Some(&"123".to_string()));
assert_eq!(
builder.params.get("float_param"),
Some(&"45.67".to_string())
);
}
#[test]
fn test_builder_optional_param_some() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
.optional_param("limit", Some(100u32))
.optional_param("after", Some(1234567890i64));
assert_eq!(builder.params.get("limit"), Some(&"100".to_string()));
assert_eq!(builder.params.get("after"), Some(&"1234567890".to_string()));
}
#[test]
fn test_builder_optional_param_none() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let none_value: Option<u32> = None;
let builder =
BitgetSignedRequestBuilder::new(&bitget, "/test").optional_param("limit", none_value);
assert!(builder.params.get("limit").is_none());
}
#[test]
fn test_builder_params_bulk() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let mut params = BTreeMap::new();
params.insert("symbol".to_string(), "BTCUSDT".to_string());
params.insert("side".to_string(), "buy".to_string());
let builder = BitgetSignedRequestBuilder::new(&bitget, "/test").params(params);
assert_eq!(builder.params.get("symbol"), Some(&"BTCUSDT".to_string()));
assert_eq!(builder.params.get("side"), Some(&"buy".to_string()));
}
#[test]
fn test_builder_body() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let body = serde_json::json!({
"symbol": "BTCUSDT",
"side": "buy"
});
let builder = BitgetSignedRequestBuilder::new(&bitget, "/test").body(body.clone());
assert_eq!(builder.body, Some(body));
}
#[test]
fn test_builder_all_http_methods() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let get_builder = BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Get);
assert_eq!(get_builder.method, HttpMethod::Get);
let post_builder =
BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Post);
assert_eq!(post_builder.method, HttpMethod::Post);
let delete_builder =
BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Delete);
assert_eq!(delete_builder.method, HttpMethod::Delete);
}
#[test]
fn test_builder_parameter_ordering() {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
.param("zebra", "z")
.param("apple", "a")
.param("mango", "m");
let keys: Vec<_> = builder.params.keys().collect();
assert_eq!(keys, vec!["apple", "mango", "zebra"]);
}
#[test]
fn test_method_to_string() {
assert_eq!(method_to_string(HttpMethod::Get), "GET");
assert_eq!(method_to_string(HttpMethod::Post), "POST");
assert_eq!(method_to_string(HttpMethod::Delete), "DELETE");
}
#[test]
fn test_build_query_string() {
let mut params = BTreeMap::new();
params.insert("symbol".to_string(), "BTCUSDT".to_string());
params.insert("side".to_string(), "buy".to_string());
params.insert("amount".to_string(), "1.5".to_string());
let query = build_query_string(¶ms);
assert_eq!(query, "amount=1.5&side=buy&symbol=BTCUSDT");
}
#[test]
fn test_build_query_string_empty() {
let params = BTreeMap::new();
let query = build_query_string(¶ms);
assert!(query.is_empty());
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use ccxt_core::ExchangeConfig;
use proptest::prelude::*;
fn param_key_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9]{0,19}".prop_map(|s| s)
}
fn param_value_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z0-9._-]{1,50}".prop_map(|s| s)
}
fn params_strategy() -> impl Strategy<Value = BTreeMap<String, String>> {
proptest::collection::btree_map(param_key_strategy(), param_value_strategy(), 0..10)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_fluent_api_method_chaining(
params in params_strategy(),
other_key in param_key_strategy(),
other_value in param_value_strategy()
) {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
.method(HttpMethod::Post)
.params(params.clone())
.param(&other_key, &other_value)
.optional_param("optional", Some("value"));
for (key, value) in ¶ms {
prop_assert_eq!(builder.params.get(key), Some(value));
}
prop_assert_eq!(builder.params.get(&other_key), Some(&other_value));
prop_assert_eq!(builder.params.get("optional"), Some(&"value".to_string()));
prop_assert_eq!(builder.method, HttpMethod::Post);
}
#[test]
fn prop_optional_param_conditional_addition(
key in param_key_strategy(),
value in proptest::option::of(param_value_strategy()),
other_key in param_key_strategy(),
other_value in param_value_strategy()
) {
let config = ExchangeConfig::default();
let bitget = Bitget::new(config).unwrap();
let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
.param(&other_key, &other_value)
.optional_param(&key, value.clone());
match value {
Some(v) => {
prop_assert!(
builder.params.contains_key(&key),
"Parameter {} should be present when value is Some",
key
);
prop_assert_eq!(
builder.params.get(&key).unwrap(),
&v,
"Parameter {} should have correct value",
key
);
}
None => {
if key != other_key {
prop_assert!(
!builder.params.contains_key(&key),
"Parameter {} should NOT be present when value is None",
key
);
}
}
}
prop_assert!(
builder.params.contains_key(&other_key),
"Other parameter {} should always be present",
other_key
);
prop_assert_eq!(
builder.params.get(&other_key).unwrap(),
&other_value,
"Other parameter {} should have correct value",
other_key
);
}
}
}