mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 08:45:41 +08:00
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:
1
src-tauri/src/config/api/mihomo.rs
Normal file
1
src-tauri/src/config/api/mihomo.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub const MIHOMO_URL: &str = concat!("http://", "127.0.0.1", ":", "9097");
|
||||
1
src-tauri/src/config/api/mod.rs
Normal file
1
src-tauri/src/config/api/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod mihomo;
|
||||
@@ -21,3 +21,6 @@ pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||
}
|
||||
"#;
|
||||
|
||||
|
||||
pub mod api;
|
||||
@@ -85,6 +85,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use serde_yaml::Value;
|
||||
|
||||
#[test]
|
||||
|
||||
20
src-tauri/src/model/api/common.rs
Normal file
20
src-tauri/src/model/api/common.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use reqwest::Client;
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) struct ApiCaller<'a> {
|
||||
pub(crate) url: &'a str,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_api_caller() {
|
||||
let _api_caller = ApiCaller {
|
||||
url: "https://example.com",
|
||||
client: Client::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
5
src-tauri/src/model/api/mihomo.rs
Normal file
5
src-tauri/src/model/api/mihomo.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use super::common::ApiCaller;
|
||||
|
||||
pub struct MihomoAPICaller {
|
||||
pub(crate) caller: ApiCaller<'static>,
|
||||
}
|
||||
2
src-tauri/src/model/api/mod.rs
Normal file
2
src-tauri/src/model/api/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod common;
|
||||
pub mod mihomo;
|
||||
@@ -1 +1,2 @@
|
||||
pub mod sysinfo;
|
||||
pub mod api;
|
||||
pub mod sysinfo;
|
||||
|
||||
70
src-tauri/src/module/api/common.rs
Normal file
70
src-tauri/src/module/api/common.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
108
src-tauri/src/module/api/mihomo.rs
Normal file
108
src-tauri/src/module/api/mihomo.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src-tauri/src/module/api/mod.rs
Normal file
2
src-tauri/src/module/api/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod common;
|
||||
pub mod mihomo;
|
||||
149
src-tauri/src/module/mihomo.rs
Normal file
149
src-tauri/src/module/mihomo.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pub mod sysinfo;
|
||||
pub mod api;
|
||||
pub mod sysinfo;
|
||||
pub mod mihomo;
|
||||
Reference in New Issue
Block a user