use crate::cli::Cli;
use crate::envelope::{ApiError, Envelope};
use crate::error::Result;
use serde_json::Value;
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: String, data: Value, human: String) -> Self {
View {
kind,
host,
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 {
render_with_hints(cli, result, |_| None)
}
pub fn render_with_hints<F>(cli: &Cli, result: Result<View>, error_hints: F) -> i32
where
F: FnOnce(&crate::error::FezError) -> Option<Value>,
{
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) = error_hints(&e) {
env = env.with_hints(h);
}
println!("{}", env.to_json_string());
} else {
eprintln!("error: {e}");
}
e.exit_code()
}
}
}
#[cfg(test)]
mod tests {
use super::{render, render_with_hints, 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 new_is_bare() {
let v = View::new("Kind", "host".into(), 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".into(), 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".into(), json!(null), String::new())
.with_hints_opt(Some(json!({"k": 1})));
assert!(some.hints.is_some());
let none = View::new("K", "h".into(), 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".into(), 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".into(), 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".into(),
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".into(),
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_with_hints_runs_hook_on_error_and_returns_exit_code() {
let c = cli(&["fez", "--json", "firewall", "status"]);
let err = FezError::UnsupportedApi("getMasquerade".into());
let expected = err.exit_code();
let called = std::cell::Cell::new(false);
let exit = render_with_hints(&c, Err(err), |_| {
called.set(true);
Some(json!({"unsupported": "treat as unavailable"}))
});
assert_eq!(exit, expected);
assert!(called.get(), "hook runs on the error path");
}
#[test]
fn render_with_hints_skips_hook_on_success() {
let c = cli(&["fez", "--json", "firewall", "status"]);
let v = View::new(
"FirewallStatus",
"localhost".into(),
json!({}),
"ok\n".into(),
);
let called = std::cell::Cell::new(false);
let exit = render_with_hints(&c, Ok(v), |_| {
called.set(true);
None
});
assert_eq!(exit, 0);
assert!(!called.get(), "hook does not run on the success path");
}
}
pub mod services;
pub mod packages;
pub mod packages_pk;
pub mod network;
pub mod firewall;