From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pbs-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 8A87D1FF189 for <inbox@lore.proxmox.com>; Fri, 21 Feb 2025 11:28:39 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B41C029473; Fri, 21 Feb 2025 07:50:50 +0100 (CET) From: Dietmar Maurer <dietmar@proxmox.com> To: pbs-devel@lists.proxmox.com Date: Fri, 21 Feb 2025 07:50:17 +0100 Message-Id: <20250221065017.1300479-2-dietmar@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250221065017.1300479-1-dietmar@proxmox.com> References: <20250221065017.1300479-1-dietmar@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.571 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [command.rs, lib.rs, format.rs] Subject: [pbs-devel] [PATCH proxmox 2/2] schema: support doc generation in Djot format X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion <pbs-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pbs-devel>, <mailto:pbs-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pbs-devel/> List-Post: <mailto:pbs-devel@lists.proxmox.com> List-Help: <mailto:pbs-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel>, <mailto:pbs-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Proxmox Backup Server development discussion <pbs-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pbs-devel-bounces@lists.proxmox.com Sender: "pbs-devel" <pbs-devel-bounces@lists.proxmox.com> Try to keep code sparate from Rest formatting, so that we can easily remove unnecessary parts later. Signed-off-by: Dietmar Maurer <dietmar@proxmox.com> --- proxmox-router/src/cli/command.rs | 18 +++ proxmox-router/src/cli/format.rs | 29 ++-- proxmox-router/src/format.rs | 28 ++-- proxmox-schema/src/format.rs | 213 ++++++++++++++++++++++++------ proxmox-section-config/src/lib.rs | 43 +++++- 5 files changed, 270 insertions(+), 61 deletions(-) diff --git a/proxmox-router/src/cli/command.rs b/proxmox-router/src/cli/command.rs index 01f64d19..64a28d92 100644 --- a/proxmox-router/src/cli/command.rs +++ b/proxmox-router/src/cli/command.rs @@ -406,6 +406,24 @@ where println!("{}", usage); std::process::exit(0); } + + if args[0] == "printdoc-djot" { + let usage = match def { + CommandLineInterface::Simple(cli_cmd) => generate_usage_str_do( + &prefix, + cli_cmd, + DocumentationFormat::Djot, + "", + &[], + [].into_iter(), + ), + CommandLineInterface::Nested(map) => { + generate_nested_usage(&prefix, map, DocumentationFormat::Djot) + } + }; + println!("{}", usage); + std::process::exit(0); + } } (prefix, args) diff --git a/proxmox-router/src/cli/format.rs b/proxmox-router/src/cli/format.rs index 95448aa9..b449df1f 100644 --- a/proxmox-router/src/cli/format.rs +++ b/proxmox-router/src/cli/format.rs @@ -106,6 +106,7 @@ pub(crate) fn generate_usage_str_do<'cli>( if is_array { args.push('{'); } + args.push('<'); args.push_str(positional_arg); args.push('>'); @@ -194,6 +195,10 @@ pub(crate) fn generate_usage_str_do<'cli>( "``{prefix}{args}{option_indicator}``\n\n{}", schema.description() ), + DocumentationFormat::Djot => format!( + "`{prefix}{args}{option_indicator}`\n\n{}", + schema.description() + ), }; if !arg_descr.is_empty() { @@ -217,7 +222,7 @@ pub(crate) fn generate_usage_str_do<'cli>( if done_hash.contains(name) { continue; } - if format == DocumentationFormat::ReST { + if format == DocumentationFormat::ReST || format == DocumentationFormat::Djot { use std::fmt::Write as _; // In the ReST outputs we don't include the documentation for global options each time @@ -233,8 +238,13 @@ pub(crate) fn generate_usage_str_do<'cli>( if !global_options.is_empty() { global_options.push_str("\n\n"); } - - let _ = write!(global_options, "``--{name}``"); + if format == DocumentationFormat::ReST { + let _ = write!(global_options, "``--{name}``"); + } else if format == DocumentationFormat::Djot { + let _ = write!(global_options, "`--{name}`"); + } else { + unreachable!(); + } } else { // For the other ones we use the same generation method as for any other option: @@ -376,8 +386,8 @@ fn generate_nested_usage_do<'cli>( let globals = state.describe_current(prefix, format); if !globals.is_empty() { - if format == DocumentationFormat::ReST { - usage.push_str("----\n\n"); + if format == DocumentationFormat::ReST || format == DocumentationFormat::Djot { + usage.push_str("\n----\n\n"); } usage.push_str(&globals); } @@ -402,8 +412,10 @@ fn generate_nested_usage_do<'cli>( match def.commands.get(cmd).unwrap() { CommandLineInterface::Simple(cli_cmd) => { - if !usage.is_empty() && format == DocumentationFormat::ReST { - usage.push_str("----\n\n"); + if !usage.is_empty() + && (format == DocumentationFormat::ReST || format == DocumentationFormat::Djot) + { + usage.push_str("\n----\n\n"); } usage.push_str(&generate_usage_str_do( &new_prefix, @@ -415,9 +427,10 @@ fn generate_nested_usage_do<'cli>( )); } CommandLineInterface::Nested(map) => { - if format == DocumentationFormat::ReST { + if format == DocumentationFormat::ReST || format == DocumentationFormat::Djot { usage.push_str("\n----\n\n"); } + usage.push_str(&generate_nested_usage_do(state, &new_prefix, map, format)); } } diff --git a/proxmox-router/src/format.rs b/proxmox-router/src/format.rs index 67568af0..8b2d4488 100644 --- a/proxmox-router/src/format.rs +++ b/proxmox-router/src/format.rs @@ -4,22 +4,28 @@ use std::io::Write; use anyhow::Error; -use proxmox_schema::format::*; +use proxmox_schema::format::{ + dump_api_return_schema_rest, dump_properties_rest, wrap_text, ParameterDisplayStyle, +}; use proxmox_schema::ObjectSchemaType; #[cfg(feature = "server")] use crate::ApiHandler; use crate::ApiMethod; -fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) -> Option<String> { +fn dump_method_definition_rest( + method: &str, + path: &str, + def: Option<&ApiMethod>, +) -> Option<String> { let style = ParameterDisplayStyle::Config; match def { None => None, Some(api_method) => { let description = wrap_text("", "", api_method.parameters.description(), 80); - let param_descr = dump_properties(&api_method.parameters, "", style, &[]); + let param_descr = dump_properties_rest(&api_method.parameters, "", style, &[]); - let return_descr = dump_api_return_schema(&api_method.returns, style); + let return_descr = dump_api_return_schema_rest(&api_method.returns, style); #[cfg(feature = "server")] let mut method = method; @@ -42,7 +48,7 @@ fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) -> } /// Generate ReST Documentation for a complete API defined by a ``Router``. -pub fn dump_api( +pub fn dump_api_rest( output: &mut dyn Write, router: &crate::Router, path: &str, @@ -61,10 +67,10 @@ pub fn dump_api( 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))?; + cond_print(dump_method_definition_rest("GET", path, router.get))?; + cond_print(dump_method_definition_rest("POST", path, router.post))?; + cond_print(dump_method_definition_rest("PUT", path, router.put))?; + cond_print(dump_method_definition_rest("DELETE", path, router.delete))?; match &router.subroute { None => return Ok(()), @@ -74,7 +80,7 @@ pub fn dump_api( } else { format!("{}/<{}>", path, param_name) }; - dump_api(output, router, &sub_path, pos)?; + dump_api_rest(output, router, &sub_path, pos)?; } Some(SubRoute::Map(dirmap)) => { //let mut keys: Vec<&String> = map.keys().collect(); @@ -85,7 +91,7 @@ pub fn dump_api( } else { format!("{}/{}", path, key) }; - dump_api(output, sub_router, &sub_path, pos)?; + dump_api_rest(output, sub_router, &sub_path, pos)?; } } } diff --git a/proxmox-schema/src/format.rs b/proxmox-schema/src/format.rs index 080b0268..45f7cd9d 100644 --- a/proxmox-schema/src/format.rs +++ b/proxmox-schema/src/format.rs @@ -28,6 +28,8 @@ pub enum DocumentationFormat { Full, /// Like full, but in reStructuredText format. ReST, + /// Like full, but in Djot format. + Djot, } /// Line wrapping to form simple list of paragraphs. @@ -135,7 +137,7 @@ fn get_simple_type_text(schema: &Schema, list_enums: bool) -> String { } /// Generate ReST Documentation for object properties -pub fn dump_properties( +pub fn dump_properties_rest( param: &dyn ObjectSchemaType, indent: &str, style: ParameterDisplayStyle, @@ -168,7 +170,7 @@ pub fn dump_properties( { match sub_schema { Schema::Object(object_schema) => { - let sub_text = dump_properties( + let sub_text = dump_properties_rest( object_schema, &next_indent, ParameterDisplayStyle::ConfigSub, @@ -218,6 +220,90 @@ pub fn dump_properties( res } +/// Generate Djot Documentation for object properties +pub fn dump_properties_djot( + 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<String> = Vec::new(); + let mut optional_list: Vec<String> = 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::Djot); + + 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_djot( + object_schema, + &next_indent, + ParameterDisplayStyle::ConfigSub, + &[], + ); + if !sub_text.is_empty() { + param_descr.push_str("\n\n"); + } + 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, @@ -269,42 +355,67 @@ pub fn get_property_description( None => String::from(descr), }; - if format == DocumentationFormat::ReST { - let mut text = match style { - ParameterDisplayStyle::Config => { - // reST definition list format - format!("``{name}`` : ``{type_text}{default_text}``\n") - } - ParameterDisplayStyle::ConfigSub => { - // reST definition list format - format!("``{name}`` = ``{type_text}{default_text}``\n") - } - ParameterDisplayStyle::Arg => { - // reST option list format - format!("``--{name}`` ``{type_text}{default_text}``\n") - } - ParameterDisplayStyle::Fixed => { - format!("``<{name}>`` : ``{type_text}{default_text}``\n") - } - }; + match format { + DocumentationFormat::ReST => { + let mut text = match style { + ParameterDisplayStyle::Config => { + // reST definition list format + format!("``{name}`` : ``{type_text}{default_text}``\n") + } + ParameterDisplayStyle::ConfigSub => { + // reST definition list format + format!("``{name}`` = ``{type_text}{default_text}``\n") + } + ParameterDisplayStyle::Arg => { + // reST option list format + format!("``--{name}`` ``{type_text}{default_text}``\n") + } + ParameterDisplayStyle::Fixed => { + format!("``<{name}>`` : ``{type_text}{default_text}``\n") + } + }; - text.push_str(&wrap_text(" ", " ", &descr, 80)); + text.push_str(&wrap_text(" ", " ", &descr, 80)); - text - } else { - let display_name = match style { - ParameterDisplayStyle::Config => format!("{name}:"), - ParameterDisplayStyle::ConfigSub => format!("{name}="), - ParameterDisplayStyle::Arg => format!("--{name}"), - ParameterDisplayStyle::Fixed => format!("<{name}>"), - }; + text + } + DocumentationFormat::Djot => { + // Djot definition list format + let mut text = match style { + ParameterDisplayStyle::Config => { + format!(": `{name} : {type_text}{default_text}`\n\n") + } + ParameterDisplayStyle::ConfigSub => { + format!(": `{name} = {type_text}{default_text}`\n\n") + } + ParameterDisplayStyle::Arg => { + format!(": `--{name} {type_text}{default_text}`\n\n") + } + ParameterDisplayStyle::Fixed => { + format!(": `<{name}> : {type_text}{default_text}`\n\n") + } + }; - let mut text = format!(" {display_name:-10} {type_text}{default_text}"); - let indent = " "; - text.push('\n'); - text.push_str(&wrap_text(indent, indent, &descr, 80)); + text.push_str(&wrap_text(" ", " ", &descr, 80)); + text.push_str("\n\n"); - text + text + } + _ => { + 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!(" {display_name:-10} {type_text}{default_text}"); + let indent = " "; + text.push('\n'); + text.push_str(&wrap_text(indent, indent, &descr, 80)); + + text + } } } @@ -428,7 +539,7 @@ fn get_object_type_text(object_schema: &ObjectSchema) -> String { } /// Generate ReST Documentation for enumeration. -pub fn dump_enum_properties(schema: &Schema) -> Result<String, Error> { +pub fn dump_enum_properties_rest(schema: &Schema) -> Result<String, Error> { let mut res = String::new(); if let Schema::String(StringSchema { @@ -450,7 +561,31 @@ pub fn dump_enum_properties(schema: &Schema) -> Result<String, Error> { bail!("dump_enum_properties failed - not an enum"); } -pub fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle) -> String { +/// Generate Djot Documentation for enumeration. +pub fn dump_enum_properties_djot(schema: &Schema) -> Result<String, Error> { + 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, ": `{}`\n\n", 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"); +} + +/// Generate ReST Documentation for return schema. +pub fn dump_api_return_schema_rest(returns: &ReturnType, style: ParameterDisplayStyle) -> String { use std::fmt::Write; let schema = &returns.schema; @@ -491,17 +626,17 @@ pub fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle Schema::Object(obj_schema) => { let description = wrap_text("", "", obj_schema.description, 80); res.push_str(&description); - res.push_str(&dump_properties(obj_schema, "", style, &[])); + res.push_str(&dump_properties_rest(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_str(&dump_properties_rest(all_of_schema, "", style, &[])); } Schema::OneOf(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_str(&dump_properties_rest(all_of_schema, "", style, &[])); } } diff --git a/proxmox-section-config/src/lib.rs b/proxmox-section-config/src/lib.rs index 1fc74e91..33f48677 100644 --- a/proxmox-section-config/src/lib.rs +++ b/proxmox-section-config/src/lib.rs @@ -30,7 +30,9 @@ use serde::ser::Serialize; use serde_json::{json, Value}; use proxmox_lang::try_block; -use proxmox_schema::format::{dump_properties, wrap_text, ParameterDisplayStyle}; +use proxmox_schema::format::{ + dump_properties_djot, dump_properties_rest, wrap_text, ParameterDisplayStyle, +}; use proxmox_schema::*; pub mod typed; @@ -1243,7 +1245,7 @@ sync: fail } /// Generate ReST Documentation for ``SectionConfig`` -pub fn dump_section_config(config: &SectionConfig) -> String { +pub fn dump_section_config_rest(config: &SectionConfig) -> String { let mut res = String::new(); let mut plugins: Vec<&String> = config.plugins().keys().collect(); @@ -1266,7 +1268,42 @@ pub fn dump_section_config(config: &SectionConfig) -> String { let _ = write!(res, "\n**Section type** \'``{name}``\': {description}\n\n"); } - res.push_str(&dump_properties( + res.push_str(&dump_properties_rest( + properties, + "", + ParameterDisplayStyle::Config, + &skip, + )); + } + + res +} + +/// Generate Djot Documentation for ``SectionConfig`` +pub fn dump_section_config_djot(config: &SectionConfig) -> String { + let mut res = String::new(); + + let mut plugins: Vec<&String> = config.plugins().keys().collect(); + plugins.sort_unstable(); + + let plugin_count = config.plugins().len(); + + for name in plugins { + let plugin = config.plugins().get(name).unwrap(); + let properties = plugin.properties(); + let skip = match plugin.id_property() { + Some(id) => vec![id], + None => Vec::new(), + }; + + if plugin_count > 1 { + use std::fmt::Write as _; + + let description = wrap_text("", "", properties.description(), 80); + let _ = write!(res, "\n*Section type* '`{name}`': {description}\n\n"); + } + + res.push_str(&dump_properties_djot( properties, "", ParameterDisplayStyle::Config, -- 2.39.5 _______________________________________________ pbs-devel mailing list pbs-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel