proxmox-rest-server: cleanup formatter, improve docs

Use trait for OutputFormatter. This is functionally equivalent,
but more rust-like...
This commit is contained in:
Dietmar Maurer 2021-09-27 12:59:06 +02:00
parent 8a23ea4656
commit 53daae8e89
8 changed files with 128 additions and 98 deletions

View File

@ -1,4 +1,4 @@
//! Helpers for daemons/services.
//! Helpers to implement restartable daemons/services.
use std::ffi::CString;
use std::future::Future;
@ -351,6 +351,7 @@ extern "C" {
fn sd_notify(unset_environment: c_int, state: *const c_char) -> c_int;
}
/// Systemd sercice startup states (see: ``man sd_notify``)
pub enum SystemdNotify {
Ready,
Reloading,
@ -359,6 +360,7 @@ pub enum SystemdNotify {
MainPid(nix::unistd::Pid),
}
/// Tells systemd the startup state of the service (see: ``man sd_notify``)
pub fn systemd_notify(state: SystemdNotify) -> Result<(), Error> {
let message = match state {
SystemdNotify::Ready => CString::new("READY=1"),

View File

@ -1,3 +1,5 @@
//! Helpers to format response data
use anyhow::{Error};
use serde_json::{json, Value};
@ -7,25 +9,28 @@ use hyper::header;
use proxmox::api::{HttpError, RpcEnvironment};
/// Extension to set error message for server side logging
pub struct ErrorMessageExtension(pub String);
pub(crate) struct ErrorMessageExtension(pub String);
pub struct OutputFormatter {
/// Methods to format data and errors
pub trait OutputFormatter: Send + Sync {
/// Transform json data into a http response
fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body>;
pub format_data: fn(data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body>,
/// Transform errors into a http response
fn format_error(&self, err: Error) -> Response<Body>;
pub format_error: fn(err: Error) -> Response<Body>,
/// Transform a [Result] into a http response
fn format_result(&self, result: Result<Value, Error>, rpcenv: &dyn RpcEnvironment) -> Response<Body> {
match result {
Ok(data) => self.format_data(data, rpcenv),
Err(err) => self.format_error(err),
}
}
}
static JSON_CONTENT_TYPE: &str = "application/json;charset=UTF-8";
pub fn json_response(result: Result<Value, Error>) -> Response<Body> {
match result {
Ok(data) => json_data_response(data),
Err(err) => json_error_response(err),
}
}
pub fn json_data_response(data: Value) -> Response<Body> {
fn json_data_response(data: Value) -> Response<Body> {
let json_str = data.to_string();
@ -51,76 +56,101 @@ fn add_result_attributes(result: &mut Value, rpcenv: &dyn RpcEnvironment)
}
}
fn json_format_data(data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body> {
let mut result = json!({
"data": data
});
struct JsonFormatter();
add_result_attributes(&mut result, rpcenv);
/// Format data as ``application/json``
///
/// Errors generates a BAD_REQUEST containing the error
/// message as string.
pub static JSON_FORMATTER: &'static dyn OutputFormatter = &JsonFormatter();
json_data_response(result)
impl OutputFormatter for JsonFormatter {
fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body> {
let mut result = json!({
"data": data
});
add_result_attributes(&mut result, rpcenv);
json_data_response(result)
}
fn format_error(&self, err: Error) -> Response<Body> {
let mut response = if let Some(apierr) = err.downcast_ref::<HttpError>() {
let mut resp = Response::new(Body::from(apierr.message.clone()));
*resp.status_mut() = apierr.code;
resp
} else {
let mut resp = Response::new(Body::from(err.to_string()));
*resp.status_mut() = StatusCode::BAD_REQUEST;
resp
};
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(JSON_CONTENT_TYPE));
response.extensions_mut().insert(ErrorMessageExtension(err.to_string()));
response
}
}
pub fn json_error_response(err: Error) -> Response<Body> {
/// Format data as ExtJS compatible ``application/json``
///
/// The returned json object contains the following properties:
///
/// * ``success``: boolean attribute indicating the success.
///
/// * ``data``: The result data (on success)
///
/// * ``message``: The error message (on failure)
///
/// * ``errors``: detailed list of errors (if available)
///
/// Any result attributes set on ``rpcenv`` are also added to the object.
///
/// Please note that errors return status code OK, but setting success
/// to false.
pub static EXTJS_FORMATTER: &'static dyn OutputFormatter = &ExtJsFormatter();
let mut response = if let Some(apierr) = err.downcast_ref::<HttpError>() {
let mut resp = Response::new(Body::from(apierr.message.clone()));
*resp.status_mut() = apierr.code;
resp
} else {
let mut resp = Response::new(Body::from(err.to_string()));
*resp.status_mut() = StatusCode::BAD_REQUEST;
resp
};
struct ExtJsFormatter();
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(JSON_CONTENT_TYPE));
impl OutputFormatter for ExtJsFormatter {
response.extensions_mut().insert(ErrorMessageExtension(err.to_string()));
fn format_data(&self, data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body> {
response
let mut result = json!({
"data": data,
"success": true
});
add_result_attributes(&mut result, rpcenv);
json_data_response(result)
}
fn format_error(&self, err: Error) -> Response<Body> {
let mut errors = vec![];
let message = err.to_string();
errors.push(&message);
let result = json!({
"message": message,
"errors": errors,
"success": false
});
let mut response = json_data_response(result);
response.extensions_mut().insert(ErrorMessageExtension(message));
response
}
}
pub static JSON_FORMATTER: OutputFormatter = OutputFormatter {
format_data: json_format_data,
format_error: json_error_response,
};
fn extjs_format_data(data: Value, rpcenv: &dyn RpcEnvironment) -> Response<Body> {
let mut result = json!({
"data": data,
"success": true
});
add_result_attributes(&mut result, rpcenv);
json_data_response(result)
}
fn extjs_format_error(err: Error) -> Response<Body> {
let mut errors = vec![];
let message = err.to_string();
errors.push(&message);
let result = json!({
"message": message,
"errors": errors,
"success": false
});
let mut response = json_data_response(result);
response.extensions_mut().insert(ErrorMessageExtension(message));
response
}
pub static EXTJS_FORMATTER: OutputFormatter = OutputFormatter {
format_data: extjs_format_data,
format_error: extjs_format_error,
};

View File

@ -51,12 +51,12 @@ impl <E: RpcEnvironment + Clone> H2Service<E> {
let mut uri_param = HashMap::new();
let formatter = &JSON_FORMATTER;
let formatter = JSON_FORMATTER;
match self.router.find_method(&components, method, &mut uri_param) {
None => {
let err = http_err!(NOT_FOUND, "Path '{}' not found.", path);
future::ok((formatter.format_error)(err)).boxed()
future::ok(formatter.format_error(err)).boxed()
}
Some(api_method) => {
crate::rest::handle_api_request(

View File

@ -12,6 +12,7 @@ mod compression;
pub use compression::*;
pub mod daemon;
pub mod formatter;
mod environment;

View File

@ -391,7 +391,7 @@ async fn proxy_protected_request(
pub(crate) async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher + Send>(
mut rpcenv: Env,
info: &'static ApiMethod,
formatter: &'static OutputFormatter,
formatter: &'static dyn OutputFormatter,
parts: Parts,
req_body: Body,
uri_param: HashMap<String, String, S>,
@ -407,14 +407,14 @@ pub(crate) async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHa
ApiHandler::Sync(handler) => {
let params =
get_request_parameters(info.parameters, parts, req_body, uri_param).await?;
(handler)(params, info, &mut rpcenv).map(|data| (formatter.format_data)(data, &rpcenv))
(handler)(params, info, &mut rpcenv).map(|data| formatter.format_data(data, &rpcenv))
}
ApiHandler::Async(handler) => {
let params =
get_request_parameters(info.parameters, parts, req_body, uri_param).await?;
(handler)(params, info, &mut rpcenv)
.await
.map(|data| (formatter.format_data)(data, &rpcenv))
.map(|data| formatter.format_data(data, &rpcenv))
}
};
@ -426,7 +426,7 @@ pub(crate) async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHa
tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await;
}
}
(formatter.format_error)(err)
formatter.format_error(err)
}
};
@ -627,9 +627,9 @@ async fn handle_request(
if comp_len >= 2 {
let format = components[1];
let formatter = match format {
"json" => &JSON_FORMATTER,
"extjs" => &EXTJS_FORMATTER,
let formatter: &dyn OutputFormatter = match format {
"json" => JSON_FORMATTER,
"extjs" => EXTJS_FORMATTER,
_ => bail!("Unsupported output format '{}'.", format),
};
@ -664,7 +664,7 @@ async fn handle_request(
// always delay unauthorized calls by 3 seconds (from start of request)
let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err);
tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await;
return Ok((formatter.format_error)(err));
return Ok(formatter.format_error(err));
}
}
}
@ -672,7 +672,7 @@ async fn handle_request(
match api_method {
None => {
let err = http_err!(NOT_FOUND, "Path '{}' not found.", path);
return Ok((formatter.format_error)(err));
return Ok(formatter.format_error(err));
}
Some(api_method) => {
let auth_id = rpcenv.get_auth_id();
@ -686,7 +686,7 @@ async fn handle_request(
) {
let err = http_err!(FORBIDDEN, "permission check failed");
tokio::time::sleep_until(Instant::from_std(access_forbidden_time)).await;
return Ok((formatter.format_error)(err));
return Ok(formatter.format_error(err));
}
let result = if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
@ -698,7 +698,7 @@ async fn handle_request(
let mut response = match result {
Ok(resp) => resp,
Err(err) => (formatter.format_error)(err),
Err(err) => formatter.format_error(err),
};
if let Some(auth_id) = auth_id {

View File

@ -1332,7 +1332,7 @@ pub fn upload_backup_log(
replace_file(&path, blob.raw_data(), CreateOptions::new())?;
// fixme: use correct formatter
Ok(formatter::json_response(Ok(Value::Null)))
Ok(formatter::JSON_FORMATTER.format_data(Value::Null, &*rpcenv))
}.boxed()
}

View File

@ -111,7 +111,7 @@ pub struct BackupEnvironment {
result_attributes: Value,
auth_id: Authid,
pub debug: bool,
pub formatter: &'static OutputFormatter,
pub formatter: &'static dyn OutputFormatter,
pub worker: Arc<WorkerTask>,
pub datastore: Arc<DataStore>,
pub backup_dir: BackupDir,
@ -146,7 +146,7 @@ impl BackupEnvironment {
worker,
datastore,
debug: false,
formatter: &JSON_FORMATTER,
formatter: JSON_FORMATTER,
backup_dir,
last_backup: None,
state: Arc::new(Mutex::new(state)),
@ -556,10 +556,7 @@ impl BackupEnvironment {
}
pub fn format_response(&self, result: Result<Value, Error>) -> Response<Body> {
match result {
Ok(data) => (self.formatter.format_data)(data, self),
Err(err) => (self.formatter.format_error)(err),
}
self.formatter.format_result(result, self)
}
/// Raise error if finished flag is not set

View File

@ -18,7 +18,7 @@ pub struct ReaderEnvironment {
result_attributes: Value,
auth_id: Authid,
pub debug: bool,
pub formatter: &'static OutputFormatter,
pub formatter: &'static dyn OutputFormatter,
pub worker: Arc<WorkerTask>,
pub datastore: Arc<DataStore>,
pub backup_dir: BackupDir,
@ -42,7 +42,7 @@ impl ReaderEnvironment {
worker,
datastore,
debug: false,
formatter: &JSON_FORMATTER,
formatter: JSON_FORMATTER,
backup_dir,
allowed_chunks: Arc::new(RwLock::new(HashSet::new())),
}