//! `pic routes` subcommands: `ls`, `create`, `rm`, `check`, `match`. use anyhow::{anyhow, Result}; use picloud_shared::{AppId, DispatchMode, HostKind, PathKind}; use crate::client::{CheckRouteBody, Client, CreateRouteBody, MatchRouteBody}; use crate::config; use crate::output::{KvBlock, OutputMode, Table}; pub async fn ls(script_id: &str, mode: OutputMode) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let routes = client.routes_list_for_script(script_id).await?; let mut table = Table::new([ "id", "method", "host", "path_kind", "path", "dispatch", "created_at", ]); for r in routes { table.row([ r.id.to_string(), r.method.clone().unwrap_or_else(|| "ANY".into()), host_label(r.host_kind, &r.host), path_kind_label(r.path_kind).to_string(), r.path.clone(), dispatch_label(r.dispatch_mode).to_string(), r.created_at.to_rfc3339(), ]); } table.print(mode); Ok(()) } #[allow(clippy::too_many_arguments)] pub async fn create( script_id: &str, method: Option<&str>, path: &str, path_kind: PathKind, host: &str, host_kind: HostKind, host_param_name: Option<&str>, dispatch_mode: DispatchMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let body = CreateRouteBody { host_kind, host, host_param_name, path_kind, path, method, dispatch_mode, }; let r = client.routes_create(script_id, &body).await?; println!( "Created route {} ({} {} {})", r.id, r.method.unwrap_or_else(|| "ANY".into()), host_label(r.host_kind, &r.host), r.path ); Ok(()) } pub async fn rm(route_id: &str) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; client.routes_delete(route_id).await?; println!("Deleted route {route_id}"); Ok(()) } pub async fn check( app: &str, method: Option<&str>, path: &str, path_kind: PathKind, host: &str, host_kind: HostKind, mode: OutputMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let app_id = resolve_app_id(&client, app).await?; let body = CheckRouteBody { app_id, host_kind, host: host.to_string(), path_kind, path: path.to_string(), method: method.map(str::to_string), }; let resp = client.routes_check(&body).await?; let mut block = KvBlock::new(); block.field("ok", resp.ok.to_string()); if let Some(reason) = resp.conflict_reason { block.field("conflict_reason", reason); } if let Some(r) = resp.conflicting_route { block .field("conflicting_route_id", r.id.to_string()) .field( "conflicting_method", r.method.unwrap_or_else(|| "ANY".into()), ) .field("conflicting_host", host_label(r.host_kind, &r.host)) .field("conflicting_path", r.path); } block.print(mode); Ok(()) } pub async fn match_route(app: &str, url: &str, method: &str, mode: OutputMode) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let app_id = resolve_app_id(&client, app).await?; let resp = client .routes_match(&MatchRouteBody { app_id, url, method, }) .await?; let mut block = KvBlock::new(); match resp.matched { None => { block.field("matched", "false"); } Some(m) => { block .field("matched", "true") .field("route_id", m.route_id) .field("script_id", m.script_id.to_string()); if !m.params.is_empty() { for (k, v) in m.params { block.field(format!("param.{k}"), v); } } if let Some(rest) = m.rest { block.field("rest", rest); } } } block.print(mode); Ok(()) } async fn resolve_app_id(client: &Client, ident: &str) -> Result { // Accept either a slug or a UUID. The apps_get endpoint takes both. let lookup = client .apps_get(ident) .await .map_err(|e| anyhow!("resolve app `{ident}`: {e}"))?; Ok(lookup.app.id) } const fn dispatch_label(d: DispatchMode) -> &'static str { match d { DispatchMode::Sync => "sync", DispatchMode::Async => "async", } } const fn path_kind_label(k: PathKind) -> &'static str { match k { PathKind::Exact => "exact", PathKind::Prefix => "prefix", PathKind::Param => "param", } } fn host_label(kind: HostKind, host: &str) -> String { match kind { HostKind::Any => "*".to_string(), HostKind::Strict => host.to_string(), HostKind::Wildcard => format!("*.{host}"), } }