]> git.proxmox.com Git - proxmox.git/blob - proxmox-rest-server/src/formatter.rs
tree-wide: run cargo fmt
[proxmox.git] / proxmox-rest-server / src / formatter.rs
1 //! Helpers to format response data
2 use std::collections::HashMap;
3
4 use anyhow::Error;
5 use serde_json::{json, Value};
6
7 use hyper::header;
8 use hyper::{Body, Response, StatusCode};
9
10 use proxmox_router::{HttpError, RpcEnvironment, SerializableReturn};
11 use proxmox_schema::ParameterError;
12
13 /// Extension to set error message for server side logging
14 pub(crate) struct ErrorMessageExtension(pub String);
15
16 /// Methods to format data and errors
17 pub trait OutputFormatter: Send + Sync {
18 /// Transform json data into a http response
19 fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body>;
20
21 /// Transform serializable data into a streaming http response
22 fn format_data_streaming(
23 &self,
24 data: Box<dyn SerializableReturn + Send>,
25 rpcenv: &dyn RpcEnvironment,
26 ) -> Result<Response<Body>, Error>;
27
28 /// Transform errors into a http response
29 fn format_error(&self, err: Error) -> Response<Body>;
30
31 /// Transform a [Result] into a http response
32 fn format_result(
33 &self,
34 result: Result<Value, Error>,
35 rpcenv: &dyn RpcEnvironment,
36 ) -> Response<Body> {
37 match result {
38 Ok(data) => self.format_data(data, rpcenv),
39 Err(err) => self.format_error(err),
40 }
41 }
42 }
43
44 static JSON_CONTENT_TYPE: &str = "application/json;charset=UTF-8";
45
46 fn json_data_response(data: Value) -> Response<Body> {
47 let json_str = data.to_string();
48
49 let raw = json_str.into_bytes();
50
51 let mut response = Response::new(raw.into());
52 response.headers_mut().insert(
53 header::CONTENT_TYPE,
54 header::HeaderValue::from_static(JSON_CONTENT_TYPE),
55 );
56
57 response
58 }
59
60 fn json_data_response_streaming(body: Body) -> Result<Response<Body>, Error> {
61 let response = Response::builder()
62 .header(
63 header::CONTENT_TYPE,
64 header::HeaderValue::from_static(JSON_CONTENT_TYPE),
65 )
66 .body(body)?;
67 Ok(response)
68 }
69
70 fn add_result_attributes(result: &mut Value, rpcenv: &dyn RpcEnvironment) {
71 let attributes = match rpcenv.result_attrib().as_object() {
72 Some(attr) => attr,
73 None => return,
74 };
75
76 for (key, value) in attributes {
77 result[key] = value.clone();
78 }
79 }
80
81 fn start_data_streaming(
82 value: Value,
83 data: Box<dyn SerializableReturn + Send>,
84 ) -> tokio::sync::mpsc::Receiver<Result<Vec<u8>, Error>> {
85 let (writer, reader) = tokio::sync::mpsc::channel(1);
86
87 tokio::task::spawn_blocking(move || {
88 let output = proxmox_async::blocking::SenderWriter::from_sender(writer);
89 let mut output = std::io::BufWriter::new(output);
90 let mut serializer = serde_json::Serializer::new(&mut output);
91 let _ = data.sender_serialize(&mut serializer, value);
92 });
93
94 reader
95 }
96
97 struct JsonFormatter();
98
99 /// Format data as ``application/json``
100 ///
101 /// The returned json object contains the following properties:
102 ///
103 /// * ``data``: The result data (on success)
104 ///
105 /// Any result attributes set on ``rpcenv`` are also added to the object.
106 ///
107 /// Errors generates a BAD_REQUEST containing the error
108 /// message as string.
109 pub static JSON_FORMATTER: &'static dyn OutputFormatter = &JsonFormatter();
110
111 impl OutputFormatter for JsonFormatter {
112 fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body> {
113 let mut result = json!({ "data": data });
114
115 add_result_attributes(&mut result, rpcenv);
116
117 json_data_response(result)
118 }
119
120 fn format_data_streaming(
121 &self,
122 data: Box<dyn SerializableReturn + Send>,
123 rpcenv: &dyn RpcEnvironment,
124 ) -> Result<Response<Body>, Error> {
125 let mut value = json!({});
126
127 add_result_attributes(&mut value, rpcenv);
128
129 let reader = start_data_streaming(value, data);
130 let stream = tokio_stream::wrappers::ReceiverStream::new(reader);
131
132 json_data_response_streaming(Body::wrap_stream(stream))
133 }
134
135 fn format_error(&self, err: Error) -> Response<Body> {
136 error_to_response(err)
137 }
138 }
139
140 pub(crate) fn error_to_response(err: Error) -> Response<Body> {
141 let mut response = if let Some(apierr) = err.downcast_ref::<HttpError>() {
142 let mut resp = Response::new(Body::from(apierr.message.clone()));
143 *resp.status_mut() = apierr.code;
144 resp
145 } else {
146 let mut resp = Response::new(Body::from(err.to_string()));
147 *resp.status_mut() = StatusCode::BAD_REQUEST;
148 resp
149 };
150
151 response.headers_mut().insert(
152 header::CONTENT_TYPE,
153 header::HeaderValue::from_static(JSON_CONTENT_TYPE),
154 );
155
156 response
157 .extensions_mut()
158 .insert(ErrorMessageExtension(err.to_string()));
159
160 response
161 }
162
163 /// Format data as ExtJS compatible ``application/json``
164 ///
165 /// The returned json object contains the following properties:
166 ///
167 /// * ``success``: boolean attribute indicating the success.
168 ///
169 /// * ``status``: api call status code.
170 ///
171 /// * ``data``: The result data (on success)
172 ///
173 /// * ``message``: The error message (on failure)
174 ///
175 /// * ``errors``: detailed list of errors (if available)
176 ///
177 /// Any result attributes set on ``rpcenv`` are also added to the object.
178 ///
179 /// Please note that errors return a HTTP response with status code OK, but setting success
180 /// to false. The real status from the API call is encoded in the status
181 /// property.
182 pub static EXTJS_FORMATTER: &'static dyn OutputFormatter = &ExtJsFormatter();
183
184 struct ExtJsFormatter();
185
186 impl OutputFormatter for ExtJsFormatter {
187 fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body> {
188 let mut result = json!({
189 "data": data,
190 "success": true,
191 "status": StatusCode::OK.as_u16(),
192 });
193
194 add_result_attributes(&mut result, rpcenv);
195
196 json_data_response(result)
197 }
198
199 fn format_data_streaming(
200 &self,
201 data: Box<dyn SerializableReturn + Send>,
202 rpcenv: &dyn RpcEnvironment,
203 ) -> Result<Response<Body>, Error> {
204 let mut value = json!({
205 "success": true,
206 "status": StatusCode::OK.as_u16(),
207 });
208
209 add_result_attributes(&mut value, rpcenv);
210
211 let reader = start_data_streaming(value, data);
212 let stream = tokio_stream::wrappers::ReceiverStream::new(reader);
213
214 json_data_response_streaming(Body::wrap_stream(stream))
215 }
216
217 fn format_error(&self, err: Error) -> Response<Body> {
218 let mut errors = HashMap::new();
219
220 let (message, status) = if err.is::<ParameterError>() {
221 match err.downcast::<ParameterError>() {
222 Ok(param_err) => {
223 for (name, err) in param_err {
224 errors.insert(name, err.to_string());
225 }
226 (
227 String::from("parameter verification errors"),
228 StatusCode::BAD_REQUEST,
229 )
230 }
231 Err(err) => (err.to_string(), StatusCode::BAD_REQUEST),
232 }
233 } else {
234 let status = if let Some(apierr) = err.downcast_ref::<HttpError>() {
235 apierr.code
236 } else {
237 StatusCode::BAD_REQUEST
238 };
239 (err.to_string(), status)
240 };
241
242 let result = json!({
243 "message": message,
244 "errors": errors,
245 "success": false,
246 "status": status.as_u16(),
247 });
248
249 let mut response = json_data_response(result);
250
251 response
252 .extensions_mut()
253 .insert(ErrorMessageExtension(message));
254
255 response
256 }
257 }