use std::io::{self, Write};
use std::net::SocketAddr;
use serde::Serialize;
use crate::cli::OutputFormat;
use crate::cli::admin_auth::{admin_get, parse_error_message, request_with_admin_auth};
pub struct RouteAddOpts {
pub model: String,
pub endpoints: Vec<String>,
pub strategy: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct RouteEndpoint {
pub provider: String,
pub service_id: String,
}
#[derive(Debug, Serialize)]
pub struct RouteEntry {
pub name: String,
pub source: String,
pub endpoints: Vec<RouteEndpoint>,
pub strategy: String,
}
#[derive(Debug, Serialize)]
pub struct RouteListData {
pub routes: Vec<RouteEntry>,
}
pub fn query_list(
config: &bitrouter_config::BitrouterConfig,
addr: SocketAddr,
) -> Result<RouteListData, Box<dyn std::error::Error>> {
let resp = admin_get(config, addr, "/admin/routes")?;
if !resp.status().is_success() {
let msg = parse_error_message(resp)?;
return Err(format!("failed to list routes: {msg}").into());
}
let body: serde_json::Value = resp.json()?;
let raw_routes = body["routes"].as_array();
let routes = match raw_routes {
Some(raw) if !raw.is_empty() => raw
.iter()
.map(|route| {
let name = route["name"].as_str().unwrap_or("?").to_owned();
let source = route["source"].as_str().unwrap_or("?").to_owned();
let strategy = route["strategy"].as_str().unwrap_or("").to_owned();
let endpoints = route["endpoints"]
.as_array()
.map(|eps| {
eps.iter()
.map(|ep| RouteEndpoint {
provider: ep["provider"].as_str().unwrap_or("?").to_owned(),
service_id: ep["service_id"].as_str().unwrap_or("").to_owned(),
})
.collect()
})
.unwrap_or_default();
RouteEntry {
name,
source,
endpoints,
strategy,
}
})
.collect(),
_ => vec![],
};
Ok(RouteListData { routes })
}
pub fn render_list_text(data: &RouteListData, w: &mut impl Write) -> io::Result<()> {
if data.routes.is_empty() {
writeln!(w, " (no routes configured)")?;
return Ok(());
}
for route in &data.routes {
let targets: Vec<String> = route
.endpoints
.iter()
.map(|ep| {
if ep.service_id.is_empty() {
ep.provider.clone()
} else {
format!("{}:{}", ep.provider, ep.service_id)
}
})
.collect();
let strategy_suffix = if route.strategy.is_empty() {
String::new()
} else {
format!(" ({})", route.strategy)
};
writeln!(
w,
" {} \u{2192} {}{strategy_suffix} [{}]",
route.name,
targets.join(", "),
route.source
)?;
}
Ok(())
}
pub fn run_list(
config: &bitrouter_config::BitrouterConfig,
addr: SocketAddr,
output: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let data = query_list(config, addr)?;
match output {
OutputFormat::Text => render_list_text(&data, &mut io::stdout())?,
OutputFormat::Json => serde_json::to_writer(io::stdout(), &data)?,
}
Ok(())
}
pub fn run_add(
config: &bitrouter_config::BitrouterConfig,
addr: SocketAddr,
opts: RouteAddOpts,
) -> Result<(), Box<dyn std::error::Error>> {
let endpoints: Vec<serde_json::Value> = opts
.endpoints
.iter()
.map(|ep| {
let (provider, service_id) = ep.split_once(':').unwrap_or((ep, ""));
serde_json::json!({
"provider": provider,
"service_id": service_id,
})
})
.collect();
let body = serde_json::json!({
"name": opts.model,
"strategy": opts.strategy.unwrap_or_else(|| "priority".to_owned()),
"endpoints": endpoints,
});
let url = format!("http://{addr}/admin/routes");
let client = reqwest::blocking::Client::new();
let resp = request_with_admin_auth(config, client.post(&url))?
.json(&body)
.send()?;
if resp.status().is_success() {
println!("route '{}' added", opts.model);
} else {
let msg = parse_error_message(resp)?;
return Err(format!("failed to add route: {msg}").into());
}
Ok(())
}
pub fn run_remove(
config: &bitrouter_config::BitrouterConfig,
addr: SocketAddr,
name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let url = format!("http://{addr}/admin/routes/{name}");
let client = reqwest::blocking::Client::new();
let resp = request_with_admin_auth(config, client.delete(&url))?.send()?;
if resp.status().is_success() {
println!("route '{name}' removed");
} else {
let msg = parse_error_message(resp)?;
return Err(format!("failed to remove route: {msg}").into());
}
Ok(())
}