public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Shannon Sterz <s.sterz@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v2 1/2] docgen: switch to proxmox-rs docgen crate
Date: Thu, 13 Nov 2025 13:00:20 +0100	[thread overview]
Message-ID: <20251113120021.331639-9-s.sterz@proxmox.com> (raw)
In-Reply-To: <20251113120021.331639-1-s.sterz@proxmox.com>

and use pdm permissions not pbs ones.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 Cargo.toml               |   2 +
 server/Cargo.toml        |   1 +
 server/src/bin/docgen.rs | 314 +--------------------------------------
 3 files changed, 7 insertions(+), 310 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 3252ccb..f9a8872 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-auth-api = "1"
 proxmox-base64 = "1"
 proxmox-client = "1"
 proxmox-daemon = "1"
+proxmox-docgen = "1"
 proxmox-http = { version = "1", features = [ "client", "http-helpers", "websocket" ] } # see below
 proxmox-human-byte = "1"
 proxmox-io = "1.0.1" # tools and client use "tokio" feature
@@ -154,6 +155,7 @@ zstd = { version = "0.13" }
 # proxmox-config-digest = { path = "../proxmox/proxmox-config-digest" }
 # proxmox-daemon = { path = "../proxmox/proxmox-daemon" }
 # proxmox-dns-api = { path = "../proxmox/proxmox-dns-api" }
+# proxmox-docgen = { path = "../proxmox/proxmox-docgen" }
 # proxmox-http-error = { path = "../proxmox/proxmox-http-error" }
 # proxmox-http = { path = "../proxmox/proxmox-http" }
 # proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" }
diff --git a/server/Cargo.toml b/server/Cargo.toml
index b336f1a..6969549 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-async.workspace = true
 proxmox-auth-api = { workspace = true, features = [ "api", "ticket", "pam-authenticator", "password-authenticator" ] }
 proxmox-base64.workspace = true
 proxmox-daemon.workspace = true
+proxmox-docgen.workspace = true
 proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
 proxmox-lang.workspace = true
 proxmox-ldap.workspace = true
diff --git a/server/src/bin/docgen.rs b/server/src/bin/docgen.rs
index cbf6fc0..85e05ec 100644
--- a/server/src/bin/docgen.rs
+++ b/server/src/bin/docgen.rs
@@ -1,12 +1,10 @@
 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, typed::ApiSectionDataEntry};
 
-use pbs_api_types::PRIVILEGES;
+use pdm_api_types::PRIVILEGES;
 
 use server::api;
 
@@ -45,12 +43,10 @@ fn main() -> Result<(), Error> {
 fn generate_api_tree() -> String {
     let mut tree = Vec::new();
 
-    let mut data = dump_api_schema(&api::ROUTER, ".");
+    let mut data = proxmox_docgen::generate_api_tree(&api::ROUTER, ".", PRIVILEGES);
     data["path"] = "/".into();
     // hack: add invisible space to sort as first entry
     data["text"] = "&#x200b;Management API (HTTP)".into();
-    data["expanded"] = true.into();
-
     tree.push(data);
 
     format!(
@@ -58,305 +54,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
-}
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


  parent reply	other threads:[~2025-11-13 12:00 UTC|newest]

Thread overview: 10+ 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-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-13 12:00 ` Shannon Sterz [this message]
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] api-viewer: add an api-viewer package 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=20251113120021.331639-9-s.sterz@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal