From: Dietmar Maurer <dietmar@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox 2/2] schema: support doc generation in Djot format
Date: Fri, 21 Feb 2025 07:50:17 +0100 [thread overview]
Message-ID: <20250221065017.1300479-2-dietmar@proxmox.com> (raw)
In-Reply-To: <20250221065017.1300479-1-dietmar@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
next prev parent reply other threads:[~2025-02-21 10:28 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-02-21 6:50 [pbs-devel] [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs Dietmar Maurer
2025-02-21 6:50 ` Dietmar Maurer [this message]
2025-02-21 9:10 ` [pbs-devel] applied: " Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250221065017.1300479-2-dietmar@proxmox.com \
--to=dietmar@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal