convertor 2.6.12

A profile converter for surge/clash.
Documentation
use crate::config::client_config::ProxyClient;
use crate::core::profile::clash_profile::ClashProfile;
use crate::core::profile::policy::Policy;
use crate::core::profile::proxy::Proxy;
use crate::core::profile::proxy_group::ProxyGroup;
use crate::core::profile::rule::{ProviderRule, Rule};
use crate::core::profile::rule_provider::RuleProvider;
use crate::core::renderer::{INDENT, Renderer};
use crate::error::RenderError;
use std::fmt::Write;
use tracing::instrument;

type Result<T> = core::result::Result<T, RenderError>;

pub struct ClashRenderer;

impl Renderer for ClashRenderer {
    type PROFILE = ClashProfile;

    fn client() -> ProxyClient {
        ProxyClient::Clash
    }

    #[instrument(skip_all)]
    fn render_profile(profile: &Self::PROFILE) -> Result<String> {
        let mut output = String::new();
        writeln!(output, "{}", Self::render_general(profile)?)?;

        let proxies = Self::render_proxies(&profile.proxies)?;
        writeln!(output, "proxies:")?;
        writeln!(output, "{proxies}")?;

        let proxy_groups = Self::render_proxy_groups(&profile.proxy_groups)?;
        writeln!(output, "proxy-groups:")?;
        writeln!(output, "{proxy_groups}")?;

        let rule_providers = Self::render_rule_providers(&profile.rule_providers)?;
        writeln!(output, "rule-providers:")?;
        writeln!(output, "{rule_providers}")?;

        let rules = Self::render_rules(&profile.rules)?;
        writeln!(output, "rules:")?;
        writeln!(output, "{rules}")?;

        Ok(output)
    }

    #[instrument(skip_all)]
    fn render_general(profile: &Self::PROFILE) -> Result<String> {
        let mut output = String::new();
        writeln!(output, "port: {}", profile.port)?;
        writeln!(output, "socks-port: {}", profile.socks_port)?;
        writeln!(output, "redir-port: {}", profile.redir_port)?;
        writeln!(output, "allow-lan: {}", profile.allow_lan)?;
        writeln!(output, "mode: {}", profile.mode)?;
        writeln!(output, "log-level: {}", profile.log_level)?;
        writeln!(output, r#"external-controller: {}"#, profile.external_controller)?;
        writeln!(output, r#"external-ui: {}"#, profile.external_ui)?;
        if let Some(secret) = &profile.secret {
            writeln!(output, r#"secret: "{secret}""#)?;
        }
        Ok(output)
    }

    fn render_proxy(proxy: &Proxy) -> Result<String> {
        let mut output = String::new();
        write!(output, "{{ ")?;
        write!(output, r#"name: "{}""#, &proxy.name)?;
        write!(output, r#", type: "{}""#, &proxy.r#type)?;
        write!(output, r#", server: "{}""#, &proxy.server)?;
        write!(output, r#", port: {}"#, &proxy.port)?;
        write!(output, r#", password: "{}""#, &proxy.password)?;
        if let Some(udp) = &proxy.udp {
            write!(output, r#", udp: {udp}"#)?;
        }
        if let Some(tfo) = &proxy.tfo {
            write!(output, r#", tfo: {tfo}"#)?;
        }
        if let Some(cipher) = &proxy.cipher {
            write!(output, r#", cipher: {cipher}"#)?;
        }
        if let Some(sni) = &proxy.sni {
            write!(output, r#", sni: "{sni}""#)?;
        }
        if let Some(skip_cert_verify) = &proxy.skip_cert_verify {
            write!(output, r#", skip-cert-verify: {skip_cert_verify}"#)?;
        }
        write!(output, " }}")?;
        Ok(output)
    }

    fn render_proxy_group(proxy_group: &ProxyGroup) -> Result<String> {
        let mut output = String::new();
        write!(output, "{{ ")?;
        write!(output, r#"name: "{}""#, proxy_group.name)?;
        write!(output, r#", type: "{}""#, proxy_group.r#type.as_str())?;
        write!(output, r#", proxies: [ {} ]"#, proxy_group.proxies.join(", "))?;
        write!(output, " }}")?;
        Ok(output)
    }

    fn render_rule(rule: &Rule) -> Result<String> {
        let mut output = String::new();
        write!(output, "{}", rule.rule_type.as_str())?;
        if let Some(value) = &rule.value {
            write!(output, ",{value}")?;
        }
        write!(output, ",{}", Self::render_policy(&rule.policy)?)?;
        Ok(output)
    }

    fn render_rule_for_provider(rule: &Rule) -> Result<String> {
        Self::render_rule(rule)
    }

    fn render_provider_rule(rule: &ProviderRule) -> Result<String> {
        Ok(format!("{},{}", rule.rule_type.as_str(), rule.value))
    }

    #[instrument(skip_all)]
    fn render_rule_providers(rule_providers: &[(String, RuleProvider)]) -> Result<String> {
        let output = rule_providers
            .iter()
            .map(Self::render_rule_provider)
            .map(|line| line.map(|line| format!("{:indent$}{}", "", line, indent = INDENT)))
            .collect::<Result<Vec<_>>>()?
            .join("\n");
        Ok(output)
    }

    fn render_rule_provider(rule_provider: &(String, RuleProvider)) -> Result<String> {
        let (name, rule_provider) = rule_provider;
        Ok(format!(
            r#"{}: {{ type: "{}", url: "{}", path: "{}", interval: {}, size-limit: {}, format: "{}", behavior: "{}" }}"#,
            name,
            rule_provider.r#type,
            rule_provider.url,
            rule_provider.path,
            rule_provider.interval,
            rule_provider.size_limit,
            rule_provider.format,
            rule_provider.behavior
        ))
    }

    fn render_provider_name_for_policy(policy: &Policy) -> String {
        let mut output = if policy.is_subscription {
            "Subscription".to_string()
        } else {
            policy.name.clone()
        };
        match &policy.option {
            Some(option) => {
                output.push('_');
                output.push_str(option.replace('-', "_").as_str());
            }
            None => {
                output.push_str("_policy");
            }
        }
        output
    }
}