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