1use serde::Serialize;
3use serde_json::Value;
4
5pub const API_VERSION: &str = "fez/v1";
7
8#[derive(Serialize)]
10pub struct Envelope {
11 #[serde(rename = "apiVersion")]
13 pub api_version: &'static str,
14 pub kind: String,
16 pub host: String,
18 pub status: Status,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub data: Option<Value>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub error: Option<ApiError>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub hints: Option<Value>,
29}
30
31#[derive(Serialize, Clone, Copy)]
33#[serde(rename_all = "lowercase")]
34pub enum Status {
35 Ok,
37 Error,
39}
40
41#[derive(Serialize)]
43pub struct ApiError {
44 pub code: String,
46 pub message: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub detail: Option<Value>,
51}
52
53impl Envelope {
54 pub fn ok(kind: &str, host: &str, data: Value) -> Self {
56 Envelope {
57 api_version: API_VERSION,
58 kind: kind.into(),
59 host: host.into(),
60 status: Status::Ok,
61 data: Some(data),
62 error: None,
63 hints: None,
64 }
65 }
66 pub fn error(kind: &str, host: &str, err: ApiError) -> Self {
68 Envelope {
69 api_version: API_VERSION,
70 kind: kind.into(),
71 host: host.into(),
72 status: Status::Error,
73 data: None,
74 error: Some(err),
75 hints: None,
76 }
77 }
78 pub fn with_hints(mut self, hints: Value) -> Self {
80 self.hints = Some(hints);
81 self
82 }
83 pub fn to_json_string(&self) -> String {
88 serde_json::to_string_pretty(self).unwrap_or_else(|_| {
89 r#"{
90 "apiVersion": "fez/v1",
91 "kind": "Error",
92 "host": "",
93 "status": "error",
94 "error": {
95 "code": "internal",
96 "message": "envelope serialization failed"
97 }
98}"#
99 .to_string()
100 })
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use serde_json::json;
108
109 #[test]
110 fn ok_envelope_shape() {
111 let e = Envelope::ok("ServiceList", "localhost", json!({"units":[]}));
112 assert_eq!(
113 serde_json::to_value(&e).unwrap(),
114 json!({
115 "apiVersion":"fez/v1","kind":"ServiceList","host":"localhost",
116 "status":"ok","data":{"units":[]}
117 })
118 );
119 }
120
121 #[test]
122 fn error_envelope_shape() {
123 let e = Envelope::error(
124 "Error",
125 "h1",
126 ApiError {
127 code: "not-found".into(),
128 message: "no unit".into(),
129 detail: None,
130 },
131 );
132 assert_eq!(
133 serde_json::to_value(&e).unwrap(),
134 json!({
135 "apiVersion":"fez/v1","kind":"Error","host":"h1",
136 "status":"error","error":{"code":"not-found","message":"no unit"}
137 })
138 );
139 }
140
141 #[test]
142 fn ok_envelope_with_hints() {
143 let e = Envelope::ok(
144 "ServiceMutation",
145 "localhost",
146 json!({"unit": "nginx.service"}),
147 )
148 .with_hints(json!({"reverse": "fez services start nginx.service"}));
149 assert_eq!(
150 serde_json::to_value(&e).unwrap(),
151 json!({
152 "apiVersion":"fez/v1","kind":"ServiceMutation","host":"localhost",
153 "status":"ok","data":{"unit":"nginx.service"},
154 "hints":{"reverse":"fez services start nginx.service"}
155 })
156 );
157 }
158}