use crate::commands::tenants::admin_request;
use clap::Subcommand;
use reqwest::Method;
use serde_json::{json, Value};
#[derive(Subcommand)]
pub enum ExtAction {
List {
#[arg(long)]
tenant: String,
},
Register {
name: String,
#[arg(long)]
tenant: String,
#[arg(long = "caller", required = true)]
callers: Vec<String>,
#[arg(long)]
force: bool,
},
Unregister {
name: String,
#[arg(long)]
tenant: String,
#[arg(long)]
force: bool,
},
}
#[derive(Debug, PartialEq)]
struct ExtRequest {
what: &'static str,
method: Method,
path: String,
body: Option<Value>,
}
fn extensions_path(tenant: &str) -> String {
format!("/api/tenants/{tenant}/extensions")
}
fn extension_path(tenant: &str, name: &str, force: bool) -> String {
let path = format!("{}/{name}", extensions_path(tenant));
if force {
format!("{path}?force=true")
} else {
path
}
}
fn request(action: &ExtAction) -> ExtRequest {
match action {
ExtAction::List { tenant } => ExtRequest {
what: "List extensions",
method: Method::GET,
path: extensions_path(tenant),
body: None,
},
ExtAction::Register {
name,
tenant,
callers,
force,
} => ExtRequest {
what: "Register extension",
method: Method::PUT,
path: extension_path(tenant, name, *force),
body: Some(json!({ "callers": callers })),
},
ExtAction::Unregister {
name,
tenant,
force,
} => ExtRequest {
what: "Unregister extension",
method: Method::DELETE,
path: extension_path(tenant, name, *force),
body: None,
},
}
}
fn callers(extension: &Value) -> String {
extension["callers"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join(", ")
}
fn registration_rows(listing: &Value) -> Vec<[String; 4]> {
listing["extensions"]
.as_array()
.into_iter()
.flatten()
.map(|extension| {
let text = |field: &str| extension[field].as_str().unwrap_or("-").to_string();
[
text("name"),
text("backend"),
callers(extension),
text("service_id"),
]
})
.collect()
}
pub async fn run(action: ExtAction, api_url: Option<&str>, env: Option<&str>) -> eyre::Result<()> {
let ExtRequest {
what,
method,
path,
body,
} = request(&action);
let answer = admin_request(what, method, &path, body, api_url, env).await?;
match action {
ExtAction::List { tenant } => {
let rows = registration_rows(&answer);
if rows.is_empty() {
println!("Tenant '{tenant}' registers no extensions");
} else {
let mut table =
crate::commands::listing(&["NAME", "BACKEND", "CALLERS", "SERVICE"]);
rows.into_iter().for_each(|row| {
table.add_row(row);
});
println!("{table}");
}
}
ExtAction::Register { name, tenant, .. } => println!(
"Registered extension '{name}' for tenant '{tenant}' as a NATS provider (callers: {})",
callers(&answer["extension"])
),
ExtAction::Unregister { name, tenant, .. } => {
println!("Removed extension '{name}' from tenant '{tenant}'");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
struct Ext {
#[command(subcommand)]
action: ExtAction,
}
fn requested(args: &[&str]) -> ExtRequest {
let parsed = Ext::try_parse_from(["ext"].iter().chain(args)).expect("parsed");
request(&parsed.action)
}
#[test]
fn listing_asks_for_every_registration_of_the_tenant() {
assert_eq!(
requested(&["list", "--tenant", "acme"]),
ExtRequest {
what: "List extensions",
method: Method::GET,
path: "/api/tenants/acme/extensions".into(),
body: None,
}
);
}
#[test]
fn registering_puts_the_callers_and_forces_only_when_asked() {
assert_eq!(
requested(&[
"register",
"google.token",
"--tenant",
"acme",
"--caller",
"reports",
"--caller",
"billing",
]),
ExtRequest {
what: "Register extension",
method: Method::PUT,
path: "/api/tenants/acme/extensions/google.token".into(),
body: Some(json!({"callers": ["reports", "billing"]})),
}
);
assert_eq!(
requested(&[
"register",
"google.token",
"--tenant",
"acme",
"--caller",
"*",
"--force",
])
.path,
"/api/tenants/acme/extensions/google.token?force=true"
);
assert!(
Ext::try_parse_from(["ext", "register", "google.token", "--tenant", "acme"]).is_err(),
"a caller is required"
);
}
#[test]
fn unregistering_deletes_the_extension_and_forces_only_when_asked() {
assert_eq!(
requested(&["unregister", "google.token", "--tenant", "acme"]),
ExtRequest {
what: "Unregister extension",
method: Method::DELETE,
path: "/api/tenants/acme/extensions/google.token".into(),
body: None,
}
);
assert_eq!(
requested(&["unregister", "google.token", "--tenant", "acme", "--force"]).path,
"/api/tenants/acme/extensions/google.token?force=true"
);
}
#[test]
fn a_registration_is_listed_by_name_backend_callers_and_service() {
assert_eq!(
registration_rows(&json!({"extensions": [
{"name": "google.token", "backend": "nats", "callers": ["reports", "billing"], "service_id": null},
{"name": "pdf.render", "backend": "native", "callers": ["*"], "service_id": "7d1c"},
]})),
[
[
"google.token".to_string(),
"nats".into(),
"reports, billing".into(),
"-".into()
],
[
"pdf.render".to_string(),
"native".into(),
"*".into(),
"7d1c".into()
],
]
);
}
}