dsc-rs 0.10.15

Discourse CLI tool for managing multiple Discourse forums: track installs, run upgrades over SSH, manage emojis, sync topics and categories as Markdown, and more.
Documentation
use crate::commands::common::{fetch_fullname_from_url, parse_tags};
use crate::config::{Config, DiscourseConfig};
use crate::utils::slugify;
use anyhow::{Context, Result};
use std::fs;
use std::io::{self, Read};
use std::path::Path;

pub fn import_discourses(config: &mut Config, path: Option<&Path>) -> Result<()> {
    let mut raw = String::new();
    if let Some(path) = path {
        if path == Path::new("-") {
            io::stdin().read_to_string(&mut raw)?;
        } else {
            raw =
                fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
        }
    } else {
        io::stdin().read_to_string(&mut raw)?;
    }
    import_from_string(config, &raw, path)?;
    Ok(())
}

fn import_from_string(config: &mut Config, raw: &str, path: Option<&Path>) -> Result<()> {
    let is_csv = path.and_then(|p| p.extension().and_then(|s| s.to_str())) == Some("csv")
        || looks_like_csv(raw);
    if is_csv {
        import_csv(config, raw)?;
    } else {
        import_text(config, raw)?;
    }
    Ok(())
}

fn import_text(config: &mut Config, raw: &str) -> Result<()> {
    for line in raw.lines() {
        let url = line.trim();
        if url.is_empty() {
            continue;
        }
        let fullname = fetch_fullname_from_url(url);
        let name = if let Some(title) = fullname.as_deref() {
            slugify(title)
        } else {
            slugify(url)
        };
        config.discourse.push(DiscourseConfig {
            name,
            baseurl: url.to_string(),
            fullname,
            ..DiscourseConfig::default()
        });
    }
    Ok(())
}

fn import_csv(config: &mut Config, raw: &str) -> Result<()> {
    let mut reader = csv::Reader::from_reader(raw.as_bytes());
    for result in reader.records() {
        let record = result?;
        let name = record.get(0).unwrap_or("").trim();
        let url = record.get(1).unwrap_or("").trim();
        if url.is_empty() {
            continue;
        }
        let fullname = fetch_fullname_from_url(url);
        let name = if name.is_empty() {
            if let Some(title) = fullname.as_deref() {
                slugify(title)
            } else {
                slugify(url)
            }
        } else {
            name.to_string()
        };
        let tags = record.get(2).map(parse_tags).filter(|t| !t.is_empty());
        config.discourse.push(DiscourseConfig {
            name,
            baseurl: url.to_string(),
            fullname,
            tags,
            ..DiscourseConfig::default()
        });
    }
    Ok(())
}

fn looks_like_csv(raw: &str) -> bool {
    let first = raw.lines().find(|line| !line.trim().is_empty());
    let Some(first) = first else { return false };
    let lower = first.to_ascii_lowercase();
    lower.contains("name") && lower.contains("url") && first.contains(',')
}