use anyhow::bail;
use semora_proto::{Mapping, MappingList};
use crate::args::{MapArgs, MapCmd};
use crate::client::Client;
use crate::{Ctx, exit, output, project_link};
pub async fn run(ctx: &Ctx, args: MapArgs) -> anyhow::Result<i32> {
let client = ctx.client()?;
let slug = project_slug(&client, args.project.as_deref())?;
match args.cmd {
MapCmd::Ls => {
let list = client.mappings(&slug).await?.mappings;
show(ctx, &slug, &list);
}
MapCmd::Add {
pattern,
folder,
at,
} => {
reject_drive_path("PATTERN", &pattern)?;
reject_drive_path("FOLDER", &folder)?;
let mut list = client.mappings(&slug).await?.mappings;
let at = match at {
None => list.len() + 1,
Some(n) if (1..=list.len() + 1).contains(&n) => n,
Some(_) => bail!("{}; --at can be 1 to {}", count(list.len()), list.len() + 1),
};
list.insert(at - 1, Mapping { pattern, folder });
let stored = client.set_mappings(&slug, &list).await?.mappings;
if !ctx.json {
println!(
"Added rule {at}: {} → {}",
stored[at - 1].pattern,
stored[at - 1].folder
);
}
show(ctx, &slug, &stored);
}
MapCmd::Rm { n } => {
let mut list = client.mappings(&slug).await?.mappings;
if n == 0 || n > list.len() {
bail!(
"there is no rule {n}; there {} {}",
if list.len() == 1 { "is" } else { "are" },
list.len()
);
}
let gone = list.remove(n - 1);
let stored = client.set_mappings(&slug, &list).await?.mappings;
if !ctx.json {
println!("Removed rule {n}: {} → {}", gone.pattern, gone.folder);
}
show(ctx, &slug, &stored);
}
}
Ok(exit::OK)
}
fn count(n: usize) -> String {
if n == 1 {
"there is 1 rule".to_string()
} else {
format!("there are {n} rules")
}
}
fn reject_drive_path(what: &str, value: &str) -> anyhow::Result<()> {
let b = value.as_bytes();
if b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && b[2] == b'/' {
bail!(
"{what} looks like a drive path. Your shell may have rewritten a leading \"/\" (Git Bash on Windows does). Leave the slash off, or use \".\" for the project root."
);
}
Ok(())
}
pub fn project_slug(client: &Client, flag: Option<&str>) -> anyhow::Result<String> {
if let Some(slug) = flag {
return Ok(slug.to_string());
}
let here = std::env::current_dir()?;
match project_link::find(&here)? {
Some(link) => {
link.check_server(client)?;
Ok(link.slug.clone())
}
None => {
bail!("no project: pass --project ID or run this inside a folder with a .semora file")
}
}
}
fn show(ctx: &Ctx, slug: &str, list: &[Mapping]) {
if ctx.json {
output::json(&MappingList {
mappings: list.to_vec(),
});
} else {
print_list(slug, list);
}
}
pub fn print_list(slug: &str, list: &[Mapping]) {
if list.is_empty() {
println!("No mappings for {slug}. Every document shows at its real path.");
return;
}
println!(
"Mappings for {slug} (the first match wins; a path no rule matches stays where it is)"
);
let number_width = list.len().to_string().len();
let pattern_width = list
.iter()
.map(|m| m.pattern.chars().count())
.max()
.unwrap_or(0);
for (i, m) in list.iter().enumerate() {
println!(
" {:>number_width$} {:<pattern_width$} → {}",
i + 1,
m.pattern,
m.folder
);
}
}