//! Module to generate and format API Documenation use anyhow::{bail, Error}; use crate::*; /// Enumerate different styles to display parameters/properties. #[derive(Copy, Clone, PartialEq, Eq)] pub enum ParameterDisplayStyle { /// Used for properties in configuration files: ``key:`` Config, /// Used for PropertyStings properties in configuration files ConfigSub, /// Used for command line options: ``--key`` Arg, /// Used for command line options passed as arguments: ```` Fixed, } /// CLI usage information format. #[derive(Copy, Clone, PartialEq, Eq)] 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).join("\n")); } else { acc.push_str(&wrapper2.wrap(p).join("\n")); } acc.push_str("\n\n"); acc }) } #[test] fn test_wrap_text() { let text = "Command. This may be a list in order to spefify nested sub-commands."; let expect = " Command. This may be a list in order to spefify nested sub-\n commands.\n\n"; let indent = " "; let wrapped = wrap_text(indent, indent, text, 80); assert_eq!(wrapped, expect); } fn get_simple_type_text(schema: &Schema, list_enums: bool) -> String { match schema { Schema::Null => String::from(""), // should not happen Schema::Boolean(_) => String::from("<1|0>"), Schema::Integer(_) => String::from(""), Schema::Number(_) => String::from(""), Schema::String(string_schema) => match string_schema { StringSchema { type_text: Some(type_text), .. } => String::from(*type_text), StringSchema { format: Some(ApiStringFormat::Enum(variants)), .. } => { if list_enums && variants.len() <= 3 { let list: Vec = variants.iter().map(|e| String::from(e.value)).collect(); list.join("|") } else { String::from("") } } _ => String::from(""), }, _ => panic!("get_simple_type_text: expected simple type"), } } /// Generate ReST Documentaion for object properties pub fn dump_properties( param: &dyn ObjectSchemaType, indent: &str, style: ParameterDisplayStyle, skip: &[&str], ) -> String { let mut res = String::new(); let next_indent = format!(" {}", indent); let mut required_list: Vec = Vec::new(); let mut optional_list: Vec = Vec::new(); for (prop, optional, schema) in param.properties() { if skip.iter().any(|n| n == prop) { continue; } let mut param_descr = get_property_description(prop, schema, style, DocumentationFormat::ReST); if !indent.is_empty() { param_descr = format!("{}{}", indent, param_descr); // indent first line param_descr = param_descr.replace('\n', &format!("\n{}", indent)); // indent rest } if style == ParameterDisplayStyle::Config { if let Schema::String(StringSchema { format: Some(ApiStringFormat::PropertyString(sub_schema)), .. }) = schema { match sub_schema { Schema::Object(object_schema) => { let sub_text = dump_properties( object_schema, &next_indent, ParameterDisplayStyle::ConfigSub, &[], ); param_descr.push_str(&sub_text); } Schema::Array(_) => { // do nothing - description should explain the list type } _ => unreachable!(), } } } if *optional { optional_list.push(param_descr); } else { required_list.push(param_descr); } } if !required_list.is_empty() { if style != ParameterDisplayStyle::ConfigSub { 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() { if style != ParameterDisplayStyle::ConfigSub { res.push_str("\n*Optional properties:*\n\n"); } for text in optional_list { res.push_str(&text); res.push('\n'); } } res } /// Helper to format an object property, including name, type and description. 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, extra) = match schema { Schema::Null => ("null", None, None), Schema::String(ref schema) => ( schema.description, schema.default.map(|v| v.to_owned()), None, ), Schema::Boolean(ref schema) => ( schema.description, schema.default.map(|v| v.to_string()), None, ), Schema::Integer(ref schema) => ( schema.description, schema.default.map(|v| v.to_string()), None, ), Schema::Number(ref schema) => ( schema.description, schema.default.map(|v| v.to_string()), None, ), Schema::Object(ref schema) => (schema.description, None, None), Schema::AllOf(ref schema) => (schema.description, None, None), Schema::Array(ref schema) => ( schema.description, None, Some(String::from("Can be specified more than once.")), ), }; let default_text = match default { Some(text) => format!(" (default={})", text), None => String::new(), }; let descr = match extra { Some(extra) => format!("{} {}", descr, extra), None => String::from(descr), }; if format == DocumentationFormat::ReST { let mut text = match style { ParameterDisplayStyle::Config => { // reST definition list format format!("``{}`` : ``{}{}``\n ", name, type_text, default_text) } ParameterDisplayStyle::ConfigSub => { // reST definition list format format!("``{}`` = ``{}{}``\n ", name, type_text, default_text) } ParameterDisplayStyle::Arg => { // reST option list format format!("``--{}`` ``{}{}``\n ", name, type_text, default_text) } ParameterDisplayStyle::Fixed => { format!("``<{}>`` : ``{}{}``\n ", name, type_text, default_text) } }; text.push_str(&wrap_text("", " ", &descr, 80)); text.push('\n'); text } else { let display_name = match style { ParameterDisplayStyle::Config => format!("{}:", name), ParameterDisplayStyle::ConfigSub => 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 } } /// Helper to format the type text /// /// The result is a short string including important constraints, for /// example `` (0 - N)``. pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> String { match schema { Schema::Null => String::from(""), // should not happen Schema::String(string_schema) => { match string_schema { StringSchema { type_text: Some(type_text), .. } => String::from(*type_text), StringSchema { format: Some(ApiStringFormat::Enum(variants)), .. } => { let list: Vec = variants.iter().map(|e| String::from(e.value)).collect(); list.join("|") } // displaying regex add more confision than it helps //StringSchema { format: Some(ApiStringFormat::Pattern(const_regex)), .. } => { // format!("/{}/", const_regex.regex_string) //} StringSchema { format: Some(ApiStringFormat::PropertyString(sub_schema)), .. } => get_property_string_type_text(sub_schema), _ => 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::Number(number_schema) => match (number_schema.minimum, number_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(schema) => get_schema_type_text(schema.items, _style), Schema::AllOf(_) => String::from(""), } } pub fn get_property_string_type_text(schema: &Schema) -> String { match schema { Schema::Object(object_schema) => get_object_type_text(object_schema), Schema::Array(array_schema) => { let item_type = get_simple_type_text(array_schema.items, true); format!("[{}, ...]", item_type) } _ => panic!("get_property_string_type_text: expected array or object"), } } fn get_object_type_text(object_schema: &ObjectSchema) -> String { let mut parts = Vec::new(); let mut add_part = |name, optional, schema| { let tt = get_simple_type_text(schema, false); let text = if parts.is_empty() { format!("{}={}", name, tt) } else { format!(",{}={}", name, tt) }; if optional { parts.push(format!("[{}]", text)); } else { parts.push(text); } }; // add default key first if let Some(ref default_key) = object_schema.default_key { let (optional, schema) = object_schema.lookup(default_key).unwrap(); add_part(default_key, optional, schema); } // add required keys for (name, optional, schema) in object_schema.properties { if *optional { continue; } if let Some(ref default_key) = object_schema.default_key { if name == default_key { continue; } } add_part(name, *optional, schema); } // add options keys for (name, optional, schema) in object_schema.properties { if !*optional { continue; } if let Some(ref default_key) = object_schema.default_key { if name == default_key { continue; } } add_part(name, *optional, schema); } let mut type_text = String::new(); type_text.push('['); type_text.push_str(&parts.join(" ")); type_text.push(']'); type_text } /// Generate ReST Documentaion for enumeration. pub fn dump_enum_properties(schema: &Schema) -> Result { let mut res = String::new(); if let Schema::String(StringSchema { format: Some(ApiStringFormat::Enum(variants)), .. }) = schema { for item in variants.iter() { use std::fmt::Write; let _ = write!(res, ":``{}``: ", item.value); let descr = wrap_text("", " ", item.description, 80); res.push_str(&descr); res.push('\n'); } return Ok(res); } bail!("dump_enum_properties failed - not an enum"); } pub fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle) -> String { use std::fmt::Write; let schema = &returns.schema; let mut res = if returns.optional { "*Returns* (optionally): ".to_string() } else { "*Returns*: ".to_string() }; let type_text = get_schema_type_text(schema, style); let _ = write!(res, "**{}**\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::Number(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) => { let description = wrap_text("", "", obj_schema.description, 80); res.push_str(&description); res.push_str(&dump_properties(obj_schema, "", style, &[])); } Schema::AllOf(all_of_schema) => { let description = wrap_text("", "", all_of_schema.description, 80); res.push_str(&description); res.push_str(&dump_properties(all_of_schema, "", style, &[])); } } res.push('\n'); res }