]> git.proxmox.com Git - proxmox.git/blob - proxmox-schema/src/format.rs
4e497ba5b32d56d80cb81e4003a19d6c23b3b163
[proxmox.git] / proxmox-schema / src / format.rs
1 //! Module to generate and format API Documenation
2
3 use anyhow::{bail, Error};
4
5 use crate::*;
6
7 /// Enumerate different styles to display parameters/properties.
8 #[derive(Copy, Clone, PartialEq, Eq)]
9 pub enum ParameterDisplayStyle {
10 /// Used for properties in configuration files: ``key:``
11 Config,
12 /// Used for PropertyStings properties in configuration files
13 ConfigSub,
14 /// Used for command line options: ``--key``
15 Arg,
16 /// Used for command line options passed as arguments: ``<key>``
17 Fixed,
18 }
19
20 /// CLI usage information format.
21 #[derive(Copy, Clone, PartialEq, Eq)]
22 pub enum DocumentationFormat {
23 /// Text, command line only (one line).
24 Short,
25 /// Text, list all options.
26 Long,
27 /// Text, include description.
28 Full,
29 /// Like full, but in reStructuredText format.
30 ReST,
31 }
32
33 /// Line wrapping to form simple list of paragraphs.
34 pub fn wrap_text(
35 initial_indent: &str,
36 subsequent_indent: &str,
37 text: &str,
38 columns: usize,
39 ) -> String {
40 let wrap_options1 = textwrap::Options::new(columns)
41 .initial_indent(initial_indent)
42 .subsequent_indent(subsequent_indent);
43
44 let wrap_options2 = textwrap::Options::new(columns)
45 .initial_indent(subsequent_indent)
46 .subsequent_indent(subsequent_indent);
47
48 text.split("\n\n")
49 .map(|p| p.trim())
50 .filter(|p| !p.is_empty())
51 .fold(String::new(), |mut acc, p| {
52 if acc.is_empty() {
53 acc.push_str(&textwrap::wrap(p, &wrap_options1).join("\n"));
54 } else {
55 acc.push_str(&textwrap::wrap(p, &wrap_options2).join("\n"));
56 }
57 acc.push_str("\n\n");
58 acc
59 })
60 }
61
62 #[test]
63 fn test_wrap_text() {
64 let text = "Command. This may be a list in order to spefify nested sub-commands.";
65 let expect = " Command. This may be a list in order to spefify nested sub-\n commands.\n\n";
66
67 let indent = " ";
68 let wrapped = wrap_text(indent, indent, text, 80);
69
70 assert_eq!(wrapped, expect);
71 }
72
73 fn get_simple_type_text(schema: &Schema, list_enums: bool) -> String {
74 match schema {
75 Schema::Null => String::from("<null>"), // should not happen
76 Schema::Boolean(_) => String::from("<1|0>"),
77 Schema::Integer(_) => String::from("<integer>"),
78 Schema::Number(_) => String::from("<number>"),
79 Schema::String(string_schema) => match string_schema {
80 StringSchema {
81 type_text: Some(type_text),
82 ..
83 } => String::from(*type_text),
84 StringSchema {
85 format: Some(ApiStringFormat::Enum(variants)),
86 ..
87 } => {
88 if list_enums && variants.len() <= 3 {
89 let list: Vec<String> =
90 variants.iter().map(|e| String::from(e.value)).collect();
91 list.join("|")
92 } else {
93 String::from("<enum>")
94 }
95 }
96 _ => String::from("<string>"),
97 },
98 _ => panic!("get_simple_type_text: expected simple type"),
99 }
100 }
101
102 /// Generate ReST Documentaion for object properties
103 pub fn dump_properties(
104 param: &dyn ObjectSchemaType,
105 indent: &str,
106 style: ParameterDisplayStyle,
107 skip: &[&str],
108 ) -> String {
109 let mut res = String::new();
110 let next_indent = format!(" {}", indent);
111
112 let mut required_list: Vec<String> = Vec::new();
113 let mut optional_list: Vec<String> = Vec::new();
114
115 for (prop, optional, schema) in param.properties() {
116 if skip.iter().any(|n| n == prop) {
117 continue;
118 }
119
120 let mut param_descr =
121 get_property_description(prop, schema, style, DocumentationFormat::ReST);
122
123 if !indent.is_empty() {
124 param_descr = format!("{}{}", indent, param_descr); // indent first line
125 param_descr = param_descr.replace('\n', &format!("\n{}", indent)); // indent rest
126 }
127
128 if style == ParameterDisplayStyle::Config {
129 if let Schema::String(StringSchema {
130 format: Some(ApiStringFormat::PropertyString(sub_schema)),
131 ..
132 }) = schema
133 {
134 match sub_schema {
135 Schema::Object(object_schema) => {
136 let sub_text = dump_properties(
137 object_schema,
138 &next_indent,
139 ParameterDisplayStyle::ConfigSub,
140 &[],
141 );
142 param_descr.push_str(&sub_text);
143 }
144 Schema::Array(_) => {
145 // do nothing - description should explain the list type
146 }
147 _ => unreachable!(),
148 }
149 }
150 }
151 if *optional {
152 optional_list.push(param_descr);
153 } else {
154 required_list.push(param_descr);
155 }
156 }
157
158 if !required_list.is_empty() {
159 if style != ParameterDisplayStyle::ConfigSub {
160 res.push_str("\n*Required properties:*\n\n");
161 }
162
163 for text in required_list {
164 res.push_str(&text);
165 res.push('\n');
166 }
167 }
168
169 if !optional_list.is_empty() {
170 if style != ParameterDisplayStyle::ConfigSub {
171 res.push_str("\n*Optional properties:*\n\n");
172 }
173
174 for text in optional_list {
175 res.push_str(&text);
176 res.push('\n');
177 }
178 }
179
180 res
181 }
182
183 /// Helper to format an object property, including name, type and description.
184 pub fn get_property_description(
185 name: &str,
186 schema: &Schema,
187 style: ParameterDisplayStyle,
188 format: DocumentationFormat,
189 ) -> String {
190 let type_text = get_schema_type_text(schema, style);
191
192 let (descr, default, extra) = match schema {
193 Schema::Null => ("null", None, None),
194 Schema::String(ref schema) => (
195 schema.description,
196 schema.default.map(|v| v.to_owned()),
197 None,
198 ),
199 Schema::Boolean(ref schema) => (
200 schema.description,
201 schema.default.map(|v| v.to_string()),
202 None,
203 ),
204 Schema::Integer(ref schema) => (
205 schema.description,
206 schema.default.map(|v| v.to_string()),
207 None,
208 ),
209 Schema::Number(ref schema) => (
210 schema.description,
211 schema.default.map(|v| v.to_string()),
212 None,
213 ),
214 Schema::Object(ref schema) => (schema.description, None, None),
215 Schema::AllOf(ref schema) => (schema.description, None, None),
216 Schema::Array(ref schema) => (
217 schema.description,
218 None,
219 Some(String::from("Can be specified more than once.")),
220 ),
221 };
222
223 let default_text = match default {
224 Some(text) => format!(" (default={})", text),
225 None => String::new(),
226 };
227
228 let descr = match extra {
229 Some(extra) => format!("{} {}", descr, extra),
230 None => String::from(descr),
231 };
232
233 if format == DocumentationFormat::ReST {
234 let mut text = match style {
235 ParameterDisplayStyle::Config => {
236 // reST definition list format
237 format!("``{}`` : ``{}{}``\n ", name, type_text, default_text)
238 }
239 ParameterDisplayStyle::ConfigSub => {
240 // reST definition list format
241 format!("``{}`` = ``{}{}``\n ", name, type_text, default_text)
242 }
243 ParameterDisplayStyle::Arg => {
244 // reST option list format
245 format!("``--{}`` ``{}{}``\n ", name, type_text, default_text)
246 }
247 ParameterDisplayStyle::Fixed => {
248 format!("``<{}>`` : ``{}{}``\n ", name, type_text, default_text)
249 }
250 };
251
252 text.push_str(&wrap_text("", " ", &descr, 80));
253 text.push('\n');
254
255 text
256 } else {
257 let display_name = match style {
258 ParameterDisplayStyle::Config => format!("{}:", name),
259 ParameterDisplayStyle::ConfigSub => format!("{}=", name),
260 ParameterDisplayStyle::Arg => format!("--{}", name),
261 ParameterDisplayStyle::Fixed => format!("<{}>", name),
262 };
263
264 let mut text = format!(" {:-10} {}{}", display_name, type_text, default_text);
265 let indent = " ";
266 text.push('\n');
267 text.push_str(&wrap_text(indent, indent, &descr, 80));
268
269 text
270 }
271 }
272
273 /// Helper to format the type text
274 ///
275 /// The result is a short string including important constraints, for
276 /// example ``<integer> (0 - N)``.
277 pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> String {
278 match schema {
279 Schema::Null => String::from("<null>"), // should not happen
280 Schema::String(string_schema) => {
281 match string_schema {
282 StringSchema {
283 type_text: Some(type_text),
284 ..
285 } => String::from(*type_text),
286 StringSchema {
287 format: Some(ApiStringFormat::Enum(variants)),
288 ..
289 } => {
290 let list: Vec<String> =
291 variants.iter().map(|e| String::from(e.value)).collect();
292 list.join("|")
293 }
294 // displaying regex add more confision than it helps
295 //StringSchema { format: Some(ApiStringFormat::Pattern(const_regex)), .. } => {
296 // format!("/{}/", const_regex.regex_string)
297 //}
298 StringSchema {
299 format: Some(ApiStringFormat::PropertyString(sub_schema)),
300 ..
301 } => get_property_string_type_text(sub_schema),
302 _ => String::from("<string>"),
303 }
304 }
305 Schema::Boolean(_) => String::from("<boolean>"),
306 Schema::Integer(integer_schema) => match (integer_schema.minimum, integer_schema.maximum) {
307 (Some(min), Some(max)) => format!("<integer> ({} - {})", min, max),
308 (Some(min), None) => format!("<integer> ({} - N)", min),
309 (None, Some(max)) => format!("<integer> (-N - {})", max),
310 _ => String::from("<integer>"),
311 },
312 Schema::Number(number_schema) => match (number_schema.minimum, number_schema.maximum) {
313 (Some(min), Some(max)) => format!("<number> ({} - {})", min, max),
314 (Some(min), None) => format!("<number> ({} - N)", min),
315 (None, Some(max)) => format!("<number> (-N - {})", max),
316 _ => String::from("<number>"),
317 },
318 Schema::Object(_) => String::from("<object>"),
319 Schema::Array(schema) => get_schema_type_text(schema.items, _style),
320 Schema::AllOf(_) => String::from("<object>"),
321 }
322 }
323
324 pub fn get_property_string_type_text(schema: &Schema) -> String {
325 match schema {
326 Schema::Object(object_schema) => get_object_type_text(object_schema),
327 Schema::Array(array_schema) => {
328 let item_type = get_simple_type_text(array_schema.items, true);
329 format!("[{}, ...]", item_type)
330 }
331 _ => panic!("get_property_string_type_text: expected array or object"),
332 }
333 }
334
335 fn get_object_type_text(object_schema: &ObjectSchema) -> String {
336 let mut parts = Vec::new();
337
338 let mut add_part = |name, optional, schema| {
339 let tt = get_simple_type_text(schema, false);
340 let text = if parts.is_empty() {
341 format!("{}={}", name, tt)
342 } else {
343 format!(",{}={}", name, tt)
344 };
345 if optional {
346 parts.push(format!("[{}]", text));
347 } else {
348 parts.push(text);
349 }
350 };
351
352 // add default key first
353 if let Some(ref default_key) = object_schema.default_key {
354 let (optional, schema) = object_schema.lookup(default_key).unwrap();
355 add_part(default_key, optional, schema);
356 }
357
358 // add required keys
359 for (name, optional, schema) in object_schema.properties {
360 if *optional {
361 continue;
362 }
363 if let Some(ref default_key) = object_schema.default_key {
364 if name == default_key {
365 continue;
366 }
367 }
368 add_part(name, *optional, schema);
369 }
370
371 // add options keys
372 for (name, optional, schema) in object_schema.properties {
373 if !*optional {
374 continue;
375 }
376 if let Some(ref default_key) = object_schema.default_key {
377 if name == default_key {
378 continue;
379 }
380 }
381 add_part(name, *optional, schema);
382 }
383
384 let mut type_text = String::new();
385 type_text.push('[');
386 type_text.push_str(&parts.join(" "));
387 type_text.push(']');
388 type_text
389 }
390
391 /// Generate ReST Documentaion for enumeration.
392 pub fn dump_enum_properties(schema: &Schema) -> Result<String, Error> {
393 let mut res = String::new();
394
395 if let Schema::String(StringSchema {
396 format: Some(ApiStringFormat::Enum(variants)),
397 ..
398 }) = schema
399 {
400 for item in variants.iter() {
401 use std::fmt::Write;
402
403 let _ = write!(res, ":``{}``: ", item.value);
404 let descr = wrap_text("", " ", item.description, 80);
405 res.push_str(&descr);
406 res.push('\n');
407 }
408 return Ok(res);
409 }
410
411 bail!("dump_enum_properties failed - not an enum");
412 }
413
414 pub fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle) -> String {
415 use std::fmt::Write;
416
417 let schema = &returns.schema;
418
419 let mut res = if returns.optional {
420 "*Returns* (optionally): ".to_string()
421 } else {
422 "*Returns*: ".to_string()
423 };
424
425 let type_text = get_schema_type_text(schema, style);
426 let _ = write!(res, "**{}**\n\n", type_text);
427
428 match schema {
429 Schema::Null => {
430 return res;
431 }
432 Schema::Boolean(schema) => {
433 let description = wrap_text("", "", schema.description, 80);
434 res.push_str(&description);
435 }
436 Schema::Integer(schema) => {
437 let description = wrap_text("", "", schema.description, 80);
438 res.push_str(&description);
439 }
440 Schema::Number(schema) => {
441 let description = wrap_text("", "", schema.description, 80);
442 res.push_str(&description);
443 }
444 Schema::String(schema) => {
445 let description = wrap_text("", "", schema.description, 80);
446 res.push_str(&description);
447 }
448 Schema::Array(schema) => {
449 let description = wrap_text("", "", schema.description, 80);
450 res.push_str(&description);
451 }
452 Schema::Object(obj_schema) => {
453 let description = wrap_text("", "", obj_schema.description, 80);
454 res.push_str(&description);
455 res.push_str(&dump_properties(obj_schema, "", style, &[]));
456 }
457 Schema::AllOf(all_of_schema) => {
458 let description = wrap_text("", "", all_of_schema.description, 80);
459 res.push_str(&description);
460 res.push_str(&dump_properties(all_of_schema, "", style, &[]));
461 }
462 }
463
464 res.push('\n');
465
466 res
467 }