code-moniker 0.4.0

Standalone CLI / linter for the code-moniker symbol graph: file and directory probes, project-wide architecture rules.
Documentation
use std::io::Write;

use crate::args::{LangsArgs, LangsFormat};
use crate::{Exit, language_kinds};

pub fn run<W1: Write, W2: Write>(args: &LangsArgs, stdout: &mut W1, stderr: &mut W2) -> Exit {
	match langs_inner(args, stdout) {
		Ok(()) => Exit::Match,
		Err(e) => {
			let _ = writeln!(stderr, "code-moniker: {e:#}");
			Exit::UsageError
		}
	}
}

fn collect_kinds(
	lang: code_moniker_core::lang::Lang,
) -> Vec<(&'static str, code_moniker_core::core::shape::Shape)> {
	use code_moniker_core::core::shape::Shape;
	language_kinds::known_kinds(std::iter::once(&lang))
		.into_iter()
		.map(|k| (k, Shape::for_kind(k.as_bytes())))
		.collect()
}

fn langs_inner<W: Write>(args: &LangsArgs, stdout: &mut W) -> anyhow::Result<()> {
	use code_moniker_core::lang::Lang;

	match &args.lang {
		None => match args.format {
			LangsFormat::Text => {
				for lang in Lang::ALL {
					writeln!(stdout, "{}", lang.tag())?;
				}
			}
			LangsFormat::Json => {
				let tags: Vec<&str> = Lang::ALL.iter().map(|l| l.tag()).collect();
				serde_json::to_writer_pretty(&mut *stdout, &tags)?;
				stdout.write_all(b"\n")?;
			}
		},
		Some(tag) => {
			let lang = Lang::from_tag(tag).ok_or_else(|| {
				let known: Vec<&str> = Lang::ALL.iter().map(|l| l.tag()).collect();
				anyhow::anyhow!("unknown language `{tag}` (known: {})", known.join(", "))
			})?;
			let kinds = collect_kinds(lang);
			let visibilities = lang.allowed_visibilities();
			match args.format {
				LangsFormat::Text => write_langs_text(stdout, lang.tag(), &kinds, visibilities)?,
				LangsFormat::Json => write_langs_json(stdout, lang.tag(), &kinds, visibilities)?,
			}
		}
	}
	Ok(())
}

fn write_langs_text<W: Write>(
	w: &mut W,
	tag: &str,
	kinds: &[(&'static str, code_moniker_core::core::shape::Shape)],
	visibilities: &[&'static str],
) -> std::io::Result<()> {
	use code_moniker_core::core::shape::Shape;
	writeln!(w, "lang: {tag}")?;
	writeln!(w, "kinds:")?;
	let width = Shape::ALL
		.iter()
		.map(|s| s.as_str().len() + 1)
		.max()
		.unwrap_or(0);
	for shape in Shape::ALL {
		let names: Vec<&str> = kinds
			.iter()
			.filter(|(_, s)| s == shape)
			.map(|(n, _)| *n)
			.collect();
		if names.is_empty() {
			continue;
		}
		writeln!(
			w,
			"  {:<width$} {}",
			format!("{}:", shape.as_str()),
			names.join(", "),
			width = width
		)?;
	}
	if visibilities.is_empty() {
		writeln!(w, "visibilities: (none — ignored by this language)")?;
	} else {
		writeln!(w, "visibilities: {}", visibilities.join(", "))?;
	}
	Ok(())
}

fn write_langs_json<W: Write>(
	w: &mut W,
	tag: &str,
	kinds: &[(&'static str, code_moniker_core::core::shape::Shape)],
	visibilities: &[&'static str],
) -> anyhow::Result<()> {
	#[derive(serde::Serialize)]
	struct KindEntry<'a> {
		name: &'a str,
		shape: &'a str,
	}
	#[derive(serde::Serialize)]
	struct Out<'a> {
		lang: &'a str,
		kinds: Vec<KindEntry<'a>>,
		visibilities: &'a [&'static str],
	}
	let out = Out {
		lang: tag,
		kinds: kinds
			.iter()
			.map(|(n, s)| KindEntry {
				name: n,
				shape: s.as_str(),
			})
			.collect(),
		visibilities,
	};
	serde_json::to_writer_pretty(&mut *w, &out)?;
	w.write_all(b"\n")?;
	Ok(())
}