use crate::cli::Cli;
use crate::envelope::{ApiError, Envelope};
use crate::error::{FezError, Result};
use crate::protocol::client::BridgeClient;
use serde_json::Value;
pub fn connect(cli: &Cli) -> Result<BridgeClient> {
let transport = crate::transport::from_host(cli.host.as_deref());
BridgeClient::connect(transport.as_ref())
}
pub fn map_service_unknown<T>(res: Result<T>, missing: impl FnOnce() -> FezError) -> Result<T> {
match res {
Err(FezError::Dbus { name, .. }) if crate::error::is_service_unknown(&name) => {
Err(missing())
}
other => other,
}
}
pub(crate) struct CapabilityContext<'a> {
pub(crate) client: &'a mut BridgeClient,
pub(crate) channel: &'a str,
pub(crate) host: &'a str,
}
pub struct View {
pub kind: &'static str,
pub host: String,
pub data: Value,
pub human: String,
pub hints: Option<Value>,
pub pre_rendered: bool,
}
impl View {
pub fn new(kind: &'static str, host: impl Into<String>, data: Value, human: String) -> Self {
View {
kind,
host: host.into(),
data,
human,
hints: None,
pre_rendered: false,
}
}
#[must_use]
pub fn with_hints(mut self, hints: Value) -> Self {
self.hints = Some(hints);
self
}
#[must_use]
pub fn with_hints_opt(mut self, hints: Option<Value>) -> Self {
self.hints = hints;
self
}
#[must_use]
pub fn pre_rendered(mut self) -> Self {
self.pre_rendered = true;
self
}
}
pub fn render(cli: &Cli, result: Result<View>) -> i32 {
let host = cli.resolved_host();
match result {
Ok(view) => {
if view.pre_rendered {
return 0;
}
if cli.json {
let mut env = Envelope::ok(view.kind, &view.host, view.data);
if let Some(h) = view.hints {
env = env.with_hints(h);
}
println!("{}", env.to_json_string());
} else {
print!("{}", view.human);
}
0
}
Err(e) => {
if cli.json {
let mut env = Envelope::error(
"Error",
&host,
ApiError {
code: e.code().into(),
message: e.to_string(),
detail: e.detail(),
},
);
if let Some(h) = e.hints() {
env = env.with_hints(h);
}
println!("{}", env.to_json_string());
} else {
eprintln!("error: {e}");
}
e.exit_code()
}
}
}
#[deprecated(since = "0.0.0", note = "use render(); hints are on FezError::hints()")]
#[allow(dead_code)]
pub fn render_with_hints<F>(cli: &Cli, result: Result<View>, _error_hints: F) -> i32
where
F: FnOnce(&crate::error::FezError) -> Option<Value>,
{
render(cli, result)
}
#[cfg(test)]
mod tests {
use super::{render, View};
use crate::cli::Cli;
use crate::error::FezError;
use clap::Parser;
use serde_json::json;
fn cli(args: &[&str]) -> Cli {
Cli::try_parse_from(args).expect("args parse")
}
#[test]
fn map_service_unknown_maps_only_service_unknown_dbus_errors() {
let mapped = super::map_service_unknown::<()>(
Err(FezError::Dbus {
name: "org.freedesktop.DBus.Error.ServiceUnknown".into(),
message: "gone".into(),
}),
|| FezError::NotFound("daemon".into()),
);
assert!(matches!(mapped, Err(FezError::NotFound(_))));
let other = super::map_service_unknown::<()>(
Err(FezError::Dbus {
name: "org.freedesktop.DBus.Error.AccessDenied".into(),
message: "no".into(),
}),
|| FezError::NotFound("daemon".into()),
);
assert!(matches!(other, Err(FezError::Dbus { .. })));
assert!(super::map_service_unknown(Ok(7), || FezError::NotFound("x".into())).is_ok());
}
#[test]
fn new_is_bare() {
let v = View::new("Kind", "host", json!({"a": 1}), "human\n".into());
assert_eq!(v.kind, "Kind");
assert_eq!(v.host, "host");
assert_eq!(v.human, "human\n");
assert!(v.hints.is_none());
assert!(!v.pre_rendered);
}
#[test]
fn with_hints_sets_hints() {
let v =
View::new("K", "h", json!(null), String::new()).with_hints(json!({"reverse": "fez x"}));
assert_eq!(v.hints.unwrap()["reverse"], "fez x");
}
#[test]
fn with_hints_opt_passes_through() {
let some =
View::new("K", "h", json!(null), String::new()).with_hints_opt(Some(json!({"k": 1})));
assert!(some.hints.is_some());
let none = View::new("K", "h", json!(null), String::new()).with_hints_opt(None);
assert!(none.hints.is_none());
}
#[test]
fn pre_rendered_marks_the_view() {
let v = View::new("K", "h", json!(null), String::new()).pre_rendered();
assert!(v.pre_rendered);
}
#[test]
fn render_pre_rendered_view_is_silent_success() {
let c = cli(&["fez", "services", "list"]);
let v = View::new("LogEntries", "localhost", json!(null), String::new()).pre_rendered();
assert_eq!(render(&c, Ok(v)), 0);
}
#[test]
fn render_ok_human_returns_zero() {
let c = cli(&["fez", "services", "list"]);
let v = View::new("ServiceList", "localhost", json!({"a": 1}), "out\n".into());
assert_eq!(render(&c, Ok(v)), 0);
}
#[test]
fn render_ok_json_with_hints_returns_zero() {
let c = cli(&["fez", "--json", "services", "stop", "x"]);
let v = View::new(
"Stopped",
"localhost",
json!({"unit": "x"}),
"stopped\n".into(),
)
.with_hints(json!({"reverse": "fez services start x"}));
assert_eq!(render(&c, Ok(v)), 0);
}
#[test]
fn render_err_human_returns_error_exit_code() {
let c = cli(&["fez", "services", "status", "missing"]);
let exit = render(&c, Err(FezError::NotFound("missing".into())));
assert_eq!(exit, FezError::NotFound("missing".into()).exit_code());
}
#[test]
fn render_err_json_emits_detail_and_exit_code() {
let c = cli(&["fez", "--json", "packages", "list"]);
let err = FezError::DependencyMissing {
component: "dnf5daemon".into(),
dbus_name: "org.rpm.dnf.v0".into(),
remediation: "install dnf5daemon-server".into(),
};
let expected = err.exit_code();
assert_eq!(render(&c, Err(err)), expected);
}
#[test]
fn render_dependency_missing_emits_hints_from_error() {
let c = cli(&["fez", "--json", "firewall", "status"]);
let err = FezError::DependencyMissing {
component: "firewalld".into(),
dbus_name: "org.fedoraproject.FirewallD1".into(),
remediation: "dnf install firewalld".into(),
};
let expected = err.exit_code();
assert_eq!(render(&c, Err(err)), expected);
}
#[test]
fn render_unsupported_api_emits_hints_from_error() {
let c = cli(&["fez", "--json", "firewall", "status"]);
let err = FezError::UnsupportedApi("getMasquerade".into());
let expected = err.exit_code();
assert_eq!(render(&c, Err(err)), expected);
}
#[test]
fn render_success_does_not_add_hints_from_error() {
let c = cli(&["fez", "--json", "firewall", "status"]);
let v = View::new("FirewallStatus", "localhost", json!({}), "ok\n".into());
assert_eq!(render(&c, Ok(v)), 0);
}
}
pub mod services;
pub mod package;
pub mod network;
pub mod firewall;