use serde::Serialize;
use std::collections::HashMap;
use crate::error::WxPayResult;
use crate::utils::nonce::generate_nonce;
use crate::utils::timestamp::get_timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
}
impl HttpMethod {
pub fn as_str(&self) -> &str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
Self::Patch => "PATCH",
}
}
}
#[derive(Debug, Clone)]
pub struct RequestBuilder {
method: HttpMethod,
path: String,
headers: HashMap<String, String>,
body: Option<String>,
timestamp: Option<i64>,
nonce: Option<String>,
}
impl RequestBuilder {
pub fn new(method: HttpMethod, path: impl Into<String>) -> Self {
Self {
method,
path: path.into(),
headers: HashMap::new(),
body: None,
timestamp: None,
nonce: None,
}
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn json_body<T: Serialize>(self, value: &T) -> WxPayResult<Self> {
let json = serde_json::to_string(value)?;
Ok(self.body(json))
}
pub fn timestamp(mut self, timestamp: i64) -> Self {
self.timestamp = Some(timestamp);
self
}
pub fn nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
pub fn build(self) -> WxPayRequest {
let timestamp = self.timestamp.unwrap_or_else(get_timestamp);
let nonce = self.nonce.unwrap_or_else(generate_nonce);
WxPayRequest {
method: self.method,
path: self.path,
headers: self.headers,
body: self.body,
timestamp,
nonce,
}
}
}
#[derive(Debug, Clone)]
pub struct WxPayRequest {
pub method: HttpMethod,
pub path: String,
pub headers: HashMap<String, String>,
pub body: Option<String>,
pub timestamp: i64,
pub nonce: String,
}
impl WxPayRequest {
pub fn method_str(&self) -> &str {
self.method.as_str()
}
pub fn sign_message(&self) -> String {
let body = self.body.as_deref().unwrap_or("");
use std::fmt::Write;
let method = self.method_str();
let mut s = String::with_capacity(
method.len()
+ self.path.len()
+ self.nonce.len()
+ body.len()
+ 20
+ 5,
);
let _ = write!(
s,
"{}\n{}\n{}\n{}\n{}\n",
method, self.path, self.timestamp, self.nonce, body
);
s
}
pub fn full_url(&self, base_url: &str) -> String {
format!("{}{}", base_url, self.path)
}
pub fn body_str(&self) -> &str {
self.body.as_deref().unwrap_or("")
}
pub fn headers_vec(&self) -> Vec<(String, String)> {
self.headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
}
#[allow(dead_code)]
pub struct WxPayRequestBuilder {
merchant_id: String,
cert_serial_number: String,
base_url: String,
}
impl WxPayRequestBuilder {
pub fn new(
merchant_id: impl Into<String>,
cert_serial_number: impl Into<String>,
base_url: impl Into<String>,
) -> Self {
Self {
merchant_id: merchant_id.into(),
cert_serial_number: cert_serial_number.into(),
base_url: base_url.into(),
}
}
pub fn get(&self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(HttpMethod::Get, path)
}
pub fn post(&self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(HttpMethod::Post, path)
}
pub fn put(&self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(HttpMethod::Put, path)
}
pub fn delete(&self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(HttpMethod::Delete, path)
}
pub fn patch(&self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(HttpMethod::Patch, path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_builder() {
let request = RequestBuilder::new(HttpMethod::Post, "/v3/pay/transactions/jsapi")
.body(r#"{"app_id":"wx88888888"}"#)
.timestamp(1609459200)
.nonce("test_nonce")
.build();
assert_eq!(request.method, HttpMethod::Post);
assert_eq!(request.path, "/v3/pay/transactions/jsapi");
assert_eq!(request.timestamp, 1609459200);
assert_eq!(request.nonce, "test_nonce");
assert!(request.body.is_some());
}
#[test]
fn test_request_sign_message() {
let request = RequestBuilder::new(HttpMethod::Post, "/v3/pay/transactions/jsapi")
.body(r#"{"app_id":"wx88888888"}"#)
.timestamp(1609459200)
.nonce("test_nonce")
.build();
let sign_message = request.sign_message();
assert!(sign_message.starts_with("POST\n"));
assert!(sign_message.contains("/v3/pay/transactions/jsapi"));
assert!(sign_message.contains("1609459200"));
assert!(sign_message.contains("test_nonce"));
assert!(sign_message.ends_with("\n"));
}
#[test]
fn test_request_full_url() {
let request = RequestBuilder::new(HttpMethod::Get, "/v3/pay/transactions/jsapi").build();
let full_url = request.full_url("https://api.mch.weixin.qq.com");
assert_eq!(
full_url,
"https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"
);
}
#[test]
fn test_request_headers() {
let request = RequestBuilder::new(HttpMethod::Post, "/v3/pay/transactions/jsapi")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.build();
let headers = request.headers_vec();
assert_eq!(headers.len(), 2);
}
#[test]
fn test_http_method_as_str() {
assert_eq!(HttpMethod::Get.as_str(), "GET");
assert_eq!(HttpMethod::Post.as_str(), "POST");
assert_eq!(HttpMethod::Put.as_str(), "PUT");
assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
assert_eq!(HttpMethod::Patch.as_str(), "PATCH");
}
#[test]
fn test_wxpay_request_builder() {
let builder =
WxPayRequestBuilder::new("1900000109", "CERT123456", "https://api.mch.weixin.qq.com");
let request = builder.post("/v3/pay/transactions/jsapi").build();
assert_eq!(request.method, HttpMethod::Post);
assert_eq!(request.path, "/v3/pay/transactions/jsapi");
}
}