1 //! Module to generate and format API Documenation
3 use anyhow
::{bail, Error}
;
7 use crate::api
::router
::ReturnType
;
8 use crate::api
::schema
::*;
9 use crate::api
::{ApiHandler, ApiMethod}
;
11 /// Enumerate different styles to display parameters/properties.
12 #[derive(Copy, Clone, PartialEq)]
13 pub enum ParameterDisplayStyle
{
14 /// Used for properties in configuration files: ``key:``
16 /// Used for PropertyStings properties in configuration files
18 /// Used for command line options: ``--key``
20 /// Used for command line options passed as arguments: ``<key>``
24 /// CLI usage information format.
25 #[derive(Copy, Clone, PartialEq)]
26 pub enum DocumentationFormat
{
27 /// Text, command line only (one line).
29 /// Text, list all options.
31 /// Text, include description.
33 /// Like full, but in reStructuredText format.
37 /// Line wrapping to form simple list of paragraphs.
40 subsequent_indent
: &str,
44 let wrapper1
= textwrap
::Wrapper
::new(columns
)
45 .initial_indent(initial_indent
)
46 .subsequent_indent(subsequent_indent
);
48 let wrapper2
= textwrap
::Wrapper
::new(columns
)
49 .initial_indent(subsequent_indent
)
50 .subsequent_indent(subsequent_indent
);
54 .filter(|p
| !p
.is_empty())
55 .fold(String
::new(), |mut acc
, p
| {
57 acc
.push_str(&wrapper1
.wrap(p
).join("\n"));
59 acc
.push_str(&wrapper2
.wrap(p
).join("\n"));
68 let text
= "Command. This may be a list in order to spefify nested sub-commands.";
69 let expect
= " Command. This may be a list in order to spefify nested sub-\n commands.\n\n";
72 let wrapped
= wrap_text(indent
, indent
, text
, 80);
74 assert_eq
!(wrapped
, expect
);
77 /// Helper to format the type text
79 /// The result is a short string including important constraints, for
80 /// example ``<integer> (0 - N)``.
81 pub fn get_schema_type_text(schema
: &Schema
, _style
: ParameterDisplayStyle
) -> String
{
83 Schema
::Null
=> String
::from("<null>"), // should not happen
84 Schema
::String(string_schema
) => {
86 StringSchema { type_text: Some(type_text), .. }
=> {
87 String
::from(*type_text
)
89 StringSchema { format: Some(ApiStringFormat::Enum(variants)), .. }
=> {
90 let list
: Vec
<String
> = variants
.iter().map(|e
| String
::from(e
.value
)).collect();
93 // displaying regex add more confision than it helps
94 //StringSchema { format: Some(ApiStringFormat::Pattern(const_regex)), .. } => {
95 // format!("/{}/", const_regex.regex_string)
97 StringSchema { format: Some(ApiStringFormat::PropertyString(sub_schema)), .. }
=> {
98 get_property_string_type_text(sub_schema
)
100 _
=> String
::from("<string>")
103 Schema
::Boolean(_
) => String
::from("<boolean>"),
104 Schema
::Integer(integer_schema
) => match (integer_schema
.minimum
, integer_schema
.maximum
) {
105 (Some(min
), Some(max
)) => format
!("<integer> ({} - {})", min
, max
),
106 (Some(min
), None
) => format
!("<integer> ({} - N)", min
),
107 (None
, Some(max
)) => format
!("<integer> (-N - {})", max
),
108 _
=> String
::from("<integer>"),
110 Schema
::Number(number_schema
) => match (number_schema
.minimum
, number_schema
.maximum
) {
111 (Some(min
), Some(max
)) => format
!("<number> ({} - {})", min
, max
),
112 (Some(min
), None
) => format
!("<number> ({} - N)", min
),
113 (None
, Some(max
)) => format
!("<number> (-N - {})", max
),
114 _
=> String
::from("<number>"),
116 Schema
::Object(_
) => String
::from("<object>"),
117 Schema
::Array(_
) => String
::from("<array>"),
118 Schema
::AllOf(_
) => String
::from("<object>"),
122 /// Helper to format an object property, including name, type and description.
123 pub fn get_property_description(
126 style
: ParameterDisplayStyle
,
127 format
: DocumentationFormat
,
129 let type_text
= get_schema_type_text(schema
, style
);
131 let (descr
, default) = match schema
{
132 Schema
::Null
=> ("null", None
),
133 Schema
::String(ref schema
) => (schema
.description
, schema
.default.map(|v
| v
.to_owned())),
134 Schema
::Boolean(ref schema
) => (schema
.description
, schema
.default.map(|v
| v
.to_string())),
135 Schema
::Integer(ref schema
) => (schema
.description
, schema
.default.map(|v
| v
.to_string())),
136 Schema
::Number(ref schema
) => (schema
.description
, schema
.default.map(|v
| v
.to_string())),
137 Schema
::Object(ref schema
) => (schema
.description
, None
),
138 Schema
::AllOf(ref schema
) => (schema
.description
, None
),
139 Schema
::Array(ref schema
) => (schema
.description
, None
),
142 let default_text
= match default {
143 Some(text
) => format
!(" (default={})", text
),
144 None
=> String
::new(),
147 if format
== DocumentationFormat
::ReST
{
148 let mut text
= match style
{
149 ParameterDisplayStyle
::Config
=> {
150 // reST definition list format
151 format
!("``{}`` : ``{}{}``\n ", name
, type_text
, default_text
)
153 ParameterDisplayStyle
::ConfigSub
=> {
154 // reST definition list format
155 format
!("``{}`` = ``{}{}``\n ", name
, type_text
, default_text
)
157 ParameterDisplayStyle
::Arg
=> {
158 // reST option list format
159 format
!("``--{}`` ``{}{}``\n ", name
, type_text
, default_text
)
161 ParameterDisplayStyle
::Fixed
=> {
162 format
!("``<{}>`` : ``{}{}``\n ", name
, type_text
, default_text
)
166 text
.push_str(&wrap_text("", " ", descr
, 80));
171 let display_name
= match style
{
172 ParameterDisplayStyle
::Config
=> format
!("{}:", name
),
173 ParameterDisplayStyle
::ConfigSub
=> format
!("{}=", name
),
174 ParameterDisplayStyle
::Arg
=> format
!("--{}", name
),
175 ParameterDisplayStyle
::Fixed
=> format
!("<{}>", name
),
178 let mut text
= format
!(" {:-10} {}{}", display_name
, type_text
, default_text
);
181 text
.push_str(&wrap_text(indent
, indent
, descr
, 80));
187 fn get_simply_type_text(
193 Schema
::Null
=> String
::from("<null>"), // should not happen
194 Schema
::Boolean(_
) => String
::from("<1|0>"),
195 Schema
::Integer(_
) => String
::from("<integer>"),
196 Schema
::Number(_
) => String
::from("<number>"),
197 Schema
::String(string_schema
) => {
198 match string_schema
{
199 StringSchema { type_text: Some(type_text), .. }
=> {
200 String
::from(*type_text
)
202 StringSchema { format: Some(ApiStringFormat::Enum(variants)), .. }
=> {
203 if list_enums
&& variants
.len() <= 3 {
204 let list
: Vec
<String
> = variants
.iter().map(|e
| String
::from(e
.value
)).collect();
207 String
::from("<enum>")
210 _
=> String
::from("<string>"),
213 _
=> panic
!("get_simply_type_text: expected simply type"),
217 fn get_object_type_text(object_schema
: &ObjectSchema
) -> String
{
219 let mut parts
= Vec
::new();
221 let mut add_part
= |name
, optional
, schema
| {
222 let tt
= get_simply_type_text(schema
, false);
223 let text
= if parts
.is_empty() {
224 format
!("{}={}", name
, tt
)
226 format
!(",{}={}", name
, tt
)
229 parts
.push(format
!("[{}]", text
));
235 // add default key first
236 if let Some(ref default_key
) = object_schema
.default_key
{
237 let (optional
, schema
) = object_schema
.lookup(default_key
).unwrap();
238 add_part(default_key
, optional
, schema
);
242 for (name
, optional
, schema
) in object_schema
.properties
{
243 if *optional { continue; }
244 if let Some(ref default_key
) = object_schema
.default_key
{
245 if name
== default_key { continue; }
247 add_part(name
, *optional
, schema
);
251 for (name
, optional
, schema
) in object_schema
.properties
{
252 if !*optional { continue; }
253 if let Some(ref default_key
) = object_schema
.default_key
{
254 if name
== default_key { continue; }
256 add_part(name
, *optional
, schema
);
259 let mut type_text
= String
::new();
261 type_text
.push_str(&parts
.join(" "));
266 fn get_property_string_type_text(
271 Schema
::Object(object_schema
) => {
272 get_object_type_text(object_schema
)
274 Schema
::Array(array_schema
) => {
275 let item_type
= get_simply_type_text(array_schema
.items
, true);
276 format
!("[{}, ...]", item_type
)
278 _
=> panic
!("get_property_string_type_text: expected array or object"),
282 /// Generate ReST Documentaion for enumeration.
283 pub fn dump_enum_properties(schema
: &Schema
) -> Result
<String
, Error
> {
285 let mut res
= String
::new();
287 if let Schema
::String(StringSchema
{
288 format
: Some(ApiStringFormat
::Enum(variants
)), ..
290 for item
in variants
.iter() {
291 res
.push_str(&format
!(":``{}``: ", item
.value
));
292 let descr
= wrap_text("", " ", item
.description
, 80);
293 res
.push_str(&descr
);
299 bail
!("dump_enum_properties failed - not an enum");
302 /// Generate ReST Documentaion for objects
303 pub fn dump_api_parameters
<I
>(
304 param
: &dyn ObjectSchemaType
<PropertyIter
= I
>,
306 style
: ParameterDisplayStyle
,
309 where I
: Iterator
<Item
= &'
static SchemaPropertyEntry
>,
311 let mut res
= wrap_text(indent
, indent
, param
.description(), 80);
313 let next_indent
= format
!(" {}", indent
);
315 let mut required_list
: Vec
<String
> = Vec
::new();
316 let mut optional_list
: Vec
<String
> = Vec
::new();
318 for (prop
, optional
, schema
) in param
.properties() {
320 if skip
.iter().find(|n
| n
== &prop
).is_some() { continue; }
322 let mut param_descr
= get_property_description(
326 DocumentationFormat
::ReST
,
329 if !indent
.is_empty() {
330 param_descr
= format
!("{}{}", indent
, param_descr
); // indent first line
331 param_descr
= param_descr
.replace("\n", &format
!("\n{}", indent
)); // indent rest
334 if style
== ParameterDisplayStyle
::Config
{
336 Schema
::String(StringSchema { format: Some(ApiStringFormat::PropertyString(sub_schema)), .. }
) => {
338 Schema
::Object(object_schema
) => {
339 let sub_text
= dump_api_parameters(
340 object_schema
, &next_indent
, ParameterDisplayStyle
::ConfigSub
, &[]);
341 param_descr
.push_str(&sub_text
);
343 Schema
::Array(_
) => {
344 // do nothing - description should explain the list type
349 _
=> { /* do nothing */ }
353 optional_list
.push(param_descr
);
355 required_list
.push(param_descr
);
359 if !required_list
.is_empty() {
360 if style
!= ParameterDisplayStyle
::ConfigSub
{
361 res
.push_str("\n*Required properties:*\n\n");
364 for text
in required_list
{
370 if !optional_list
.is_empty() {
371 if style
!= ParameterDisplayStyle
::ConfigSub
{
372 res
.push_str("\n*Optional properties:*\n\n");
375 for text
in optional_list
{
384 fn dump_api_return_schema(
385 returns
: &ReturnType
,
386 style
: ParameterDisplayStyle
,
388 let schema
= &returns
.schema
;
390 let mut res
= if returns
.optional
{
391 "*Returns* (optionally): ".to_string()
393 "*Returns*: ".to_string()
396 let type_text
= get_schema_type_text(schema
, style
);
397 res
.push_str(&format
!("**{}**\n\n", type_text
));
403 Schema
::Boolean(schema
) => {
404 let description
= wrap_text("", "", schema
.description
, 80);
405 res
.push_str(&description
);
407 Schema
::Integer(schema
) => {
408 let description
= wrap_text("", "", schema
.description
, 80);
409 res
.push_str(&description
);
411 Schema
::Number(schema
) => {
412 let description
= wrap_text("", "", schema
.description
, 80);
413 res
.push_str(&description
);
415 Schema
::String(schema
) => {
416 let description
= wrap_text("", "", schema
.description
, 80);
417 res
.push_str(&description
);
419 Schema
::Array(schema
) => {
420 let description
= wrap_text("", "", schema
.description
, 80);
421 res
.push_str(&description
);
423 Schema
::Object(obj_schema
) => {
424 res
.push_str(&dump_api_parameters(obj_schema
, "", style
, &[]));
426 Schema
::AllOf(all_of_schema
) => {
427 res
.push_str(&dump_api_parameters(all_of_schema
, "", style
, &[]));
436 fn dump_method_definition(method
: &str, path
: &str, def
: Option
<&ApiMethod
>) -> Option
<String
> {
437 let style
= ParameterDisplayStyle
::Config
;
440 Some(api_method
) => {
441 let param_descr
= dump_api_parameters(&api_method
.parameters
, "", style
, &[]);
443 let return_descr
= dump_api_return_schema(&api_method
.returns
, style
);
445 let mut method
= method
;
447 if let ApiHandler
::AsyncHttp(_
) = api_method
.handler
{
448 method
= if method
== "POST" { "UPLOAD" }
else { method }
;
449 method
= if method
== "GET" { "DOWNLOAD" }
else { method }
;
453 "**{} {}**\n\n{}\n\n{}",
454 method
, path
, param_descr
, return_descr
461 /// Generate ReST Documentaion for a complete API defined by a ``Router``.
463 output
: &mut dyn Write
,
464 router
: &crate::api
::Router
,
467 ) -> Result
<(), Error
> {
468 use crate::api
::SubRoute
;
470 let mut cond_print
= |x
| -> Result
<_
, Error
> {
471 if let Some(text
) = x
{
473 writeln
!(output
, "-----\n")?
;
475 writeln
!(output
, "{}", text
)?
;
481 cond_print(dump_method_definition("GET", path
, router
.get
))?
;
482 cond_print(dump_method_definition("POST", path
, router
.post
))?
;
483 cond_print(dump_method_definition("PUT", path
, router
.put
))?
;
484 cond_print(dump_method_definition("DELETE", path
, router
.delete
))?
;
486 match &router
.subroute
{
487 None
=> return Ok(()),
488 Some(SubRoute
::MatchAll { router, param_name }
) => {
489 let sub_path
= if path
== "." {
490 format
!("<{}>", param_name
)
492 format
!("{}/<{}>", path
, param_name
)
494 dump_api(output
, router
, &sub_path
, pos
)?
;
496 Some(SubRoute
::Map(dirmap
)) => {
497 //let mut keys: Vec<&String> = map.keys().collect();
498 //keys.sort_unstable_by(|a, b| a.cmp(b));
499 for (key
, sub_router
) in dirmap
.iter() {
500 let sub_path
= if path
== "." {
503 format
!("{}/{}", path
, key
)
505 dump_api(output
, sub_router
, &sub_path
, pos
)?
;