cargo-tangle 0.5.0-alpha.19

A command-line tool to create and deploy blueprints on Tangle Network
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use alloy_primitives::Address;
use blueprint_manager::config::SourceType;
use blueprint_runner::config::{Protocol, ProtocolSettings};
use blueprint_runner::error::ConfigError;
use blueprint_runner::tangle::config::TangleProtocolSettings;
use dotenv::from_path;

pub fn load_protocol_settings(
    protocol: Protocol,
    settings_file: &PathBuf,
) -> Result<ProtocolSettings, ConfigError> {
    from_path(settings_file)
        .map_err(|e| ConfigError::Other(format!("Failed to load settings file: {e}").into()))?;

    match protocol {
        Protocol::Tangle => {
            let blueprint_id = std::env::var("BLUEPRINT_ID")
                .map_err(|_| ConfigError::Other("Missing BLUEPRINT_ID".into()))?
                .parse()
                .map_err(|_| ConfigError::Other("Invalid BLUEPRINT_ID".into()))?;
            let service_id = std::env::var("SERVICE_ID")
                .ok()
                .and_then(|v| v.parse().ok());
            let tangle_contract = env_var("TANGLE_CONTRACT")?;
            let staking_contract = env_var("STAKING_CONTRACT")?;
            let status_registry_contract = env_var("STATUS_REGISTRY_CONTRACT")?;

            Ok(ProtocolSettings::Tangle(TangleProtocolSettings {
                blueprint_id,
                service_id,
                tangle_contract,
                staking_contract,
                status_registry_contract,
            }))
        }
        _ => Err(ConfigError::UnexpectedProtocol("Unsupported protocol")),
    }
}

fn env_var(name: &str) -> Result<Address, ConfigError> {
    let value =
        std::env::var(name).map_err(|_| ConfigError::Other(format!("Missing {name}").into()))?;
    Address::from_str(&value).map_err(|_| ConfigError::Other(format!("Invalid {name}").into()))
}

const PREFERRED_SOURCE_KEY: &str = "PREFERRED_SOURCE";
const USE_VM_KEY: &str = "USE_VM";

#[derive(Clone, Copy, Debug, Default)]
pub struct RuntimePreferences {
    pub preferred_source: Option<SourceType>,
    pub use_vm: Option<bool>,
}

pub fn load_runtime_preferences() -> RuntimePreferences {
    let preferred_source = std::env::var(PREFERRED_SOURCE_KEY)
        .ok()
        .and_then(|value| parse_source_type(&value));
    let use_vm = std::env::var(USE_VM_KEY)
        .ok()
        .and_then(|value| parse_bool(&value));
    RuntimePreferences {
        preferred_source,
        use_vm,
    }
}

pub fn write_runtime_preferences(
    path: &Path,
    prefs: RuntimePreferences,
) -> Result<(), ConfigError> {
    let mut updates = Vec::new();
    if let Some(source) = prefs.preferred_source {
        updates.push((
            PREFERRED_SOURCE_KEY.to_string(),
            source_to_str(source).to_string(),
        ));
    }
    if let Some(use_vm) = prefs.use_vm {
        updates.push((USE_VM_KEY.to_string(), use_vm.to_string()));
    }

    if updates.is_empty() {
        return Ok(());
    }

    write_env_entries(path, &updates)
}

fn parse_source_type(value: &str) -> Option<SourceType> {
    match value.to_lowercase().as_str() {
        "native" => Some(SourceType::Native),
        "container" => Some(SourceType::Container),
        "wasm" => Some(SourceType::Wasm),
        _ => None,
    }
}

fn source_to_str(source: SourceType) -> &'static str {
    match source {
        SourceType::Native => "native",
        SourceType::Container => "container",
        SourceType::Wasm => "wasm",
    }
}

fn parse_bool(value: &str) -> Option<bool> {
    match value.to_lowercase().as_str() {
        "1" | "true" | "yes" => Some(true),
        "0" | "false" | "no" => Some(false),
        _ => None,
    }
}

fn write_env_entries(path: &Path, updates: &[(String, String)]) -> Result<(), ConfigError> {
    let mut lines = if path.exists() {
        let content = fs::read_to_string(path).map_err(|e| {
            ConfigError::Other(format!("Failed to read {}: {e}", path.display()).into())
        })?;
        parse_env_lines(&content)
    } else {
        Vec::new()
    };

    let mut remaining = updates.iter().cloned().collect::<BTreeMap<_, _>>();

    for line in &mut lines {
        if let EnvLine::Entry { key, value } = line {
            if let Some(new_value) = remaining.remove(key) {
                *value = new_value;
            }
        }
    }

    for (key, value) in remaining {
        lines.push(EnvLine::Entry { key, value });
    }

    let mut content = String::new();
    for line in lines {
        match line {
            EnvLine::Entry { key, value } => {
                content.push_str(&format!("{key}={value}\n"));
            }
            EnvLine::Other(raw) => {
                content.push_str(&raw);
                content.push('\n');
            }
        }
    }

    fs::write(path, content)
        .map_err(|e| ConfigError::Other(format!("Failed to write {}: {e}", path.display()).into()))
}

fn parse_env_lines(input: &str) -> Vec<EnvLine> {
    input
        .lines()
        .map(|line| {
            if let Some((key, value)) = line.split_once('=') {
                EnvLine::Entry {
                    key: key.trim().to_string(),
                    value: value.trim().to_string(),
                }
            } else {
                EnvLine::Other(line.to_string())
            }
        })
        .collect()
}

enum EnvLine {
    Entry { key: String, value: String },
    Other(String),
}