supercode-interchange 0.4.15

Canonical, provider-neutral session interchange primitives for Supercode
Documentation
//! `.env` in a profile folder is the vault's store: `KEY=value`, quotes
//! stripped on read, JSON-quoted on write, sorted by key.

use std::collections::BTreeMap;

/// Parse a `.env` text into its entries.
pub fn parse_dotenv(text: &str) -> BTreeMap<String, String> {
    let mut out = BTreeMap::new();
    for line in text.lines() {
        let t = line.trim();
        if t.is_empty() || t.starts_with('#') {
            continue;
        }
        let Some(eq) = t.find('=') else { continue };
        let key = t[..eq].trim().to_string();
        let mut value = t[eq + 1..].trim().to_string();
        let quoted = (value.starts_with('"') && value.ends_with('"') && value.len() >= 2)
            || (value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2);
        if quoted {
            value = value[1..value.len() - 1].to_string();
        }
        out.insert(key, value);
    }
    out
}

/// Render entries as `.env` text, sorted by key, values JSON-quoted.
pub fn render_dotenv(entries: &BTreeMap<String, String>) -> String {
    let mut text = entries
        .iter()
        .map(|(k, v)| format!("{k}={}", serde_json::to_string(v).unwrap()))
        .collect::<Vec<_>>()
        .join("\n");
    if !entries.is_empty() {
        text.push('\n');
    }
    text
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn round_trip_and_quotes() {
        let parsed = parse_dotenv("# c\nA=1\nB=\"two words\"\nC='x'\nbad line\n");
        assert_eq!(parsed.get("A").map(String::as_str), Some("1"));
        assert_eq!(parsed.get("B").map(String::as_str), Some("two words"));
        assert_eq!(parsed.get("C").map(String::as_str), Some("x"));
        assert_eq!(parsed.len(), 3);
        assert_eq!(
            render_dotenv(&parsed),
            "A=\"1\"\nB=\"two words\"\nC=\"x\"\n"
        );
        assert_eq!(render_dotenv(&BTreeMap::new()), "");
    }
}