libsubconverter/generator/yaml/
proxy_group_output.rs

1use crate::models::{ProxyGroupConfig, ProxyGroupType};
2use serde::ser::SerializeMap;
3use serde::{Serialize, Serializer};
4use std::collections::HashMap;
5
6/// Serialize a ProxyGroupConfig for Clash output
7///
8/// This implementation follows the serialization logic from the C++ code in subexport.cpp
9/// and ensures compatibility with the existing Clash configuration generation.
10///
11/// Specific serialization rules:
12/// - All groups have "name" and "type" fields
13/// - Type-specific fields are only included for relevant group types
14/// - Fields with default values are omitted
15/// - Empty lists are omitted
16/// - Special handling for DIRECT proxy and Smart group type
17pub fn serialize_proxy_group<S>(
18    group: &ProxyGroupConfig,
19    proxies: &[String],
20    serializer: S,
21) -> Result<S::Ok, S::Error>
22where
23    S: Serializer,
24{
25    let mut map = serializer.serialize_map(None)?;
26
27    // Always include name and type
28    map.serialize_entry("name", &group.name)?;
29
30    // Handle type (special case for Smart type which becomes url-test)
31    let type_str = if group.group_type == ProxyGroupType::Smart {
32        "url-test"
33    } else {
34        group.type_str()
35    };
36    map.serialize_entry("type", &type_str)?;
37
38    // Add type-specific fields
39    match group.group_type {
40        ProxyGroupType::Select | ProxyGroupType::Relay => {
41            // No special fields for these types
42        }
43        ProxyGroupType::LoadBalance => {
44            // Add strategy for load balancing
45            map.serialize_entry("strategy", &group.strategy_str())?;
46
47            // If not lazy, include the flag (false is default, so omit if true)
48            if !group.lazy {
49                map.serialize_entry("lazy", &group.lazy)?;
50            }
51
52            // Add URL test fields
53            map.serialize_entry("url", &group.url)?;
54
55            if group.interval > 0 {
56                map.serialize_entry("interval", &group.interval)?;
57            }
58
59            if group.tolerance > 0 {
60                map.serialize_entry("tolerance", &group.tolerance)?;
61            }
62        }
63        ProxyGroupType::Smart | ProxyGroupType::URLTest => {
64            // If not lazy, include the flag (true is default, so only include if false)
65            if !group.lazy {
66                map.serialize_entry("lazy", &group.lazy)?;
67            }
68
69            // Add URL test fields
70            map.serialize_entry("url", &group.url)?;
71
72            if group.interval > 0 {
73                map.serialize_entry("interval", &group.interval)?;
74            }
75
76            if group.tolerance > 0 {
77                map.serialize_entry("tolerance", &group.tolerance)?;
78            }
79        }
80        ProxyGroupType::Fallback => {
81            // Add URL test fields
82            map.serialize_entry("url", &group.url)?;
83
84            if group.interval > 0 {
85                map.serialize_entry("interval", &group.interval)?;
86            }
87
88            if group.tolerance > 0 {
89                map.serialize_entry("tolerance", &group.tolerance)?;
90            }
91        }
92        ProxyGroupType::SSID => {
93            // Not fully implemented in the original code
94            // Skip for now or add SSID-specific fields if needed
95        }
96    }
97
98    // Add optional common fields if they're not default values
99    if group.disable_udp {
100        map.serialize_entry("disable-udp", &group.disable_udp)?;
101    }
102
103    if group.persistent {
104        map.serialize_entry("persistent", &group.persistent)?;
105    }
106
107    if group.evaluate_before_use {
108        map.serialize_entry("evaluate-before-use", &group.evaluate_before_use)?;
109    }
110
111    // Add provider via "use" field if present, or filtered nodes
112    if !group.using_provider.is_empty() {
113        let provider_seq: Vec<&String> = group.using_provider.iter().collect();
114        map.serialize_entry("use", &provider_seq)?;
115    } else {
116        // Add proxies list if we have any
117        if !proxies.is_empty() {
118            map.serialize_entry("proxies", &proxies)?;
119        } else {
120            // Add DIRECT if empty, as seen in the original code
121            map.serialize_entry("proxies", &["DIRECT"])?;
122        }
123    }
124
125    map.end()
126}
127
128/// ClashProxyGroup represents a serializable proxy group for Clash configurations
129///
130/// This struct is designed to be serialized directly to YAML for Clash configurations.
131/// It contains all necessary fields with proper serde annotations to control when
132/// fields are included in the output.
133#[derive(Debug, Serialize)]
134pub struct ClashProxyGroup {
135    /// Name of the proxy group
136    pub name: String,
137
138    /// Type of the proxy group (select, url-test, fallback, load-balance, etc.)
139    #[serde(rename = "type")]
140    pub group_type: String,
141
142    /// List of proxy names in this group
143    #[serde(skip_serializing_if = "Vec::is_empty")]
144    pub proxies: Vec<String>,
145
146    /// List of provider names used by this group
147    #[serde(rename = "use", skip_serializing_if = "Vec::is_empty")]
148    pub using_provider: Vec<String>,
149
150    /// URL for testing (for url-test, fallback, and load-balance types)
151    #[serde(skip_serializing_if = "String::is_empty")]
152    pub url: String,
153
154    /// Interval in seconds between tests (for url-test, fallback, and load-balance types)
155    #[serde(skip_serializing_if = "is_zero_u32")]
156    pub interval: u32,
157
158    /// Timeout in seconds for tests
159    #[serde(skip_serializing_if = "is_zero_u32")]
160    pub timeout: u32,
161
162    /// Tolerance value for tests
163    #[serde(skip_serializing_if = "is_zero_u32")]
164    pub tolerance: u32,
165
166    /// Strategy for load balancing (for load-balance type)
167    #[serde(skip_serializing_if = "String::is_empty")]
168    pub strategy: String,
169
170    /// Whether to use lazy loading
171    #[serde(skip_serializing_if = "is_true")]
172    pub lazy: bool,
173
174    /// Whether to disable UDP support
175    #[serde(rename = "disable-udp", skip_serializing_if = "is_false")]
176    pub disable_udp: bool,
177
178    /// Whether to persist connections
179    #[serde(skip_serializing_if = "is_false")]
180    pub persistent: bool,
181
182    /// Whether to evaluate before use
183    #[serde(rename = "evaluate-before-use", skip_serializing_if = "is_false")]
184    pub evaluate_before_use: bool,
185}
186
187// Helper functions for serde skip conditions
188fn is_zero_u32(val: &u32) -> bool {
189    *val == 0
190}
191
192fn is_true(val: &bool) -> bool {
193    *val
194}
195
196fn is_false(val: &bool) -> bool {
197    !*val
198}
199
200impl From<&ProxyGroupConfig> for ClashProxyGroup {
201    fn from(config: &ProxyGroupConfig) -> Self {
202        // Special handling for Smart type which becomes url-test
203        let type_str = if config.group_type == ProxyGroupType::Smart {
204            "url-test".to_string()
205        } else {
206            config.type_str().to_string()
207        };
208
209        // Create a basic proxy group with common fields
210        let mut clash_group = ClashProxyGroup {
211            name: config.name.clone(),
212            group_type: type_str,
213            proxies: config.proxies.clone(),
214            using_provider: config.using_provider.clone(),
215            url: String::new(),
216            interval: 0,
217            timeout: 0,
218            tolerance: 0,
219            strategy: String::new(),
220            lazy: true, // Default to true
221            disable_udp: config.disable_udp,
222            persistent: config.persistent,
223            evaluate_before_use: config.evaluate_before_use,
224        };
225
226        // Add type-specific fields
227        match config.group_type {
228            ProxyGroupType::LoadBalance => {
229                clash_group.strategy = config.strategy_str().to_string();
230                clash_group.lazy = config.lazy;
231                clash_group.url = config.url.clone();
232                clash_group.interval = config.interval;
233                clash_group.tolerance = config.tolerance;
234            }
235            ProxyGroupType::URLTest | ProxyGroupType::Smart | ProxyGroupType::Fallback => {
236                clash_group.url = config.url.clone();
237                clash_group.interval = config.interval;
238                clash_group.tolerance = config.tolerance;
239
240                // Only URLTest and Smart use lazy loading
241                if matches!(
242                    config.group_type,
243                    ProxyGroupType::URLTest | ProxyGroupType::Smart
244                ) {
245                    clash_group.lazy = config.lazy;
246                }
247            }
248            _ => {} // No special fields for other types
249        }
250
251        // If proxies list is empty and no providers, add DIRECT
252        if clash_group.proxies.is_empty() && clash_group.using_provider.is_empty() {
253            clash_group.proxies = vec!["DIRECT".to_string()];
254        }
255
256        clash_group
257    }
258}
259
260/// Converts ProxyGroupConfigs to a vector of ClashProxyGroup objects
261pub fn convert_proxy_groups(
262    group_configs: &[ProxyGroupConfig],
263    filtered_nodes_map: Option<&HashMap<String, Vec<String>>>,
264) -> Vec<ClashProxyGroup> {
265    let mut clash_groups = Vec::with_capacity(group_configs.len());
266
267    for group in group_configs {
268        let mut clash_group = ClashProxyGroup::from(group);
269
270        // Replace proxies with filtered nodes if available
271        if let Some(filtered_map) = filtered_nodes_map {
272            if let Some(filtered_nodes) = filtered_map.get(&group.name) {
273                clash_group.proxies = filtered_nodes.clone();
274
275                // If proxies list is empty and no providers, add DIRECT
276                if clash_group.proxies.is_empty() && clash_group.using_provider.is_empty() {
277                    clash_group.proxies = vec!["DIRECT".to_string()];
278                }
279            }
280        }
281
282        clash_groups.push(clash_group);
283    }
284
285    clash_groups
286}
287
288/// Example function showing how to use the ClashProxyGroup
289///
290/// This demonstrates how to create and serialize proxy group configurations
291/// for Clash.
292///
293/// # Example
294///
295/// ```rust
296/// use crate::generator::yaml::proxy_group_output::{ClashProxyGroup, convert_proxy_groups};
297/// use crate::models::{ProxyGroupConfig, ProxyGroupType};
298/// use std::collections::HashMap;
299///
300/// // Create some proxy groups
301/// let mut groups = Vec::new();
302///
303/// // Add a simple selector group
304/// let mut selector = ProxyGroupConfig::new("Proxy".to_string(), ProxyGroupType::Select);
305/// selector.proxies = vec!["Server1".to_string(), "Server2".to_string()];
306/// groups.push(selector);
307///
308/// // Add a URL test group
309/// let mut url_test = ProxyGroupConfig::new("Auto".to_string(), ProxyGroupType::URLTest);
310/// url_test.url = "http://www.gstatic.com/generate_204".to_string();
311/// url_test.interval = 300;
312/// url_test.proxies = vec!["Server1".to_string(), "Server2".to_string()];
313/// groups.push(url_test);
314///
315/// // Create a Fallback group
316/// let mut fallback_group = ProxyGroupConfig::new("Fallback".to_string(), ProxyGroupType::Fallback);
317/// fallback_group.url = "http://www.gstatic.com/generate_204".to_string();
318/// fallback_group.interval = 300;
319/// fallback_group.proxies = vec!["Hong Kong".to_string(), "Singapore".to_string(), "US".to_string()];
320/// groups.push(fallback_group);
321///
322/// // Convert to Clash format
323/// let clash_groups = convert_proxy_groups(&groups, None);
324///
325/// // Serialize to YAML
326/// let yaml = serde_yaml::to_string(&clash_groups).unwrap();
327/// println!("{}", yaml);
328/// ```
329pub fn example_clash_groups() -> Vec<ClashProxyGroup> {
330    // Create sample proxy groups
331    let mut groups = Vec::new();
332
333    // Create a Select group (Manual selection)
334    let mut select_group = ProxyGroupConfig::new("Proxy".to_string(), ProxyGroupType::Select);
335    select_group.proxies = vec![
336        "Hong Kong".to_string(),
337        "Singapore".to_string(),
338        "US".to_string(),
339    ];
340    groups.push(select_group);
341
342    // Create a URL-Test group (Auto selection by latency)
343    let mut urltest_group = ProxyGroupConfig::new("Auto".to_string(), ProxyGroupType::URLTest);
344    urltest_group.url = "http://www.gstatic.com/generate_204".to_string();
345    urltest_group.interval = 300;
346    urltest_group.proxies = vec!["Hong Kong".to_string(), "Singapore".to_string()];
347    groups.push(urltest_group);
348
349    // Create a Fallback group
350    let mut fallback_group =
351        ProxyGroupConfig::new("Fallback".to_string(), ProxyGroupType::Fallback);
352    fallback_group.url = "http://www.gstatic.com/generate_204".to_string();
353    fallback_group.interval = 300;
354    fallback_group.proxies = vec![
355        "Hong Kong".to_string(),
356        "Singapore".to_string(),
357        "US".to_string(),
358    ];
359    groups.push(fallback_group);
360
361    // Convert to Clash format
362    convert_proxy_groups(&groups, None)
363}