feat: add Mihomo API modules and manager (#2869)

• Introduce new API caller implementations for Mihomo in model and module layers.
• Add configuration and API integration files under /src-tauri/src/config/api and /src-tauri/src/model/api.
• Implement a singleton MihomoAPICaller with async API call support and integration tests.
• Create a new MihomoManager module to refresh and fetch proxies from the API.
• Update Cargo.lock and Cargo.toml with additional dependencies (async-trait, env_logger, mockito, tempfile, etc.) related to the Mihomo API support.
This commit is contained in:
Tunglies
2025-03-03 19:31:44 +08:00
committed by GitHub
parent 3e53ea7209
commit 3b69465016
15 changed files with 505 additions and 4 deletions

View File

@@ -0,0 +1,70 @@
use crate::model::api::common::ApiCaller;
use async_trait::async_trait;
use reqwest::{
header::{HeaderMap, HeaderName, HeaderValue},
RequestBuilder,
};
use serde::de::DeserializeOwned;
impl<'a> ApiCaller<'a> {
pub async fn send_request(
&self,
method: &str,
path: &str,
body: Option<&str>,
headers: Option<Vec<(&str, &str)>>,
) -> Result<String, String> {
let full_url = format!("{}{}", self.url, path); // 拼接完整 URL
let mut request: RequestBuilder = match method {
"GET" => self.client.get(&full_url),
"POST" => self
.client
.post(&full_url)
.body(body.unwrap_or("").to_string()),
"PUT" => self
.client
.put(&full_url)
.body(body.unwrap_or("").to_string()),
"DELETE" => self.client.delete(&full_url),
_ => return Err("Unsupported HTTP method".to_string()),
};
// 处理 headers
if let Some(hdrs) = headers {
let mut header_map = HeaderMap::new();
for (key, value) in hdrs {
if let (Ok(header_name), Ok(header_value)) = (
HeaderName::from_bytes(key.as_bytes()),
HeaderValue::from_str(value),
) {
header_map.insert(header_name, header_value);
}
}
request = request.headers(header_map);
}
let response = request.send().await.map_err(|e| e.to_string())?;
response.text().await.map_err(|e| e.to_string())
}
}
#[allow(unused)]
#[async_trait]
pub trait ApiCallerTrait: Send + Sync {
async fn call_api<T>(
&self,
method: &str,
path: &str,
body: Option<&str>,
headers: Option<Vec<(&str, &str)>>
) -> Result<T, String>
where
T: DeserializeOwned + Send + Sync;
fn parse_json_response<T>(json_str: &str) -> Result<T, String>
where
T: DeserializeOwned,
{
serde_json::from_str(json_str).map_err(|e| e.to_string())
}
}

View File

@@ -0,0 +1,108 @@
use super::common::ApiCallerTrait;
use crate::config::api::mihomo::MIHOMO_URL;
use crate::model::api::common::ApiCaller;
use crate::model::api::mihomo::MihomoAPICaller;
use async_trait::async_trait;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use reqwest::Client;
use serde::de::DeserializeOwned;
use std::sync::Arc;
impl MihomoAPICaller {
#[allow(dead_code)]
pub fn new() -> Arc<RwLock<Self>> {
static INSTANCE: OnceCell<Arc<RwLock<MihomoAPICaller>>> = OnceCell::new();
INSTANCE
.get_or_init(|| {
let client = Client::new();
Arc::new(RwLock::new(MihomoAPICaller {
caller: ApiCaller {
url: MIHOMO_URL,
client,
},
}))
})
.clone()
}
}
#[async_trait]
impl ApiCallerTrait for MihomoAPICaller {
async fn call_api<T>(
&self,
method: &str,
path: &str,
body: Option<&str>,
headers: Option<Vec<(&str, &str)>>,
) -> Result<T, String>
where
T: DeserializeOwned + Send + Sync,
{
let response = self
.caller
.send_request(method, path, body, headers)
.await
.map_err(|e| e.to_string())?;
Self::parse_json_response::<T>(&response)
}
}
#[allow(unused)]
impl MihomoAPICaller {
pub async fn get_proxies() -> Result<serde_json::Value, String> {
Self::new()
.read()
.call_api("GET", "/proxies", None, None)
.await
}
pub async fn get_providers_proxies() -> Result<serde_json::Value, String> {
Self::new()
.read()
.call_api("GET", "/providers/proxies", None, None)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mihomo_api_singleton() {
let mihomo_api_caller1 = MihomoAPICaller::new();
let mihomo_api_caller2 = MihomoAPICaller::new();
assert!(Arc::ptr_eq(&mihomo_api_caller1, &mihomo_api_caller2));
}
#[tokio::test]
async fn test_mihomo_api_version() {
let mihomo_caller = MihomoAPICaller::new();
let response: Result<serde_json::Value, String> = mihomo_caller
.read()
.call_api("GET", "/version", None, None)
.await;
assert!(response.is_ok());
}
#[tokio::test]
async fn test_mihomo_get_proxies() {
let response = MihomoAPICaller::get_proxies().await;
assert!(response.is_ok());
if let Ok(proxies) = &response {
assert!(!proxies.get("proxies").is_none());
}
}
#[tokio::test]
async fn test_mihomo_get_providers_proxies() {
let response = MihomoAPICaller::get_providers_proxies().await;
println!("{:?}", response);
assert!(response.is_ok());
if let Ok(providers_proxies) = &response {
assert!(!providers_proxies.get("providers").is_none());
}
}
}

View File

@@ -0,0 +1,2 @@
pub mod common;
pub mod mihomo;

View File

@@ -0,0 +1,149 @@
use std::sync::Arc;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use crate::model::api::mihomo::MihomoAPICaller;
#[allow(unused)]
pub struct MihomoManager {
proxies: serde_json::Value,
providers_proxies: serde_json::Value,
}
#[allow(unused)]
impl MihomoManager {
pub fn new() -> Arc<RwLock<Self>> {
static INSTANCE: OnceCell<Arc<RwLock<MihomoManager>>> = OnceCell::new();
INSTANCE
.get_or_init(|| {
Arc::new(RwLock::new(MihomoManager {
proxies: serde_json::Value::Null,
providers_proxies: serde_json::Value::Null,
}))
})
.clone()
}
pub fn fetch_proxies(&self) -> &serde_json::Value {
&self.proxies
}
pub fn fetch_providers_proxies(&self) -> &serde_json::Value {
&self.providers_proxies
}
pub async fn refresh_proxies(&mut self) {
match MihomoAPICaller::get_proxies().await {
Ok(proxies) => self.proxies = proxies,
Err(e) => log::error!("Failed to get proxies: {}", e),
}
}
pub async fn refresh_providers_proxies(&mut self) {
match MihomoAPICaller::get_providers_proxies().await {
Ok(providers_proxies) => self.providers_proxies = providers_proxies,
Err(e) => log::error!("Failed to get providers proxies: {}", e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mihomo_manager_singleton() {
let manager1 = MihomoManager::new();
let manager2 = MihomoManager::new();
assert!(Arc::ptr_eq(&manager1, &manager2), "Should return same instance");
let manager = manager1.read();
assert!(manager.proxies.is_null());
assert!(manager.providers_proxies.is_null());
}
#[tokio::test]
async fn test_refresh_proxies() {
let manager = MihomoManager::new();
// Test initial state
{
let data = manager.read();
assert!(data.proxies.is_null());
}
// Test refresh
{
let mut data = manager.write();
data.refresh_proxies().await;
// Note: Since this depends on external API call,
// we can only verify that the refresh call completes
// without panicking. For more thorough testing,
// we would need to mock the API caller.
}
}
#[tokio::test]
async fn test_refresh_providers_proxies() {
let manager = MihomoManager::new();
// Test initial state
{
let data = manager.read();
assert!(data.providers_proxies.is_null());
}
// Test refresh
{
let mut data = manager.write();
data.refresh_providers_proxies().await;
// Note: Since this depends on external API call,
// we can only verify that the refresh call completes
// without panicking. For more thorough testing,
// we would need to mock the API caller.
}
}
#[tokio::test]
async fn test_fetch_proxies() {
let manager = MihomoManager::new();
// Test initial state
{
let data = manager.read();
let proxies = data.fetch_proxies();
assert!(proxies.is_null());
}
// Test after refresh
{
let mut data = manager.write();
data.refresh_proxies().await;
let _proxies = data.fetch_proxies();
// Can only verify the method returns without panicking
// Would need API mocking for more thorough testing
}
}
#[tokio::test]
async fn test_fetch_providers_proxies() {
let manager = MihomoManager::new();
// Test initial state
{
let data = manager.read();
let providers_proxies = data.fetch_providers_proxies();
assert!(providers_proxies.is_null());
}
// Test after refresh
{
let mut data = manager.write();
data.refresh_providers_proxies().await;
let _providers_proxies = data.fetch_providers_proxies();
// Can only verify the method returns without panicking
// Would need API mocking for more thorough testing
}
}
}

View File

@@ -1 +1,3 @@
pub mod sysinfo;
pub mod api;
pub mod sysinfo;
pub mod mihomo;