2019-11-23 10:42:17 +01:00
//! Module to generate and format API Documenation
2019-12-05 13:36:05 +01:00
2021-02-08 09:13:55 +01:00
use anyhow ::{ bail , Error } ;
2019-11-22 09:28:22 +01:00
2021-10-07 09:36:06 +02:00
use crate ::* ;
2019-11-22 09:28:22 +01:00
2019-12-02 20:06:14 +01:00
/// Enumerate different styles to display parameters/properties.
2022-05-31 09:42:25 +02:00
#[ derive(Copy, Clone, PartialEq, Eq) ]
2019-11-22 09:28:22 +01:00
pub enum ParameterDisplayStyle {
2019-12-02 20:06:14 +01:00
/// Used for properties in configuration files: ``key:``
2019-11-22 09:28:22 +01:00
Config ,
2021-02-10 08:28:22 +01:00
/// Used for PropertyStings properties in configuration files
ConfigSub ,
2019-12-02 20:06:14 +01:00
/// Used for command line options: ``--key``
2019-11-22 09:28:22 +01:00
Arg ,
2019-12-02 20:06:14 +01:00
/// Used for command line options passed as arguments: ``<key>``
2019-11-22 09:28:22 +01:00
Fixed ,
}
2019-12-02 20:06:14 +01:00
/// CLI usage information format.
2022-05-31 09:42:25 +02:00
#[ derive(Copy, Clone, PartialEq, Eq) ]
2019-11-22 09:28:22 +01:00
pub enum DocumentationFormat {
2019-12-02 20:06:14 +01:00
/// Text, command line only (one line).
2019-11-22 09:28:22 +01:00
Short ,
2019-12-02 20:06:14 +01:00
/// Text, list all options.
2019-11-22 09:28:22 +01:00
Long ,
2019-12-02 20:06:14 +01:00
/// Text, include description.
2019-11-22 09:28:22 +01:00
Full ,
2019-12-02 20:06:14 +01:00
/// Like full, but in reStructuredText format.
2019-11-22 09:28:22 +01:00
ReST ,
}
/// Line wrapping to form simple list of paragraphs.
pub fn wrap_text (
initial_indent : & str ,
subsequent_indent : & str ,
text : & str ,
columns : usize ,
) -> String {
let wrapper1 = textwrap ::Wrapper ::new ( columns )
. initial_indent ( initial_indent )
. subsequent_indent ( subsequent_indent ) ;
let wrapper2 = textwrap ::Wrapper ::new ( columns )
. initial_indent ( subsequent_indent )
. subsequent_indent ( subsequent_indent ) ;
text . split ( " \n \n " )
. map ( | p | p . trim ( ) )
. filter ( | p | ! p . is_empty ( ) )
. fold ( String ::new ( ) , | mut acc , p | {
if acc . is_empty ( ) {
2019-12-01 12:29:26 +01:00
acc . push_str ( & wrapper1 . wrap ( p ) . join ( " \n " ) ) ;
2019-11-22 09:28:22 +01:00
} else {
2019-12-01 12:29:26 +01:00
acc . push_str ( & wrapper2 . wrap ( p ) . join ( " \n " ) ) ;
2019-11-22 09:28:22 +01:00
}
acc . push_str ( " \n \n " ) ;
acc
} )
}
2019-12-01 12:29:26 +01:00
#[ test ]
fn test_wrap_text ( ) {
let text = " Command. This may be a list in order to spefify nested sub-commands. " ;
let expect = " Command. This may be a list in order to spefify nested sub- \n commands. \n \n " ;
let indent = " " ;
let wrapped = wrap_text ( indent , indent , text , 80 ) ;
assert_eq! ( wrapped , expect ) ;
}
2021-10-07 09:36:06 +02:00
fn get_simple_type_text ( schema : & Schema , list_enums : bool ) -> String {
2019-11-22 09:28:22 +01:00
match schema {
Schema ::Null = > String ::from ( " <null> " ) , // should not happen
2021-10-07 09:36:06 +02:00
Schema ::Boolean ( _ ) = > String ::from ( " <1|0> " ) ,
Schema ::Integer ( _ ) = > String ::from ( " <integer> " ) ,
Schema ::Number ( _ ) = > String ::from ( " <number> " ) ,
Schema ::String ( string_schema ) = > match string_schema {
StringSchema {
type_text : Some ( type_text ) ,
..
} = > String ::from ( * type_text ) ,
StringSchema {
format : Some ( ApiStringFormat ::Enum ( variants ) ) ,
..
} = > {
if list_enums & & variants . len ( ) < = 3 {
2021-02-23 14:35:12 +01:00
let list : Vec < String > =
variants . iter ( ) . map ( | e | String ::from ( e . value ) ) . collect ( ) ;
2021-02-10 08:28:22 +01:00
list . join ( " | " )
2021-10-07 09:36:06 +02:00
} else {
String ::from ( " <enum> " )
2021-02-10 08:28:22 +01:00
}
2021-10-07 09:36:06 +02:00
}
_ = > String ::from ( " <string> " ) ,
} ,
_ = > panic! ( " get_simple_type_text: expected simple type " ) ,
}
}
/// Generate ReST Documentaion for object properties
pub fn dump_properties (
param : & dyn ObjectSchemaType ,
indent : & str ,
style : ParameterDisplayStyle ,
skip : & [ & str ] ,
) -> String {
let mut res = String ::new ( ) ;
let next_indent = format! ( " {} " , indent ) ;
let mut required_list : Vec < String > = Vec ::new ( ) ;
let mut optional_list : Vec < String > = Vec ::new ( ) ;
for ( prop , optional , schema ) in param . properties ( ) {
2021-12-02 08:58:10 +01:00
if skip . iter ( ) . any ( | n | n = = prop ) {
2021-10-07 09:36:06 +02:00
continue ;
}
let mut param_descr =
2021-12-02 08:58:10 +01:00
get_property_description ( prop , schema , style , DocumentationFormat ::ReST ) ;
2021-10-07 09:36:06 +02:00
if ! indent . is_empty ( ) {
param_descr = format! ( " {} {} " , indent , param_descr ) ; // indent first line
2022-05-31 09:42:25 +02:00
param_descr = param_descr . replace ( '\n' , & format! ( " \n {} " , indent ) ) ; // indent rest
2021-10-07 09:36:06 +02:00
}
if style = = ParameterDisplayStyle ::Config {
2021-12-02 09:01:52 +01:00
if let Schema ::String ( StringSchema {
2021-12-07 11:53:18 +01:00
format : Some ( ApiStringFormat ::PropertyString ( sub_schema ) ) ,
..
} ) = schema
{
2021-12-02 09:01:52 +01:00
match sub_schema {
Schema ::Object ( object_schema ) = > {
let sub_text = dump_properties (
object_schema ,
& next_indent ,
ParameterDisplayStyle ::ConfigSub ,
& [ ] ,
) ;
param_descr . push_str ( & sub_text ) ;
2021-10-07 09:36:06 +02:00
}
2021-12-02 09:01:52 +01:00
Schema ::Array ( _ ) = > {
// do nothing - description should explain the list type
}
_ = > unreachable! ( ) ,
2021-10-07 09:36:06 +02:00
}
2021-02-09 09:06:39 +01:00
}
}
2021-10-07 09:36:06 +02:00
if * optional {
optional_list . push ( param_descr ) ;
} else {
required_list . push ( param_descr ) ;
}
}
if ! required_list . is_empty ( ) {
if style ! = ParameterDisplayStyle ::ConfigSub {
res . push_str ( " \n *Required properties:* \n \n " ) ;
}
for text in required_list {
res . push_str ( & text ) ;
res . push ( '\n' ) ;
}
}
if ! optional_list . is_empty ( ) {
if style ! = ParameterDisplayStyle ::ConfigSub {
res . push_str ( " \n *Optional properties:* \n \n " ) ;
}
for text in optional_list {
res . push_str ( & text ) ;
res . push ( '\n' ) ;
}
2019-11-22 09:28:22 +01:00
}
2021-10-07 09:36:06 +02:00
res
2019-11-22 09:28:22 +01:00
}
2019-12-02 19:50:04 +01:00
/// Helper to format an object property, including name, type and description.
2019-11-22 09:28:22 +01:00
pub fn get_property_description (
name : & str ,
schema : & Schema ,
style : ParameterDisplayStyle ,
format : DocumentationFormat ,
) -> String {
let type_text = get_schema_type_text ( schema , style ) ;
2021-09-15 15:36:34 +02:00
let ( descr , default , extra ) = match schema {
Schema ::Null = > ( " null " , None , None ) ,
2021-10-07 09:36:06 +02:00
Schema ::String ( ref schema ) = > (
schema . description ,
schema . default . map ( | v | v . to_owned ( ) ) ,
None ,
) ,
Schema ::Boolean ( ref schema ) = > (
schema . description ,
schema . default . map ( | v | v . to_string ( ) ) ,
None ,
) ,
Schema ::Integer ( ref schema ) = > (
schema . description ,
schema . default . map ( | v | v . to_string ( ) ) ,
None ,
) ,
Schema ::Number ( ref schema ) = > (
schema . description ,
schema . default . map ( | v | v . to_string ( ) ) ,
None ,
) ,
2021-09-15 15:36:34 +02:00
Schema ::Object ( ref schema ) = > ( schema . description , None , None ) ,
Schema ::AllOf ( ref schema ) = > ( schema . description , None , None ) ,
2021-10-07 09:36:06 +02:00
Schema ::Array ( ref schema ) = > (
schema . description ,
None ,
Some ( String ::from ( " Can be specified more than once. " ) ) ,
) ,
2019-11-22 09:28:22 +01:00
} ;
let default_text = match default {
Some ( text ) = > format! ( " (default= {} ) " , text ) ,
None = > String ::new ( ) ,
} ;
2021-09-15 15:36:34 +02:00
let descr = match extra {
Some ( extra ) = > format! ( " {} {} " , descr , extra ) ,
None = > String ::from ( descr ) ,
} ;
2019-11-22 09:28:22 +01:00
if format = = DocumentationFormat ::ReST {
let mut text = match style {
ParameterDisplayStyle ::Config = > {
2021-02-09 08:53:08 +01:00
// reST definition list format
format! ( " `` {} `` : `` {} {} `` \n " , name , type_text , default_text )
2019-11-22 09:28:22 +01:00
}
2021-02-10 08:28:22 +01:00
ParameterDisplayStyle ::ConfigSub = > {
// reST definition list format
format! ( " `` {} `` = `` {} {} `` \n " , name , type_text , default_text )
}
2019-11-22 09:28:22 +01:00
ParameterDisplayStyle ::Arg = > {
2021-02-09 08:53:08 +01:00
// reST option list format
format! ( " ``-- {} `` `` {} {} `` \n " , name , type_text , default_text )
2019-11-22 09:28:22 +01:00
}
ParameterDisplayStyle ::Fixed = > {
2021-02-09 08:53:08 +01:00
format! ( " ``< {} >`` : `` {} {} `` \n " , name , type_text , default_text )
2019-11-22 09:28:22 +01:00
}
} ;
2021-09-15 15:36:34 +02:00
text . push_str ( & wrap_text ( " " , " " , & descr , 80 ) ) ;
2019-11-22 09:28:22 +01:00
text . push ( '\n' ) ;
text
} else {
let display_name = match style {
ParameterDisplayStyle ::Config = > format! ( " {} : " , name ) ,
2021-02-10 08:28:22 +01:00
ParameterDisplayStyle ::ConfigSub = > format! ( " {} = " , name ) ,
2019-11-22 09:28:22 +01:00
ParameterDisplayStyle ::Arg = > format! ( " -- {} " , name ) ,
ParameterDisplayStyle ::Fixed = > format! ( " < {} > " , name ) ,
} ;
let mut text = format! ( " {:-10} {} {} " , display_name , type_text , default_text ) ;
let indent = " " ;
text . push ( '\n' ) ;
2021-09-15 15:36:34 +02:00
text . push_str ( & wrap_text ( indent , indent , & descr , 80 ) ) ;
2019-11-22 09:28:22 +01:00
text
}
}
2021-10-07 09:36:06 +02:00
/// Helper to format the type text
///
/// The result is a short string including important constraints, for
/// example ``<integer> (0 - N)``.
pub fn get_schema_type_text ( schema : & Schema , _style : ParameterDisplayStyle ) -> String {
2021-02-10 08:28:22 +01:00
match schema {
Schema ::Null = > String ::from ( " <null> " ) , // should not happen
2021-10-07 09:36:06 +02:00
Schema ::String ( string_schema ) = > {
match string_schema {
StringSchema {
type_text : Some ( type_text ) ,
..
} = > String ::from ( * type_text ) ,
StringSchema {
format : Some ( ApiStringFormat ::Enum ( variants ) ) ,
..
} = > {
2021-02-23 14:35:12 +01:00
let list : Vec < String > =
variants . iter ( ) . map ( | e | String ::from ( e . value ) ) . collect ( ) ;
list . join ( " | " )
2021-02-10 08:28:22 +01:00
}
2021-10-07 09:36:06 +02:00
// displaying regex add more confision than it helps
//StringSchema { format: Some(ApiStringFormat::Pattern(const_regex)), .. } => {
// format!("/{}/", const_regex.regex_string)
//}
StringSchema {
format : Some ( ApiStringFormat ::PropertyString ( sub_schema ) ) ,
..
} = > get_property_string_type_text ( sub_schema ) ,
_ = > String ::from ( " <string> " ) ,
2021-02-10 08:28:22 +01:00
}
2021-10-07 09:36:06 +02:00
}
Schema ::Boolean ( _ ) = > String ::from ( " <boolean> " ) ,
Schema ::Integer ( integer_schema ) = > match ( integer_schema . minimum , integer_schema . maximum ) {
( Some ( min ) , Some ( max ) ) = > format! ( " <integer> ( {} - {} ) " , min , max ) ,
( Some ( min ) , None ) = > format! ( " <integer> ( {} - N) " , min ) ,
( None , Some ( max ) ) = > format! ( " <integer> (-N - {} ) " , max ) ,
_ = > String ::from ( " <integer> " ) ,
} ,
Schema ::Number ( number_schema ) = > match ( number_schema . minimum , number_schema . maximum ) {
( Some ( min ) , Some ( max ) ) = > format! ( " <number> ( {} - {} ) " , min , max ) ,
( Some ( min ) , None ) = > format! ( " <number> ( {} - N) " , min ) ,
( None , Some ( max ) ) = > format! ( " <number> (-N - {} ) " , max ) ,
_ = > String ::from ( " <number> " ) ,
2021-02-23 14:35:12 +01:00
} ,
2021-10-07 09:36:06 +02:00
Schema ::Object ( _ ) = > String ::from ( " <object> " ) ,
Schema ::Array ( schema ) = > get_schema_type_text ( schema . items , _style ) ,
Schema ::AllOf ( _ ) = > String ::from ( " <object> " ) ,
}
}
pub fn get_property_string_type_text ( schema : & Schema ) -> String {
match schema {
Schema ::Object ( object_schema ) = > get_object_type_text ( object_schema ) ,
Schema ::Array ( array_schema ) = > {
let item_type = get_simple_type_text ( array_schema . items , true ) ;
format! ( " [ {} , ...] " , item_type )
}
_ = > panic! ( " get_property_string_type_text: expected array or object " ) ,
2021-02-10 08:28:22 +01:00
}
}
fn get_object_type_text ( object_schema : & ObjectSchema ) -> String {
let mut parts = Vec ::new ( ) ;
let mut add_part = | name , optional , schema | {
2021-10-07 09:36:06 +02:00
let tt = get_simple_type_text ( schema , false ) ;
2021-02-10 08:28:22 +01:00
let text = if parts . is_empty ( ) {
format! ( " {} = {} " , name , tt )
} else {
format! ( " , {} = {} " , name , tt )
} ;
if optional {
parts . push ( format! ( " [ {} ] " , text ) ) ;
} else {
parts . push ( text ) ;
}
} ;
// add default key first
if let Some ( ref default_key ) = object_schema . default_key {
2021-02-23 14:35:12 +01:00
let ( optional , schema ) = object_schema . lookup ( default_key ) . unwrap ( ) ;
2021-02-10 08:28:22 +01:00
add_part ( default_key , optional , schema ) ;
}
// add required keys
for ( name , optional , schema ) in object_schema . properties {
2021-02-23 14:35:12 +01:00
if * optional {
continue ;
}
2021-02-10 08:28:22 +01:00
if let Some ( ref default_key ) = object_schema . default_key {
2021-02-23 14:35:12 +01:00
if name = = default_key {
continue ;
}
2021-02-10 08:28:22 +01:00
}
add_part ( name , * optional , schema ) ;
}
// add options keys
for ( name , optional , schema ) in object_schema . properties {
2021-02-23 14:35:12 +01:00
if ! * optional {
continue ;
}
2021-02-10 08:28:22 +01:00
if let Some ( ref default_key ) = object_schema . default_key {
2021-02-23 14:35:12 +01:00
if name = = default_key {
continue ;
}
2021-02-10 08:28:22 +01:00
}
add_part ( name , * optional , schema ) ;
}
let mut type_text = String ::new ( ) ;
type_text . push ( '[' ) ;
type_text . push_str ( & parts . join ( " " ) ) ;
type_text . push ( ']' ) ;
type_text
}
2021-02-08 09:13:55 +01:00
/// Generate ReST Documentaion for enumeration.
pub fn dump_enum_properties ( schema : & Schema ) -> Result < String , Error > {
let mut res = String ::new ( ) ;
if let Schema ::String ( StringSchema {
2021-02-23 14:35:12 +01:00
format : Some ( ApiStringFormat ::Enum ( variants ) ) ,
..
} ) = schema
{
2021-02-08 09:13:55 +01:00
for item in variants . iter ( ) {
2022-05-31 09:42:25 +02:00
use std ::fmt ::Write ;
let _ = write! ( res , " :``{}``: " , item . value ) ;
2021-02-08 09:13:55 +01:00
let descr = wrap_text ( " " , " " , item . description , 80 ) ;
res . push_str ( & descr ) ;
res . push ( '\n' ) ;
}
return Ok ( res ) ;
}
bail! ( " dump_enum_properties failed - not an enum " ) ;
}
2021-10-07 09:36:06 +02:00
pub fn dump_api_return_schema ( returns : & ReturnType , style : ParameterDisplayStyle ) -> String {
2022-05-31 09:42:25 +02:00
use std ::fmt ::Write ;
2020-12-18 12:25:50 +01:00
let schema = & returns . schema ;
let mut res = if returns . optional {
" *Returns* (optionally): " . to_string ( )
} else {
" *Returns*: " . to_string ( )
} ;
2019-11-22 09:28:22 +01:00
2021-02-10 08:28:22 +01:00
let type_text = get_schema_type_text ( schema , style ) ;
2022-05-31 09:42:25 +02:00
let _ = write! ( res , " **{}** \n \n " , type_text ) ;
2019-11-22 09:28:22 +01:00
match schema {
Schema ::Null = > {
return res ;
}
Schema ::Boolean ( schema ) = > {
let description = wrap_text ( " " , " " , schema . description , 80 ) ;
res . push_str ( & description ) ;
}
Schema ::Integer ( schema ) = > {
let description = wrap_text ( " " , " " , schema . description , 80 ) ;
res . push_str ( & description ) ;
}
2019-12-18 11:14:57 +01:00
Schema ::Number ( schema ) = > {
let description = wrap_text ( " " , " " , schema . description , 80 ) ;
res . push_str ( & description ) ;
}
2019-11-22 09:28:22 +01:00
Schema ::String ( schema ) = > {
let description = wrap_text ( " " , " " , schema . description , 80 ) ;
res . push_str ( & description ) ;
}
Schema ::Array ( schema ) = > {
let description = wrap_text ( " " , " " , schema . description , 80 ) ;
res . push_str ( & description ) ;
}
Schema ::Object ( obj_schema ) = > {
2021-02-11 10:32:39 +01:00
let description = wrap_text ( " " , " " , obj_schema . description , 80 ) ;
res . push_str ( & description ) ;
res . push_str ( & dump_properties ( obj_schema , " " , style , & [ ] ) ) ;
2019-11-22 09:28:22 +01:00
}
2020-12-18 12:25:52 +01:00
Schema ::AllOf ( all_of_schema ) = > {
2021-02-11 10:32:39 +01:00
let description = wrap_text ( " " , " " , all_of_schema . description , 80 ) ;
res . push_str ( & description ) ;
res . push_str ( & dump_properties ( all_of_schema , " " , style , & [ ] ) ) ;
2020-12-18 12:25:52 +01:00
}
2019-11-22 09:28:22 +01:00
}
res . push ( '\n' ) ;
res
}