]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/docgen.rs
move drive config to pbs_config workspace
[proxmox-backup.git] / src / bin / docgen.rs
1 use anyhow::{bail, Error};
2 use serde_json::{json, Value};
3
4 use proxmox::{
5 api::{
6 schema::{
7 ApiType,
8 Schema,
9 ObjectSchemaType,
10 ApiStringFormat,
11 },
12 router::{
13 ApiAccess,
14 },
15 format::{
16 dump_enum_properties,
17 dump_section_config,
18 get_property_string_type_text,
19 },
20 ApiMethod,
21 ApiHandler,
22 Router,
23 SubRoute,
24 Permission,
25 },
26 };
27
28 use proxmox_backup::{
29 api2,
30 config::{
31 self,
32 acl::PRIVILEGES,
33 },
34 };
35
36 fn get_args() -> (String, Vec<String>) {
37
38 let mut args = std::env::args();
39 let prefix = args.next().unwrap();
40 let prefix = prefix.rsplit('/').next().unwrap().to_string(); // without path
41 let args: Vec<String> = args.collect();
42
43 (prefix, args)
44 }
45
46 fn main() -> Result<(), Error> {
47
48 let (_prefix, args) = get_args();
49
50 if args.len() < 1 {
51 bail!("missing arguments");
52 }
53
54 for arg in args.iter() {
55 let text = match arg.as_ref() {
56 "apidata.js" => generate_api_tree(),
57 "datastore.cfg" => dump_section_config(&config::datastore::CONFIG),
58 "tape.cfg" => dump_section_config(&pbs_config::drive::CONFIG),
59 "tape-job.cfg" => dump_section_config(&config::tape_job::CONFIG),
60 "user.cfg" => dump_section_config(&config::user::CONFIG),
61 "remote.cfg" => dump_section_config(&pbs_config::remote::CONFIG),
62 "sync.cfg" => dump_section_config(&config::sync::CONFIG),
63 "verification.cfg" => dump_section_config(&config::verify::CONFIG),
64 "media-pool.cfg" => dump_section_config(&config::media_pool::CONFIG),
65 "config::acl::Role" => dump_enum_properties(&config::acl::Role::API_SCHEMA)?,
66 _ => bail!("docgen: got unknown type"),
67 };
68 println!("{}", text);
69 }
70
71 Ok(())
72 }
73
74 fn generate_api_tree() -> String {
75
76 let mut tree = Vec::new();
77
78 let mut data = dump_api_schema(& api2::ROUTER, ".");
79 data["path"] = "/".into();
80 // hack: add invisible space to sort as first entry
81 data["text"] = "&#x200b;Management API (HTTP)".into();
82 data["expanded"] = true.into();
83
84 tree.push(data);
85
86 let mut data = dump_api_schema(&api2::backup::BACKUP_API_ROUTER, "/backup/_upgrade_");
87 data["path"] = "/backup/_upgrade_".into();
88 data["text"] = "Backup API (HTTP/2)".into();
89 tree.push(data);
90
91 let mut data = dump_api_schema(&api2::reader::READER_API_ROUTER, "/reader/_upgrade_");
92 data["path"] = "/reader/_upgrade_".into();
93 data["text"] = "Restore API (HTTP/2)".into();
94 tree.push(data);
95
96 format!("var apiSchema = {};", serde_json::to_string_pretty(&tree).unwrap())
97 }
98
99 pub fn dump_schema(schema: &Schema) -> Value {
100
101 let mut data;
102
103 match schema {
104 Schema::Null => {
105 data = json!({
106 "type": "null",
107 });
108 }
109 Schema::Boolean(boolean_schema) => {
110 data = json!({
111 "type": "boolean",
112 "description": boolean_schema.description,
113 });
114 if let Some(default) = boolean_schema.default {
115 data["default"] = default.into();
116 }
117 }
118 Schema::String(string_schema) => {
119 data = json!({
120 "type": "string",
121 "description": string_schema.description,
122 });
123 if let Some(default) = string_schema.default {
124 data["default"] = default.into();
125 }
126 if let Some(min_length) = string_schema.min_length {
127 data["minLength"] = min_length.into();
128 }
129 if let Some(max_length) = string_schema.max_length {
130 data["maxLength"] = max_length.into();
131 }
132 if let Some(type_text) = string_schema.type_text {
133 data["typetext"] = type_text.into();
134 }
135 match string_schema.format {
136 None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
137 Some(ApiStringFormat::Pattern(const_regex)) => {
138 data["pattern"] = format!("/{}/", const_regex.regex_string)
139 .into();
140 }
141 Some(ApiStringFormat::Enum(variants)) => {
142 let variants: Vec<String> = variants
143 .iter()
144 .map(|e| e.value.to_string())
145 .collect();
146 data["enum"] = serde_json::to_value(variants).unwrap();
147 }
148 Some(ApiStringFormat::PropertyString(subschema)) => {
149
150 match subschema {
151 Schema::Object(_) | Schema::Array(_) => {
152 data["format"] = dump_schema(subschema);
153 data["typetext"] = get_property_string_type_text(subschema)
154 .into();
155 }
156 _ => { /* do nothing - shouldnot happen */ }
157 };
158 }
159 }
160 // fixme: dump format
161 }
162 Schema::Integer(integer_schema) => {
163 data = json!({
164 "type": "integer",
165 "description": integer_schema.description,
166 });
167 if let Some(default) = integer_schema.default {
168 data["default"] = default.into();
169 }
170 if let Some(minimum) = integer_schema.minimum {
171 data["minimum"] = minimum.into();
172 }
173 if let Some(maximum) = integer_schema.maximum {
174 data["maximum"] = maximum.into();
175 }
176 }
177 Schema::Number(number_schema) => {
178 data = json!({
179 "type": "number",
180 "description": number_schema.description,
181 });
182 if let Some(default) = number_schema.default {
183 data["default"] = default.into();
184 }
185 if let Some(minimum) = number_schema.minimum {
186 data["minimum"] = minimum.into();
187 }
188 if let Some(maximum) = number_schema.maximum {
189 data["maximum"] = maximum.into();
190 }
191 }
192 Schema::Object(object_schema) => {
193 data = dump_property_schema(object_schema);
194 data["type"] = "object".into();
195 if let Some(default_key) = object_schema.default_key {
196 data["default_key"] = default_key.into();
197 }
198 }
199 Schema::Array(array_schema) => {
200 data = json!({
201 "type": "array",
202 "description": array_schema.description,
203 "items": dump_schema(array_schema.items),
204 });
205 if let Some(min_length) = array_schema.min_length {
206 data["minLength"] = min_length.into();
207 }
208 if let Some(max_length) = array_schema.min_length {
209 data["maxLength"] = max_length.into();
210 }
211 }
212 Schema::AllOf(alloff_schema) => {
213 data = dump_property_schema(alloff_schema);
214 data["type"] = "object".into();
215 }
216 };
217
218 data
219 }
220
221 pub fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
222 let mut properties = json!({});
223
224 for (prop, optional, schema) in param.properties() {
225 let mut property = dump_schema(schema);
226 if *optional {
227 property["optional"] = 1.into();
228 }
229 properties[prop] = property;
230 }
231
232 let data = json!({
233 "description": param.description(),
234 "additionalProperties": param.additional_properties(),
235 "properties": properties,
236 });
237
238 data
239 }
240
241 fn dump_api_permission(permission: &Permission) -> Value {
242
243 match permission {
244 Permission::Superuser => json!({ "user": "root@pam" }),
245 Permission::User(user) => json!({ "user": user }),
246 Permission::Anybody => json!({ "user": "all" }),
247 Permission::World => json!({ "user": "world" }),
248 Permission::UserParam(param) => json!({ "userParam": param }),
249 Permission::Group(group) => json!({ "group": group }),
250 Permission::WithParam(param, sub_permission) => {
251 json!({
252 "withParam": {
253 "name": param,
254 "permissions": dump_api_permission(sub_permission),
255 },
256 })
257 }
258 Permission::Privilege(name, value, partial) => {
259
260 let mut privs = Vec::new();
261 for (name, v) in PRIVILEGES {
262 if (value & v) != 0 {
263 privs.push(name.to_string());
264 }
265 }
266
267 json!({
268 "check": {
269 "path": name,
270 "privs": privs,
271 "partial": partial,
272 }
273 })
274 }
275 Permission::And(list) => {
276 let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
277 json!({ "and": list })
278 }
279 Permission::Or(list) => {
280 let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
281 json!({ "or": list })
282 }
283 }
284 }
285
286 fn dump_api_method_schema(
287 method: &str,
288 api_method: &ApiMethod,
289 ) -> Value {
290 let mut data = json!({
291 "description": api_method.parameters.description(),
292 });
293
294 data["parameters"] = dump_property_schema(&api_method.parameters);
295
296 let mut returns = dump_schema(&api_method.returns.schema);
297 if api_method.returns.optional {
298 returns["optional"] = 1.into();
299 }
300 data["returns"] = returns;
301
302 match api_method.access {
303 ApiAccess { description: None, permission: Permission::Superuser } => {
304 // no need to output default
305 }
306 ApiAccess { description, permission } => {
307 let mut permissions = dump_api_permission(permission);
308 if let Some(description) = description {
309 permissions["description"] = description.into();
310 }
311 data["permissions"] = permissions;
312 }
313 }
314
315 let mut method = method;
316
317 if let ApiHandler::AsyncHttp(_) = api_method.handler {
318 method = if method == "POST" { "UPLOAD" } else { method };
319 method = if method == "GET" { "DOWNLOAD" } else { method };
320 }
321
322 data["method"] = method.into();
323
324 data
325 }
326
327 pub fn dump_api_schema(
328 router: &Router,
329 path: &str,
330 ) -> Value {
331
332 let mut data = json!({});
333
334 let mut info = json!({});
335 if let Some(api_method) = router.get {
336 info["GET"] = dump_api_method_schema("GET", api_method);
337 }
338 if let Some(api_method) = router.post {
339 info["POST"] = dump_api_method_schema("POST", api_method);
340 }
341 if let Some(api_method) = router.put {
342 info["PUT"] = dump_api_method_schema("PUT", api_method);
343 }
344 if let Some(api_method) = router.delete {
345 info["DELETE"] = dump_api_method_schema("DELETE", api_method);
346 }
347
348 data["info"] = info;
349
350 match &router.subroute {
351 None => {
352 data["leaf"] = 1.into();
353 },
354 Some(SubRoute::MatchAll { router, param_name }) => {
355 let sub_path = if path == "." {
356 format!("/{{{}}}", param_name)
357 } else {
358 format!("{}/{{{}}}", path, param_name)
359 };
360 let mut child = dump_api_schema(router, &sub_path);
361 child["path"] = sub_path.into();
362 child["text"] = format!("{{{}}}", param_name).into();
363
364 let mut children = Vec::new();
365 children.push(child);
366 data["children"] = children.into();
367 data["leaf"] = 0.into();
368 }
369 Some(SubRoute::Map(dirmap)) => {
370
371 let mut children = Vec::new();
372
373 for (key, sub_router) in dirmap.iter() {
374 let sub_path = if path == "." {
375 format!("/{}", key)
376 } else {
377 format!("{}/{}", path, key)
378 };
379 let mut child = dump_api_schema(sub_router, &sub_path);
380 child["path"] = sub_path.into();
381 child["text"] = key.to_string().into();
382 children.push(child);
383 }
384
385 data["children"] = children.into();
386 data["leaf"] = 0.into();
387 }
388 }
389
390 data
391 }