1use serde::Serialize;
3use serde_json::Value;
4
5pub const API_VERSION: &str = "fez/v1";
7
8pub fn table_data(columns: &[&str], rows: Vec<Value>) -> Value {
21 let count = rows.len();
22 serde_json::json!({
23 "columns": columns,
24 "rows": rows,
25 "count": count,
26 })
27}
28
29#[derive(Serialize)]
31pub struct Envelope {
32 #[serde(rename = "apiVersion")]
34 pub api_version: &'static str,
35 pub kind: String,
37 pub host: String,
39 pub status: Status,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub data: Option<Value>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub error: Option<ApiError>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub hints: Option<Value>,
50}
51
52#[derive(Serialize, Clone, Copy)]
54#[serde(rename_all = "lowercase")]
55pub enum Status {
56 Ok,
58 Error,
60}
61
62#[derive(Serialize)]
64pub struct ApiError {
65 pub code: String,
67 pub message: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub detail: Option<Value>,
72}
73
74impl Envelope {
75 pub fn ok(kind: &str, host: &str, data: Value) -> Self {
77 Envelope {
78 api_version: API_VERSION,
79 kind: kind.into(),
80 host: host.into(),
81 status: Status::Ok,
82 data: Some(data),
83 error: None,
84 hints: None,
85 }
86 }
87 pub fn error(kind: &str, host: &str, err: ApiError) -> Self {
89 Envelope {
90 api_version: API_VERSION,
91 kind: kind.into(),
92 host: host.into(),
93 status: Status::Error,
94 data: None,
95 error: Some(err),
96 hints: None,
97 }
98 }
99 pub fn with_hints(mut self, hints: Value) -> Self {
101 self.hints = Some(hints);
102 self
103 }
104 pub fn to_json_string(&self) -> String {
109 serde_json::to_string(self).unwrap_or_else(|_| {
110 r#"{"apiVersion":"fez/v1","kind":"Error","host":"","status":"error","error":{"code":"internal","message":"envelope serialization failed"}}"#
111 .to_string()
112 })
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use serde_json::json;
120
121 #[test]
122 fn ok_envelope_shape() {
123 let e = Envelope::ok("Sample", "localhost", json!({"k":"v"}));
124 assert_eq!(
125 serde_json::to_value(&e).unwrap(),
126 json!({
127 "apiVersion":"fez/v1","kind":"Sample","host":"localhost",
128 "status":"ok","data":{"k":"v"}
129 })
130 );
131 }
132
133 #[test]
134 fn error_envelope_shape() {
135 let e = Envelope::error(
136 "Error",
137 "h1",
138 ApiError {
139 code: "not-found".into(),
140 message: "no unit".into(),
141 detail: None,
142 },
143 );
144 assert_eq!(
145 serde_json::to_value(&e).unwrap(),
146 json!({
147 "apiVersion":"fez/v1","kind":"Error","host":"h1",
148 "status":"error","error":{"code":"not-found","message":"no unit"}
149 })
150 );
151 }
152
153 #[test]
154 fn table_data_projects_columns_rows_count() {
155 let td = table_data(
156 &["name", "size"],
157 vec![json!(["bash", 7340032]), json!(["htop", 245760])],
158 );
159 assert_eq!(
160 td,
161 json!({
162 "columns": ["name", "size"],
163 "rows": [["bash", 7340032], ["htop", 245760]],
164 "count": 2
165 })
166 );
167 assert!(td["rows"][0][1].is_i64() || td["rows"][0][1].is_u64());
169 }
170
171 #[test]
172 fn table_data_empty_has_zero_count() {
173 let td = table_data(&["name"], vec![]);
174 assert_eq!(td, json!({"columns": ["name"], "rows": [], "count": 0}));
175 }
176
177 #[test]
178 fn ok_envelope_with_hints() {
179 let e = Envelope::ok(
180 "ServiceMutation",
181 "localhost",
182 json!({"unit": "nginx.service"}),
183 )
184 .with_hints(json!({"reverse": "fez services start nginx.service"}));
185 assert_eq!(
186 serde_json::to_value(&e).unwrap(),
187 json!({
188 "apiVersion":"fez/v1","kind":"ServiceMutation","host":"localhost",
189 "status":"ok","data":{"unit":"nginx.service"},
190 "hints":{"reverse":"fez services start nginx.service"}
191 })
192 );
193 }
194}