From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 5409C1FF16B for ; Fri, 21 Nov 2025 16:51:11 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 55927261CF; Fri, 21 Nov 2025 16:51:18 +0100 (CET) Mime-Version: 1.0 Date: Fri, 21 Nov 2025 16:51:12 +0100 Message-Id: To: "Shannon Sterz" X-Mailer: aerc 0.20.0 References: <20251113120021.331639-1-s.sterz@proxmox.com> <20251113120021.331639-8-s.sterz@proxmox.com> In-Reply-To: <20251113120021.331639-8-s.sterz@proxmox.com> From: "Shannon Sterz" X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1763740240037 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.554 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_ZWNJBAD 1.25 Attempted & failed Use of zero-width characters indicates a goal to elude scanners 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. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches 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. [docgen.rs, router.post] Subject: Re: [pdm-devel] [PATCH proxmox-backup v2 1/1] docgen: use proxmox-rs docgen crate X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Cc: pdm-devel@lists.proxmox.com Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" 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 > --- > 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 @@ > Proxmox Backup Server API Documentation > > > + > + > > > > 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 = > - 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 = list.iter().map(|p| dump_api_permission(p)).collect(); > - json!({ "and": list }) > - } > - Permission::Or(list) => { > - let list: Vec = 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