1use crate::cli::Cli;
4use crate::envelope::{ApiError, Envelope};
5use crate::error::Result;
6use serde_json::Value;
7
8pub struct View {
16 pub kind: &'static str,
18 pub host: String,
20 pub data: Value,
22 pub human: String,
24 pub hints: Option<Value>,
26 pub pre_rendered: bool,
29}
30
31impl View {
32 pub fn new(kind: &'static str, host: String, data: Value, human: String) -> Self {
34 View {
35 kind,
36 host,
37 data,
38 human,
39 hints: None,
40 pre_rendered: false,
41 }
42 }
43
44 #[must_use]
46 pub fn with_hints(mut self, hints: Value) -> Self {
47 self.hints = Some(hints);
48 self
49 }
50
51 #[must_use]
54 pub fn with_hints_opt(mut self, hints: Option<Value>) -> Self {
55 self.hints = hints;
56 self
57 }
58
59 #[must_use]
62 pub fn pre_rendered(mut self) -> Self {
63 self.pre_rendered = true;
64 self
65 }
66}
67
68pub fn render(cli: &Cli, result: Result<View>) -> i32 {
80 render_with_hints(cli, result, |_| None)
81}
82
83pub fn render_with_hints<F>(cli: &Cli, result: Result<View>, error_hints: F) -> i32
91where
92 F: FnOnce(&crate::error::FezError) -> Option<Value>,
93{
94 let host = cli.resolved_host();
95 match result {
96 Ok(view) => {
97 if view.pre_rendered {
98 return 0;
99 }
100 if cli.json {
101 let mut env = Envelope::ok(view.kind, &view.host, view.data);
102 if let Some(h) = view.hints {
103 env = env.with_hints(h);
104 }
105 println!("{}", env.to_json_string());
106 } else {
107 print!("{}", view.human);
108 }
109 0
110 }
111 Err(e) => {
112 if cli.json {
113 let mut env = Envelope::error(
114 "Error",
115 &host,
116 ApiError {
117 code: e.code().into(),
118 message: e.to_string(),
119 detail: e.detail(),
120 },
121 );
122 if let Some(h) = error_hints(&e) {
123 env = env.with_hints(h);
124 }
125 println!("{}", env.to_json_string());
126 } else {
127 eprintln!("error: {e}");
128 }
129 e.exit_code()
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::{render, render_with_hints, View};
137 use crate::cli::Cli;
138 use crate::error::FezError;
139 use clap::Parser;
140 use serde_json::json;
141
142 fn cli(args: &[&str]) -> Cli {
143 Cli::try_parse_from(args).expect("args parse")
144 }
145
146 #[test]
147 fn new_is_bare() {
148 let v = View::new("Kind", "host".into(), json!({"a": 1}), "human\n".into());
149 assert_eq!(v.kind, "Kind");
150 assert_eq!(v.host, "host");
151 assert_eq!(v.human, "human\n");
152 assert!(v.hints.is_none());
153 assert!(!v.pre_rendered);
154 }
155
156 #[test]
157 fn with_hints_sets_hints() {
158 let v = View::new("K", "h".into(), json!(null), String::new())
159 .with_hints(json!({"reverse": "fez x"}));
160 assert_eq!(v.hints.unwrap()["reverse"], "fez x");
161 }
162
163 #[test]
164 fn with_hints_opt_passes_through() {
165 let some = View::new("K", "h".into(), json!(null), String::new())
166 .with_hints_opt(Some(json!({"k": 1})));
167 assert!(some.hints.is_some());
168 let none = View::new("K", "h".into(), json!(null), String::new()).with_hints_opt(None);
169 assert!(none.hints.is_none());
170 }
171
172 #[test]
173 fn pre_rendered_marks_the_view() {
174 let v = View::new("K", "h".into(), json!(null), String::new()).pre_rendered();
175 assert!(v.pre_rendered);
176 }
177
178 #[test]
179 fn render_pre_rendered_view_is_silent_success() {
180 let c = cli(&["fez", "services", "list"]);
181 let v =
182 View::new("LogEntries", "localhost".into(), json!(null), String::new()).pre_rendered();
183 assert_eq!(render(&c, Ok(v)), 0);
184 }
185
186 #[test]
187 fn render_ok_human_returns_zero() {
188 let c = cli(&["fez", "services", "list"]);
189 let v = View::new(
190 "ServiceList",
191 "localhost".into(),
192 json!({"a": 1}),
193 "out\n".into(),
194 );
195 assert_eq!(render(&c, Ok(v)), 0);
196 }
197
198 #[test]
199 fn render_ok_json_with_hints_returns_zero() {
200 let c = cli(&["fez", "--json", "services", "stop", "x"]);
201 let v = View::new(
202 "Stopped",
203 "localhost".into(),
204 json!({"unit": "x"}),
205 "stopped\n".into(),
206 )
207 .with_hints(json!({"reverse": "fez services start x"}));
208 assert_eq!(render(&c, Ok(v)), 0);
209 }
210
211 #[test]
212 fn render_err_human_returns_error_exit_code() {
213 let c = cli(&["fez", "services", "status", "missing"]);
214 let exit = render(&c, Err(FezError::NotFound("missing".into())));
215 assert_eq!(exit, FezError::NotFound("missing".into()).exit_code());
216 }
217
218 #[test]
219 fn render_err_json_emits_detail_and_exit_code() {
220 let c = cli(&["fez", "--json", "packages", "list"]);
221 let err = FezError::DependencyMissing {
222 component: "dnf5daemon".into(),
223 dbus_name: "org.rpm.dnf.v0".into(),
224 remediation: "install dnf5daemon-server".into(),
225 };
226 let expected = err.exit_code();
227 assert_eq!(render(&c, Err(err)), expected);
228 }
229
230 #[test]
231 fn render_with_hints_runs_hook_on_error_and_returns_exit_code() {
232 let c = cli(&["fez", "--json", "firewall", "status"]);
233 let err = FezError::UnsupportedApi("getMasquerade".into());
234 let expected = err.exit_code();
235 let called = std::cell::Cell::new(false);
236 let exit = render_with_hints(&c, Err(err), |_| {
237 called.set(true);
238 Some(json!({"unsupported": "treat as unavailable"}))
239 });
240 assert_eq!(exit, expected);
241 assert!(called.get(), "hook runs on the error path");
242 }
243
244 #[test]
245 fn render_with_hints_skips_hook_on_success() {
246 let c = cli(&["fez", "--json", "firewall", "status"]);
247 let v = View::new(
248 "FirewallStatus",
249 "localhost".into(),
250 json!({}),
251 "ok\n".into(),
252 );
253 let called = std::cell::Cell::new(false);
254 let exit = render_with_hints(&c, Ok(v), |_| {
255 called.set(true);
256 None
257 });
258 assert_eq!(exit, 0);
259 assert!(!called.get(), "hook does not run on the success path");
260 }
261}
262
263pub mod services;
265
266pub mod packages;
268
269pub mod packages_pk;
271
272pub mod network;
274
275pub mod firewall;