ztnet 0.1.14

ZTNet CLI — manage ZeroTier networks via ZTNet
use std::io::{self, Write};

use comfy_table::{presets, Cell, Table};
use serde_json::Value;

use crate::cli::OutputFormat;
use crate::error::CliError;

pub fn print_value(value: &Value, format: OutputFormat, no_color: bool) -> Result<(), CliError> {
	let mut stdout = io::stdout().lock();
	write_value(&mut stdout, value, format, no_color)?;
	writeln!(&mut stdout)?;
	Ok(())
}

pub fn write_value<W: Write>(
	mut writer: W,
	value: &Value,
	format: OutputFormat,
	no_color: bool,
) -> Result<(), CliError> {
	match format {
		OutputFormat::Json => {
			let pretty = serde_json::to_string_pretty(value)?;
			write!(writer, "{pretty}")?;
		}
		OutputFormat::Yaml => {
			let yaml = serde_yaml::to_string(value)
				.map_err(|err| CliError::InvalidArgument(format!("yaml serialize error: {err}")))?;
			write!(writer, "{yaml}")?;
		}
		OutputFormat::Raw => {
			let compact = serde_json::to_string(value)?;
			write!(writer, "{compact}")?;
		}
		OutputFormat::Table => {
			if !write_table(&mut writer, value, no_color)? {
				let pretty = serde_json::to_string_pretty(value)?;
				write!(writer, "{pretty}")?;
			}
		}
	}

	Ok(())
}

fn write_table<W: Write>(mut writer: W, value: &Value, _no_color: bool) -> Result<bool, CliError> {
	let Some(rows) = value.as_array() else {
		return Ok(false);
	};

	let mut table = Table::new();
	table.load_preset(presets::UTF8_FULL);

	let preferred_columns = [
		"id",
		"name",
		"orgName",
		"nwid",
		"nwname",
		"authorized",
		"memberCount",
		"host",
		"default_profile",
		"profiles",
	];

	let mut columns: Vec<&'static str> = Vec::new();
	for col in preferred_columns {
		if rows.iter().any(|row| row.get(col).is_some()) {
			columns.push(col);
		}
	}
	if columns.is_empty() {
		return Ok(false);
	}

	table.set_header(columns.iter().copied());

	for row in rows {
		let mut cells = Vec::with_capacity(columns.len());
		for col in &columns {
			let text = row.get(*col).map(value_to_cell).unwrap_or_default();
			cells.push(Cell::new(text));
		}
		table.add_row(cells);
	}

	write!(writer, "{table}")?;
	Ok(true)
}

fn value_to_cell(value: &Value) -> String {
	match value {
		Value::Null => String::new(),
		Value::Bool(v) => v.to_string(),
		Value::Number(v) => v.to_string(),
		Value::String(v) => v.clone(),
		_ => serde_json::to_string(value).unwrap_or_default(),
	}
}