diff --git a/proxmox-api/Cargo.toml b/proxmox-api/Cargo.toml index 7bd79b01..1bb7f83e 100644 --- a/proxmox-api/Cargo.toml +++ b/proxmox-api/Cargo.toml @@ -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] diff --git a/proxmox-api/src/format.rs b/proxmox-api/src/format.rs new file mode 100644 index 00000000..62a1deec --- /dev/null +++ b/proxmox-api/src/format.rs @@ -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(""), // should not happen + Schema::String(_) => String::from(""), + Schema::Boolean(_) => String::from(""), + Schema::Integer(integer_schema) => match (integer_schema.minimum, integer_schema.maximum) { + (Some(min), Some(max)) => format!(" ({} - {})", min, max), + (Some(min), None) => format!(" ({} - N)", min), + (None, Some(max)) => format!(" (-N - {})", max), + _ => String::from(""), + }, + Schema::Object(_) => String::from(""), + Schema::Array(_) => String::from(""), + } +} + +/// 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 = Vec::new(); + let mut optional_list: Vec = 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 { + 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(()) +} diff --git a/proxmox-api/src/lib.rs b/proxmox-api/src/lib.rs index b38d3b8b..67afc788 100644 --- a/proxmox-api/src/lib.rs +++ b/proxmox-api/src/lib.rs @@ -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;