From: "Shannon Sterz" <s.sterz@proxmox.com>
To: "Shannon Sterz" <s.sterz@proxmox.com>
Cc: pdm-devel@lists.proxmox.com
Subject: Re: [pdm-devel] [PATCH proxmox-backup v2 1/1] docgen: use proxmox-rs docgen crate
Date: Fri, 21 Nov 2025 16:51:12 +0100 [thread overview]
Message-ID: <DEEHQVFCO6T1.36SFFAMFBM6DK@proxmox.com> (raw)
In-Reply-To: <20251113120021.331639-8-s.sterz@proxmox.com>
not sure what happened that these two pbs patches where added here.
sorry for that, i'll respin this series to make it easier to
apply/review. sorry for the noise.
On Thu Nov 13, 2025 at 1:00 PM CET, Shannon Sterz wrote:
> this has the benefit of also handling the unstable flag correctly.
> hence, the api viewer needs to add the css definitions to properly
> render unstable endpoints.
>
> Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
> ---
> Cargo.toml | 3 +
> docs/api-viewer/index.html | 2 +
> src/bin/docgen.rs | 326 ++-----------------------------------
> 3 files changed, 20 insertions(+), 311 deletions(-)
>
> diff --git a/Cargo.toml b/Cargo.toml
> index 28fdbf54..fbd16ccb 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -62,6 +62,7 @@ proxmox-borrow = "1"
> proxmox-compression = "1.0.1"
> proxmox-config-digest = "1"
> proxmox-daemon = "1"
> +proxmox-docgen = "1"
> proxmox-fuse = "1"
> proxmox-http = { version = "1.0.2", features = [ "client", "http-helpers", "websocket" ] } # see below
> proxmox-human-byte = "1"
> @@ -216,6 +217,7 @@ proxmox-base64.workspace = true
> proxmox-compression.workspace = true
> proxmox-config-digest.workspace = true
> proxmox-daemon.workspace = true
> +proxmox-docgen.workspace = true
> proxmox-http = { workspace = true, features = [ "body", "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
> proxmox-human-byte.workspace = true
> proxmox-io.workspace = true
> @@ -275,6 +277,7 @@ proxmox-rrd-api-types.workspace = true
> #proxmox-compression = { path = "../proxmox/proxmox-compression" }
> #proxmox-config-digest = { path = "../proxmox/proxmox-config-digest" }
> #proxmox-daemon = { path = "../proxmox/proxmox-daemon" }
> +#proxmox-docgen = { path = "../proxmox/proxmox-docgen" }
> #proxmox-http = { path = "../proxmox/proxmox-http" }
> #proxmox-http-error = { path = "../proxmox/proxmox-http-error" }
> #proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" }
> diff --git a/docs/api-viewer/index.html b/docs/api-viewer/index.html
> index 72dae96a..28f5b3b7 100644
> --- a/docs/api-viewer/index.html
> +++ b/docs/api-viewer/index.html
> @@ -6,6 +6,8 @@
> <title>Proxmox Backup Server API Documentation</title>
>
> <link rel="stylesheet" type="text/css" href="extjs/theme-crisp/resources/theme-crisp-all.css">
> + <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
> + <link rel="stylesheet" type="text/css" href="/widgettoolkit/css/ext6-pmx.css" />
> <link rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" href="/widgettoolkit/themes/theme-proxmox-dark.css" />
> <script type="text/javascript" src="extjs/ext-all.js"></script>
> <script type="text/javascript" src="apidoc.js"></script>
> diff --git a/src/bin/docgen.rs b/src/bin/docgen.rs
> index 674be945..04d2de29 100644
> --- a/src/bin/docgen.rs
> +++ b/src/bin/docgen.rs
> @@ -1,9 +1,7 @@
> use anyhow::{bail, Error};
> -use serde_json::{json, Value};
>
> -use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute};
> -use proxmox_schema::format::{dump_enum_properties, get_property_string_type_text};
> -use proxmox_schema::{ApiStringFormat, ApiType, ObjectSchemaType, Schema};
> +use proxmox_schema::format::dump_enum_properties;
> +use proxmox_schema::ApiType;
> use proxmox_section_config::dump_section_config;
>
> use pbs_api_types::PRIVILEGES;
> @@ -55,22 +53,30 @@ fn main() -> Result<(), Error> {
> fn generate_api_tree() -> String {
> let mut tree = Vec::new();
>
> - let mut data = dump_api_schema(&api2::ROUTER, ".");
> + let mut data = proxmox_docgen::generate_api_tree(&api2::ROUTER, ".", PRIVILEGES);
> data["path"] = "/".into();
> // hack: add invisible space to sort as first entry
> data["text"] = "​Management API (HTTP)".into();
> - data["expanded"] = true.into();
> -
> tree.push(data);
>
> - let mut data = dump_api_schema(&api2::backup::BACKUP_API_ROUTER, "/backup/_upgrade_");
> + let mut data = proxmox_docgen::generate_api_tree(
> + &api2::backup::BACKUP_API_ROUTER,
> + "/backup/_upgrade_",
> + PRIVILEGES,
> + );
> data["path"] = "/backup/_upgrade_".into();
> data["text"] = "Backup API (HTTP/2)".into();
> + data["expanded"] = false.into();
> tree.push(data);
>
> - let mut data = dump_api_schema(&api2::reader::READER_API_ROUTER, "/reader/_upgrade_");
> + let mut data = proxmox_docgen::generate_api_tree(
> + &api2::reader::READER_API_ROUTER,
> + "/reader/_upgrade_",
> + PRIVILEGES,
> + );
> data["path"] = "/reader/_upgrade_".into();
> data["text"] = "Restore API (HTTP/2)".into();
> + data["expanded"] = false.into();
> tree.push(data);
>
> format!(
> @@ -78,305 +84,3 @@ fn generate_api_tree() -> String {
> serde_json::to_string_pretty(&tree).unwrap()
> )
> }
> -
> -pub fn dump_schema(schema: &Schema) -> Value {
> - let mut data;
> -
> - match schema {
> - Schema::Null => {
> - data = json!({
> - "type": "null",
> - });
> - }
> - Schema::Boolean(boolean_schema) => {
> - data = json!({
> - "type": "boolean",
> - "description": boolean_schema.description,
> - });
> - if let Some(default) = boolean_schema.default {
> - data["default"] = default.into();
> - }
> - }
> - Schema::String(string_schema) => {
> - data = json!({
> - "type": "string",
> - "description": string_schema.description,
> - });
> - if let Some(default) = string_schema.default {
> - data["default"] = default.into();
> - }
> - if let Some(min_length) = string_schema.min_length {
> - data["minLength"] = min_length.into();
> - }
> - if let Some(max_length) = string_schema.max_length {
> - data["maxLength"] = max_length.into();
> - }
> - if let Some(type_text) = string_schema.type_text {
> - data["typetext"] = type_text.into();
> - }
> - match string_schema.format {
> - None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
> - Some(ApiStringFormat::Pattern(const_regex)) => {
> - data["pattern"] = format!("/{}/", const_regex.regex_string).into();
> - }
> - Some(ApiStringFormat::Enum(variants)) => {
> - let variants: Vec<String> =
> - variants.iter().map(|e| e.value.to_string()).collect();
> - data["enum"] = serde_json::to_value(variants).unwrap();
> - }
> - Some(ApiStringFormat::PropertyString(subschema)) => {
> - match subschema {
> - Schema::Object(_) | Schema::Array(_) => {
> - data["format"] = dump_schema(subschema);
> - data["typetext"] = get_property_string_type_text(subschema).into();
> - }
> - _ => { /* do nothing - should not happen */ }
> - };
> - }
> - }
> - // fixme: dump format
> - }
> - Schema::Integer(integer_schema) => {
> - data = json!({
> - "type": "integer",
> - "description": integer_schema.description,
> - });
> - if let Some(default) = integer_schema.default {
> - data["default"] = default.into();
> - }
> - if let Some(minimum) = integer_schema.minimum {
> - data["minimum"] = minimum.into();
> - }
> - if let Some(maximum) = integer_schema.maximum {
> - data["maximum"] = maximum.into();
> - }
> - }
> - Schema::Number(number_schema) => {
> - data = json!({
> - "type": "number",
> - "description": number_schema.description,
> - });
> - if let Some(default) = number_schema.default {
> - data["default"] = default.into();
> - }
> - if let Some(minimum) = number_schema.minimum {
> - data["minimum"] = minimum.into();
> - }
> - if let Some(maximum) = number_schema.maximum {
> - data["maximum"] = maximum.into();
> - }
> - }
> - Schema::Object(object_schema) => {
> - data = dump_property_schema(object_schema);
> - data["type"] = "object".into();
> - if let Some(default_key) = object_schema.default_key {
> - data["default_key"] = default_key.into();
> - }
> - }
> - Schema::Array(array_schema) => {
> - data = json!({
> - "type": "array",
> - "description": array_schema.description,
> - "items": dump_schema(array_schema.items),
> - });
> - if let Some(min_length) = array_schema.min_length {
> - data["minLength"] = min_length.into();
> - }
> - if let Some(max_length) = array_schema.min_length {
> - data["maxLength"] = max_length.into();
> - }
> - }
> - Schema::AllOf(alloff_schema) => {
> - data = dump_property_schema(alloff_schema);
> - data["type"] = "object".into();
> - }
> - Schema::OneOf(schema) => {
> - let mut type_schema = dump_schema(schema.type_schema());
> - if schema.type_property_entry.1 {
> - type_schema["optional"] = true.into();
> - }
> - data = json!({
> - "type": "object",
> - "description": schema.description,
> - "typeProperty": schema.type_property(),
> - "typeSchema": type_schema,
> - });
> - let mut variants = Vec::with_capacity(schema.list.len());
> - for (title, variant) in schema.list {
> - let mut entry = dump_schema(variant);
> - entry["title"] = (*title).into();
> - variants.push(entry);
> - }
> - data["oneOf"] = variants.into();
> - }
> - };
> -
> - data
> -}
> -
> -pub fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
> - let mut properties = json!({});
> -
> - for (prop, optional, schema) in param.properties() {
> - let mut property = dump_schema(schema);
> - if *optional {
> - property["optional"] = 1.into();
> - }
> - properties[prop] = property;
> - }
> -
> - let data = json!({
> - "description": param.description(),
> - "additionalProperties": param.additional_properties(),
> - "properties": properties,
> - });
> -
> - data
> -}
> -
> -fn dump_api_permission(permission: &Permission) -> Value {
> - match permission {
> - Permission::Superuser => json!({ "user": "root@pam" }),
> - Permission::User(user) => json!({ "user": user }),
> - Permission::Anybody => json!({ "user": "all" }),
> - Permission::World => json!({ "user": "world" }),
> - Permission::UserParam(param) => json!({ "userParam": param }),
> - Permission::Group(group) => json!({ "group": group }),
> - Permission::WithParam(param, sub_permission) => {
> - json!({
> - "withParam": {
> - "name": param,
> - "permissions": dump_api_permission(sub_permission),
> - },
> - })
> - }
> - Permission::Privilege(name, value, partial) => {
> - let mut privs = Vec::new();
> - for (name, v) in PRIVILEGES {
> - if (value & v) != 0 {
> - privs.push(name.to_string());
> - }
> - }
> -
> - json!({
> - "check": {
> - "path": name,
> - "privs": privs,
> - "partial": partial,
> - }
> - })
> - }
> - Permission::And(list) => {
> - let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
> - json!({ "and": list })
> - }
> - Permission::Or(list) => {
> - let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
> - json!({ "or": list })
> - }
> - }
> -}
> -
> -fn dump_api_method_schema(method: &str, api_method: &ApiMethod) -> Value {
> - let mut data = json!({
> - "description": api_method.parameters.description(),
> - });
> -
> - data["parameters"] = dump_property_schema(&api_method.parameters);
> -
> - let mut returns = dump_schema(api_method.returns.schema);
> - if api_method.returns.optional {
> - returns["optional"] = 1.into();
> - }
> - data["returns"] = returns;
> -
> - match api_method.access {
> - ApiAccess {
> - description: None,
> - permission: Permission::Superuser,
> - } => {
> - // no need to output default
> - }
> - ApiAccess {
> - description,
> - permission,
> - } => {
> - let mut permissions = dump_api_permission(permission);
> - if let Some(description) = description {
> - permissions["description"] = description.into();
> - }
> - data["permissions"] = permissions;
> - }
> - }
> -
> - let mut method = method;
> -
> - if let ApiHandler::AsyncHttp(_) = api_method.handler {
> - method = if method == "POST" { "UPLOAD" } else { method };
> - method = if method == "GET" { "DOWNLOAD" } else { method };
> - }
> -
> - data["method"] = method.into();
> -
> - data
> -}
> -
> -pub fn dump_api_schema(router: &Router, path: &str) -> Value {
> - let mut data = json!({});
> -
> - let mut info = json!({});
> - if let Some(api_method) = router.get {
> - info["GET"] = dump_api_method_schema("GET", api_method);
> - }
> - if let Some(api_method) = router.post {
> - info["POST"] = dump_api_method_schema("POST", api_method);
> - }
> - if let Some(api_method) = router.put {
> - info["PUT"] = dump_api_method_schema("PUT", api_method);
> - }
> - if let Some(api_method) = router.delete {
> - info["DELETE"] = dump_api_method_schema("DELETE", api_method);
> - }
> -
> - data["info"] = info;
> -
> - match &router.subroute {
> - None => {
> - data["leaf"] = 1.into();
> - }
> - Some(SubRoute::MatchAll { router, param_name }) => {
> - let sub_path = if path == "." {
> - format!("/{{{param_name}}}")
> - } else {
> - format!("{path}/{{{param_name}}}")
> - };
> - let mut child = dump_api_schema(router, &sub_path);
> - child["path"] = sub_path.into();
> - child["text"] = format!("{{{param_name}}}").into();
> -
> - let children = vec![child];
> - data["children"] = children.into();
> - data["leaf"] = 0.into();
> - }
> - Some(SubRoute::Map(dirmap)) => {
> - let mut children = Vec::new();
> -
> - for (key, sub_router) in dirmap.iter() {
> - let sub_path = if path == "." {
> - format!("/{key}")
> - } else {
> - format!("{path}/{key}")
> - };
> - let mut child = dump_api_schema(sub_router, &sub_path);
> - child["path"] = sub_path.into();
> - child["text"] = key.to_string().into();
> - children.push(child);
> - }
> -
> - data["children"] = children.into();
> - data["leaf"] = 0.into();
> - }
> - }
> -
> - data
> -}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-11-21 15:51 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 1/5] router/api-macro: add unstable flag for ApiMethod Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 2/5] pve-api-types: generate array objects Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 3/5] pve-api-types: fix clippy lints Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 4/5] docgen: add docgen crate Shannon Sterz
2025-11-19 13:13 ` Lukas Wagner
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 5/5] docgen: add support for the new stable flag Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH widget-toolkit v2 1/1] api viewer: add support for endpoints that are marked as unstable Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox-backup v2 1/1] docgen: use proxmox-rs docgen crate Shannon Sterz
2025-11-21 15:51 ` Shannon Sterz [this message]
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] docgen: switch to " Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] api-viewer: add an api-viewer package Shannon Sterz
2025-11-19 13:13 ` [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Lukas Wagner
2025-11-19 13:46 ` Shannon Sterz
2025-11-19 14:16 ` [pdm-devel] Superseded: " Shannon Sterz
2025-11-21 16:05 ` Shannon Sterz
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=DEEHQVFCO6T1.36SFFAMFBM6DK@proxmox.com \
--to=s.sterz@proxmox.com \
--cc=pdm-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