api: add format module

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-11-22 09:28:22 +01:00
parent a6ce1e432b
commit f73ebb7209
3 changed files with 287 additions and 0 deletions

View File

@ -15,6 +15,7 @@ regex = "1.0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
textwrap = "0.11"
url = "1.7"
[dev-dependencies]

285
proxmox-api/src/format.rs Normal file
View File

@ -0,0 +1,285 @@
use failure::*;
use std::io::Write;
use super::router::{Router, SubRoute};
use super::schema::*;
use super::{ApiHandler, ApiMethod};
#[derive(Copy, Clone)]
pub enum ParameterDisplayStyle {
Config,
//SonfigSub,
Arg,
Fixed,
}
/// CLI usage information format
#[derive(Copy, Clone, PartialEq)]
pub enum DocumentationFormat {
/// text, command line only (one line)
Short,
/// text, list all options
Long,
/// text, include description
Full,
/// like full, but in reStructuredText format
ReST,
}
/// Line wrapping to form simple list of paragraphs.
pub fn wrap_text(
initial_indent: &str,
subsequent_indent: &str,
text: &str,
columns: usize,
) -> String {
let wrapper1 = textwrap::Wrapper::new(columns)
.initial_indent(initial_indent)
.subsequent_indent(subsequent_indent);
let wrapper2 = textwrap::Wrapper::new(columns)
.initial_indent(subsequent_indent)
.subsequent_indent(subsequent_indent);
text.split("\n\n")
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.fold(String::new(), |mut acc, p| {
if acc.is_empty() {
acc.push_str(&wrapper1.wrap(p).concat());
} else {
acc.push_str(&wrapper2.wrap(p).concat());
}
acc.push_str("\n\n");
acc
})
}
/// TODO: Document output format.
pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> String {
match schema {
Schema::Null => String::from("<null>"), // should not happen
Schema::String(_) => String::from("<string>"),
Schema::Boolean(_) => String::from("<boolean>"),
Schema::Integer(integer_schema) => match (integer_schema.minimum, integer_schema.maximum) {
(Some(min), Some(max)) => format!("<integer> ({} - {})", min, max),
(Some(min), None) => format!("<integer> ({} - N)", min),
(None, Some(max)) => format!("<integer> (-N - {})", max),
_ => String::from("<integer>"),
},
Schema::Object(_) => String::from("<object>"),
Schema::Array(_) => String::from("<array>"),
}
}
/// TODO: Document output format.
pub fn get_property_description(
name: &str,
schema: &Schema,
style: ParameterDisplayStyle,
format: DocumentationFormat,
) -> String {
let type_text = get_schema_type_text(schema, style);
let (descr, default) = match schema {
Schema::Null => ("null", None),
Schema::String(ref schema) => (schema.description, schema.default.map(|v| v.to_owned())),
Schema::Boolean(ref schema) => (schema.description, schema.default.map(|v| v.to_string())),
Schema::Integer(ref schema) => (schema.description, schema.default.map(|v| v.to_string())),
Schema::Object(ref schema) => (schema.description, None),
Schema::Array(ref schema) => (schema.description, None),
};
let default_text = match default {
Some(text) => format!(" (default={})", text),
None => String::new(),
};
if format == DocumentationFormat::ReST {
let mut text = match style {
ParameterDisplayStyle::Config => {
format!(":``{} {}{}``: ", name, type_text, default_text)
}
ParameterDisplayStyle::Arg => {
format!(":``--{} {}{}``: ", name, type_text, default_text)
}
ParameterDisplayStyle::Fixed => {
format!(":``<{}> {}{}``: ", name, type_text, default_text)
}
};
text.push_str(&wrap_text("", "", descr, 80));
text.push('\n');
text.push('\n');
text
} else {
let display_name = match style {
ParameterDisplayStyle::Config => format!("{}:", name),
ParameterDisplayStyle::Arg => format!("--{}", name),
ParameterDisplayStyle::Fixed => format!("<{}>", name),
};
let mut text = format!(" {:-10} {}{}", display_name, type_text, default_text);
let indent = " ";
text.push('\n');
text.push_str(&wrap_text(indent, indent, descr, 80));
text.push('\n');
text.push('\n');
text
}
}
fn dump_api_parameters(param: &ObjectSchema) -> String {
let mut res = wrap_text("", "", param.description, 80);
let mut required_list: Vec<String> = Vec::new();
let mut optional_list: Vec<String> = Vec::new();
for (prop, optional, schema) in param.properties {
let param_descr = get_property_description(
prop,
&schema,
ParameterDisplayStyle::Config,
DocumentationFormat::ReST,
);
if *optional {
optional_list.push(param_descr);
} else {
required_list.push(param_descr);
}
}
if !required_list.is_empty() {
res.push_str("\n*Required properties:*\n\n");
for text in required_list {
res.push_str(&text);
res.push('\n');
}
}
if !optional_list.is_empty() {
res.push_str("\n*Optional properties:*\n\n");
for text in optional_list {
res.push_str(&text);
res.push('\n');
}
}
res
}
fn dump_api_return_schema(schema: &Schema) -> String {
let mut res = String::from("*Returns*: ");
let type_text = get_schema_type_text(schema, ParameterDisplayStyle::Config);
res.push_str(&format!("**{}**\n\n", type_text));
match schema {
Schema::Null => {
return res;
}
Schema::Boolean(schema) => {
let description = wrap_text("", "", schema.description, 80);
res.push_str(&description);
}
Schema::Integer(schema) => {
let description = wrap_text("", "", schema.description, 80);
res.push_str(&description);
}
Schema::String(schema) => {
let description = wrap_text("", "", schema.description, 80);
res.push_str(&description);
}
Schema::Array(schema) => {
let description = wrap_text("", "", schema.description, 80);
res.push_str(&description);
}
Schema::Object(obj_schema) => {
res.push_str(&dump_api_parameters(obj_schema));
}
}
res.push('\n');
res
}
fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) -> Option<String> {
match def {
None => None,
Some(api_method) => {
let param_descr = dump_api_parameters(api_method.parameters);
let return_descr = dump_api_return_schema(api_method.returns);
let mut method = method;
if let ApiHandler::Async(_) = api_method.handler {
method = if method == "POST" { "UPLOAD" } else { method };
method = if method == "GET" { "DOWNLOAD" } else { method };
}
let res = format!(
"**{} {}**\n\n{}\n\n{}",
method, path, param_descr, return_descr
);
Some(res)
}
}
}
/// TODO: Document output format.
pub fn dump_api(
output: &mut dyn Write,
router: &Router,
path: &str,
mut pos: usize,
) -> Result<(), Error> {
let mut cond_print = |x| -> Result<_, Error> {
if let Some(text) = x {
if pos > 0 {
writeln!(output, "-----\n")?;
}
writeln!(output, "{}", text)?;
pos += 1;
}
Ok(())
};
cond_print(dump_method_definition("GET", path, router.get))?;
cond_print(dump_method_definition("POST", path, router.post))?;
cond_print(dump_method_definition("PUT", path, router.put))?;
cond_print(dump_method_definition("DELETE", path, router.delete))?;
match &router.subroute {
None => return Ok(()),
Some(SubRoute::MatchAll { router, param_name }) => {
let sub_path = if path == "." {
format!("<{}>", param_name)
} else {
format!("{}/<{}>", path, param_name)
};
dump_api(output, router, &sub_path, pos)?;
}
Some(SubRoute::Map(dirmap)) => {
//let mut keys: Vec<&String> = map.keys().collect();
//keys.sort_unstable_by(|a, b| a.cmp(b));
for (key, sub_router) in dirmap.iter() {
let sub_path = if path == "." {
key.to_string()
} else {
format!("{}/{}", path, key)
};
dump_api(output, sub_router, &sub_path, pos)?;
}
}
}
Ok(())
}

View File

@ -10,6 +10,7 @@ use serde_json::Value;
pub mod const_regex;
pub mod error;
pub mod format;
pub mod router;
pub mod rpc_environment;
pub mod schema;