use anyhow::Result;
use clap::{Args, Subcommand};
use serde_json::json;
use crate::commands::provision::{client_from_config, developer_session};
use crate::config::CliConfig;
use crate::output;
#[derive(Subcommand)]
pub enum OrgsCommand {
List(ListArgs),
Create(CreateArgs),
Use(UseArgs),
}
#[derive(Args)]
pub struct UseArgs {
pub org: String,
}
#[derive(Args)]
pub struct ListArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct CreateArgs {
pub name: String,
#[arg(long)]
pub json: bool,
}
pub async fn run(command: OrgsCommand) -> Result<()> {
match command {
OrgsCommand::List(list) => list_orgs(list).await,
OrgsCommand::Create(create) => create_org(create).await,
OrgsCommand::Use(args) => use_org(args).await,
}
}
async fn use_org(args: UseArgs) -> Result<()> {
let mut config = CliConfig::load()?;
let token = developer_session(&mut config, "orgs").await?;
let client = client_from_config(&config).await?;
let orgs = client.list_organizations(&token).await?;
let org = find_org(&orgs, &args.org)?;
let switched = config.org_id.as_deref() != Some(org.id.as_str());
config.org_id = Some(org.id.clone());
if switched {
config.app_id = None;
config.api_key = None;
}
config.save()?;
output::success(&format!("using organization `{}` ({})", org.name, org.id));
if switched {
output::note("Run `offline setup` to pick or create an application in it");
}
Ok(())
}
pub(crate) fn find_org<'a>(
orgs: &'a [crate::api::types::Organization],
wanted: &str,
) -> Result<&'a crate::api::types::Organization> {
let wanted = wanted.trim();
let mut by_name = orgs.iter().filter(|o| o.name.eq_ignore_ascii_case(wanted));
if let Some(org) = orgs.iter().find(|o| o.id == wanted) {
return Ok(org);
}
match (by_name.next(), by_name.next()) {
(Some(org), None) => Ok(org),
(Some(_), Some(_)) => anyhow::bail!(
"more than one organization is named `{wanted}` — pass its id (`offline orgs list`)"
),
_ => anyhow::bail!("no organization `{wanted}` — see `offline orgs list`"),
}
}
async fn list_orgs(args: ListArgs) -> Result<()> {
let mut config = CliConfig::load()?;
let token = developer_session(&mut config, "orgs").await?;
let client = client_from_config(&config).await?;
let orgs = client.list_organizations(&token).await?;
if args.json {
output::json(&json!({ "organizations": orgs }))?;
return Ok(());
}
output::heading("organizations");
if orgs.is_empty() {
output::note("no organizations — run `offline orgs create \"My Org\"`");
return Ok(());
}
for org in &orgs {
let role = org.role.as_deref().unwrap_or("member");
println!(" {} {} ({role})", org.id, org.name);
}
if orgs.len() == 1 {
config.org_id = Some(orgs[0].id.clone());
config.save()?;
}
Ok(())
}
async fn create_org(args: CreateArgs) -> Result<()> {
let mut config = CliConfig::load()?;
let token = developer_session(&mut config, "orgs").await?;
let client = client_from_config(&config).await?;
let org = client.create_organization(&token, &args.name).await?;
config.org_id = Some(org.id.clone());
config.save()?;
if args.json {
output::json(&json!({ "organization": org }))?;
} else {
output::success(&format!("created organization `{}` ({})", org.name, org.id));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::types::Organization;
fn org(id: &str, name: &str) -> Organization {
serde_json::from_value(serde_json::json!({ "id": id, "name": name })).unwrap()
}
#[test]
fn org_use_matches_an_id_or_a_unique_name() {
let orgs = vec![
org("org_1", "Acme"),
org("org_2", "Beta"),
org("org_3", "beta"),
];
assert_eq!(find_org(&orgs, "org_1").unwrap().id, "org_1");
assert_eq!(find_org(&orgs, "acme").unwrap().id, "org_1");
assert!(find_org(&orgs, "Beta")
.unwrap_err()
.to_string()
.contains("more than one"));
assert!(find_org(&orgs, "nope")
.unwrap_err()
.to_string()
.contains("no organization"));
}
}