Merge #26
26: Refactor r=JoelWachsler a=JoelWachsler Co-authored-by: Joel Wachsler <JoelWachsler@users.noreply.github.com>
This commit is contained in:
commit
85bb6aee43
|
@ -0,0 +1,156 @@
|
||||||
|
use case::CaseExt;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use crate::{generate::util, parser, types};
|
||||||
|
|
||||||
|
use super::{return_type::create_return_type, send_method_builder::SendMethodBuilder};
|
||||||
|
|
||||||
|
pub fn create_method_with_params(
|
||||||
|
group: &parser::ApiGroup,
|
||||||
|
method: &parser::ApiMethod,
|
||||||
|
params: &[types::Type],
|
||||||
|
method_name: &proc_macro2::Ident,
|
||||||
|
url: &str,
|
||||||
|
) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
|
||||||
|
let param_type = util::to_ident(&format!(
|
||||||
|
"{}{}Parameters",
|
||||||
|
group.name.to_camel(),
|
||||||
|
method.name.to_camel()
|
||||||
|
));
|
||||||
|
|
||||||
|
let mandatory_params = mandatory_params(params);
|
||||||
|
let mandatory_param_args = generate_mandatory_params(&mandatory_params);
|
||||||
|
|
||||||
|
let mandatory_param_names = mandatory_params.iter().map(|param| {
|
||||||
|
let (name, ..) = param_name(param);
|
||||||
|
quote! { #name }
|
||||||
|
});
|
||||||
|
|
||||||
|
let group_name = util::to_ident(&group.name.to_camel());
|
||||||
|
let send_builder =
|
||||||
|
SendMethodBuilder::new(&util::to_ident("send"), url, quote! { self.group.auth })
|
||||||
|
.with_form();
|
||||||
|
|
||||||
|
let generate_send_impl = |send_method: proc_macro2::TokenStream| {
|
||||||
|
let optional_params = generate_optional_params(params);
|
||||||
|
let mandatory_param_form_build = generate_mandatory_param_builder(&mandatory_params);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl<'a> #param_type<'a> {
|
||||||
|
fn new(group: &'a #group_name, #(#mandatory_param_args),*) -> Self {
|
||||||
|
let form = reqwest::multipart::Form::new();
|
||||||
|
#(#mandatory_param_form_build)*
|
||||||
|
Self { group, form }
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#optional_params)*
|
||||||
|
#send_method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let send = match create_return_type(group, method) {
|
||||||
|
Some((return_type_name, return_type)) => {
|
||||||
|
let send_impl = generate_send_impl(send_builder.return_type(&return_type_name).build());
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#send_impl
|
||||||
|
#return_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => generate_send_impl(send_builder.build()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let builder = util::add_docs(
|
||||||
|
&method.description,
|
||||||
|
quote! {
|
||||||
|
pub fn #method_name(&self, #(#mandatory_param_args),*) -> #param_type {
|
||||||
|
#param_type::new(self, #(#mandatory_param_names),*)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let group_impl = quote! {
|
||||||
|
pub struct #param_type<'a> {
|
||||||
|
group: &'a #group_name<'a>,
|
||||||
|
form: reqwest::multipart::Form,
|
||||||
|
}
|
||||||
|
|
||||||
|
#send
|
||||||
|
};
|
||||||
|
|
||||||
|
(builder, Some(group_impl))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_mandatory_params(mandatory_params: &[&types::Type]) -> Vec<proc_macro2::TokenStream> {
|
||||||
|
mandatory_params
|
||||||
|
.iter()
|
||||||
|
.map(|param| param_with_name(param))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_mandatory_param_builder(
|
||||||
|
mandatory_params: &[&types::Type],
|
||||||
|
) -> Vec<proc_macro2::TokenStream> {
|
||||||
|
mandatory_params
|
||||||
|
.iter()
|
||||||
|
.map(|param| {
|
||||||
|
let (name, name_as_str) = param_name(param);
|
||||||
|
quote! { let form = form.text(#name_as_str, #name.to_string()); }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_optional_params(params: &[types::Type]) -> Vec<proc_macro2::TokenStream> {
|
||||||
|
params
|
||||||
|
.iter()
|
||||||
|
.filter(|param| param.get_type_info().is_optional)
|
||||||
|
.map(generate_optional_param)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mandatory_params(params: &[types::Type]) -> Vec<&types::Type> {
|
||||||
|
params
|
||||||
|
.iter()
|
||||||
|
.filter(|param| !param.get_type_info().is_optional)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_optional_param(param: &types::Type) -> proc_macro2::TokenStream {
|
||||||
|
let n = ¶m.get_type_info().name;
|
||||||
|
let name = util::to_ident(&n.to_snake());
|
||||||
|
let t = util::to_ident(¶m.to_borrowed_type());
|
||||||
|
let builder_param = if param.should_borrow() {
|
||||||
|
quote! { &#t }
|
||||||
|
} else {
|
||||||
|
quote! { #t }
|
||||||
|
};
|
||||||
|
|
||||||
|
util::add_docs(
|
||||||
|
¶m.get_type_info().description,
|
||||||
|
quote! {
|
||||||
|
pub fn #name(mut self, value: #builder_param) -> Self {
|
||||||
|
self.form = self.form.text(#n, value.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn param_name(param: &types::Type) -> (proc_macro2::Ident, String) {
|
||||||
|
let name_as_str = param.get_type_info().name.to_snake();
|
||||||
|
(util::to_ident(&name_as_str), name_as_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn param_with_name(param: &types::Type) -> proc_macro2::TokenStream {
|
||||||
|
let t = util::to_ident(¶m.to_borrowed_type());
|
||||||
|
|
||||||
|
let (name, ..) = param_name(param);
|
||||||
|
let t = if param.should_borrow() {
|
||||||
|
quote! { &#t }
|
||||||
|
} else {
|
||||||
|
quote! { #t }
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! { #name: #t }
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use super::{return_type::create_return_type, send_method_builder::SendMethodBuilder};
|
||||||
|
use crate::parser;
|
||||||
|
|
||||||
|
pub fn create_method_without_params(
|
||||||
|
group: &parser::ApiGroup,
|
||||||
|
method: &parser::ApiMethod,
|
||||||
|
method_name: proc_macro2::Ident,
|
||||||
|
url: &str,
|
||||||
|
) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
|
||||||
|
let builder = SendMethodBuilder::new(&method_name, url, quote! { self.auth })
|
||||||
|
.description(&method.description);
|
||||||
|
|
||||||
|
match create_return_type(group, method) {
|
||||||
|
Some((return_type_name, return_type)) => (
|
||||||
|
builder.return_type(&return_type_name).build(),
|
||||||
|
Some(return_type),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
builder.build(),
|
||||||
|
// assume that all methods without a return type returns a string
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
52
qbittorrent-web-api-gen/src/generate/group/method/mod.rs
Normal file
52
qbittorrent-web-api-gen/src/generate/group/method/mod.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
mod method_with_params;
|
||||||
|
mod method_without_params;
|
||||||
|
mod return_type;
|
||||||
|
mod send_method_builder;
|
||||||
|
|
||||||
|
use crate::{generate::util, parser};
|
||||||
|
use case::CaseExt;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
method_with_params::create_method_with_params,
|
||||||
|
method_without_params::create_method_without_params,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn generate_methods(
|
||||||
|
group: &parser::ApiGroup,
|
||||||
|
auth: &syn::Ident,
|
||||||
|
group_name_camel: &syn::Ident,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
let methods_and_param_structs = group
|
||||||
|
.methods
|
||||||
|
.iter()
|
||||||
|
.map(|method| generate_method(group, method));
|
||||||
|
|
||||||
|
let methods = methods_and_param_structs.clone().map(|(method, ..)| method);
|
||||||
|
let structs = methods_and_param_structs.flat_map(|(_, s)| s);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl <'a> #group_name_camel<'a> {
|
||||||
|
pub fn new(auth: &'a #auth) -> Self {
|
||||||
|
Self { auth }
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#methods)*
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#structs)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_method(
|
||||||
|
group: &parser::ApiGroup,
|
||||||
|
method: &parser::ApiMethod,
|
||||||
|
) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
|
||||||
|
let method_name = util::to_ident(&method.name.to_snake());
|
||||||
|
let url = format!("/api/v2/{}/{}", group.url, method.url);
|
||||||
|
|
||||||
|
match &method.parameters {
|
||||||
|
Some(params) => create_method_with_params(group, method, params, &method_name, &url),
|
||||||
|
None => create_method_without_params(group, method, method_name, &url),
|
||||||
|
}
|
||||||
|
}
|
203
qbittorrent-web-api-gen/src/generate/group/method/return_type.rs
Normal file
203
qbittorrent-web-api-gen/src/generate/group/method/return_type.rs
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use case::CaseExt;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::{generate::util, parser, types};
|
||||||
|
|
||||||
|
pub fn create_return_type(
|
||||||
|
group: &parser::ApiGroup,
|
||||||
|
method: &parser::ApiMethod,
|
||||||
|
) -> Option<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
|
||||||
|
let return_type = match &method.return_type {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let to_enum_name = |name: &str| to_enum_name(&group.name, &method.name, name);
|
||||||
|
|
||||||
|
let enum_types_with_names: Vec<(String, proc_macro2::TokenStream)> =
|
||||||
|
create_enum_with_names(return_type, &group.name, &method.name);
|
||||||
|
|
||||||
|
let enum_names: HashMap<String, String> = enum_types_with_names
|
||||||
|
.iter()
|
||||||
|
.map(|(enum_name, _)| (enum_name.clone(), to_enum_name(enum_name)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let enum_types = enum_types_with_names.iter().map(|(_, enum_type)| enum_type);
|
||||||
|
|
||||||
|
let builder_fields = return_type
|
||||||
|
.parameters
|
||||||
|
.iter()
|
||||||
|
.map(|parameter| generate_builder_field(parameter, &enum_names));
|
||||||
|
|
||||||
|
let return_type_name = util::to_ident(&format!(
|
||||||
|
"{}{}Result",
|
||||||
|
&group.name.to_camel(),
|
||||||
|
&method.name.to_camel()
|
||||||
|
));
|
||||||
|
|
||||||
|
let result_type = if return_type.is_list {
|
||||||
|
quote! { std::vec::Vec<#return_type_name> }
|
||||||
|
} else {
|
||||||
|
quote! { #return_type_name }
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((
|
||||||
|
result_type,
|
||||||
|
quote! {
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct #return_type_name {
|
||||||
|
#(#builder_fields,)*
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#enum_types)*
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_enum_with_names(
|
||||||
|
return_type: &parser::ReturnType,
|
||||||
|
group_name: &str,
|
||||||
|
method_name: &str,
|
||||||
|
) -> Vec<(String, proc_macro2::TokenStream)> {
|
||||||
|
return_type
|
||||||
|
.parameters
|
||||||
|
.iter()
|
||||||
|
.flat_map(create_enum_fields)
|
||||||
|
.map(|(name, enum_fields)| create_enum(enum_fields, group_name, method_name, name))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_enum(
|
||||||
|
enum_fields: Vec<proc_macro2::TokenStream>,
|
||||||
|
group_name: &str,
|
||||||
|
method_name: &str,
|
||||||
|
name: String,
|
||||||
|
) -> (String, proc_macro2::TokenStream) {
|
||||||
|
let enum_name = util::to_ident(&to_enum_name(group_name, method_name, &name));
|
||||||
|
(
|
||||||
|
name,
|
||||||
|
quote! {
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum #enum_name {
|
||||||
|
#(#enum_fields,)*
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_enum_fields(
|
||||||
|
parameter: &parser::ReturnTypeParameter,
|
||||||
|
) -> Option<(String, Vec<proc_macro2::TokenStream>)> {
|
||||||
|
match ¶meter.return_type {
|
||||||
|
types::Type::Number(types::TypeInfo {
|
||||||
|
ref name,
|
||||||
|
type_description: Some(type_description),
|
||||||
|
..
|
||||||
|
}) => create_enum_field_value(type_description, name, create_number_enum_value),
|
||||||
|
types::Type::String(types::TypeInfo {
|
||||||
|
ref name,
|
||||||
|
type_description: Some(type_description),
|
||||||
|
..
|
||||||
|
}) => create_enum_field_value(type_description, name, create_string_enum_value),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_builder_field(
|
||||||
|
parameter: &parser::ReturnTypeParameter,
|
||||||
|
enum_names: &HashMap<String, String>,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
let namestr = ¶meter.name;
|
||||||
|
let name = util::to_ident(&namestr.to_snake().replace("__", "_"));
|
||||||
|
let enum_name = match enum_names.get(namestr) {
|
||||||
|
Some(enum_type) => enum_type.to_owned(),
|
||||||
|
None => parameter.return_type.to_owned_type(),
|
||||||
|
};
|
||||||
|
let rtype = util::to_ident(&enum_name);
|
||||||
|
let rtype_as_quote = if parameter.return_type.get_type_info().is_list {
|
||||||
|
quote! { std::vec::Vec<#rtype> }
|
||||||
|
} else {
|
||||||
|
quote! { #rtype }
|
||||||
|
};
|
||||||
|
let generate_field = |field_name| {
|
||||||
|
quote! {
|
||||||
|
#[serde(rename = #namestr)]
|
||||||
|
pub #field_name: #rtype_as_quote
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// "type" is a reserved keyword in Rust, so we just add "t_" to it.
|
||||||
|
if namestr == "type" {
|
||||||
|
generate_field(format_ident!("t_{}", name))
|
||||||
|
} else {
|
||||||
|
generate_field(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_enum_field_value<F>(
|
||||||
|
type_description: &types::TypeDescription,
|
||||||
|
name: &str,
|
||||||
|
f: F,
|
||||||
|
) -> Option<(String, Vec<proc_macro2::TokenStream>)>
|
||||||
|
where
|
||||||
|
F: Fn(&types::TypeDescriptions) -> proc_macro2::TokenStream,
|
||||||
|
{
|
||||||
|
let enum_fields: Vec<proc_macro2::TokenStream> = type_description
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.map(f)
|
||||||
|
.collect::<Vec<proc_macro2::TokenStream>>();
|
||||||
|
|
||||||
|
let nn = name.to_string();
|
||||||
|
|
||||||
|
Some((nn, enum_fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_string_enum_value(
|
||||||
|
type_description: &types::TypeDescriptions,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
let value = &type_description.value;
|
||||||
|
let value_as_ident = util::to_ident(&value.to_camel());
|
||||||
|
create_enum_field(&value_as_ident, value, &type_description.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_number_enum_value(value: &types::TypeDescriptions) -> proc_macro2::TokenStream {
|
||||||
|
let v = &value.value;
|
||||||
|
let re = Regex::new(r#"\(.*\)"#).unwrap();
|
||||||
|
let desc = &value
|
||||||
|
.description
|
||||||
|
.replace(' ', "_")
|
||||||
|
.replace('-', "_")
|
||||||
|
.replace(',', "_");
|
||||||
|
let desc_without_parentheses = re.replace_all(desc, "");
|
||||||
|
let ident = util::to_ident(&desc_without_parentheses.to_camel());
|
||||||
|
|
||||||
|
create_enum_field(&ident, v, &value.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_enum_field(
|
||||||
|
ident: &syn::Ident,
|
||||||
|
rename: &str,
|
||||||
|
description: &str,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
util::add_docs(
|
||||||
|
&Some(description.to_string()),
|
||||||
|
quote! {
|
||||||
|
#[serde(rename = #rename)]
|
||||||
|
#ident
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_enum_name(group_name: &str, method_name: &str, name: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
group_name.to_camel(),
|
||||||
|
method_name.to_camel(),
|
||||||
|
name.to_camel()
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use crate::generate::util;
|
||||||
|
|
||||||
|
pub struct SendMethodBuilder {
|
||||||
|
method_name: syn::Ident,
|
||||||
|
url: String,
|
||||||
|
auth_module_path: proc_macro2::TokenStream,
|
||||||
|
return_type: Option<proc_macro2::TokenStream>,
|
||||||
|
description: Option<String>,
|
||||||
|
form: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendMethodBuilder {
|
||||||
|
pub fn new(
|
||||||
|
method_name: &syn::Ident,
|
||||||
|
url: &str,
|
||||||
|
auth_module_path: proc_macro2::TokenStream,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
method_name: method_name.clone(),
|
||||||
|
url: url.to_string(),
|
||||||
|
auth_module_path,
|
||||||
|
return_type: None,
|
||||||
|
description: None,
|
||||||
|
form: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn return_type(mut self, value: &proc_macro2::TokenStream) -> Self {
|
||||||
|
self.return_type = Some(value.clone());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description(mut self, value: &Option<String>) -> Self {
|
||||||
|
self.description = value.clone();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_form(mut self) -> Self {
|
||||||
|
self.form = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&self) -> proc_macro2::TokenStream {
|
||||||
|
let method_name = &self.method_name;
|
||||||
|
let (return_type, parse_type) = match &self.return_type {
|
||||||
|
Some(t) => (t.clone(), quote! { .json::<#t>() }),
|
||||||
|
None => (quote! { String }, quote! { .text() }),
|
||||||
|
};
|
||||||
|
let url = &self.url;
|
||||||
|
let auth_module_path = &self.auth_module_path;
|
||||||
|
let form = if self.form {
|
||||||
|
quote! { .multipart(self.form) }
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
};
|
||||||
|
|
||||||
|
util::add_docs(
|
||||||
|
&self.description,
|
||||||
|
quote! {
|
||||||
|
pub async fn #method_name(self) -> Result<#return_type> {
|
||||||
|
let res = #auth_module_path
|
||||||
|
.authenticated_client(#url)
|
||||||
|
#form
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
#parse_type
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
49
qbittorrent-web-api-gen/src/generate/group/mod.rs
Normal file
49
qbittorrent-web-api-gen/src/generate/group/mod.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
mod method;
|
||||||
|
|
||||||
|
use crate::parser;
|
||||||
|
use case::CaseExt;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use self::method::generate_methods;
|
||||||
|
|
||||||
|
use super::{skeleton::auth_ident, util};
|
||||||
|
|
||||||
|
pub fn generate_groups(groups: Vec<parser::ApiGroup>) -> proc_macro2::TokenStream {
|
||||||
|
let gr = groups
|
||||||
|
.iter()
|
||||||
|
// implemented manually
|
||||||
|
.filter(|group| group.name != "authentication")
|
||||||
|
.map(generate_group);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#(#gr)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_group(group: &parser::ApiGroup) -> proc_macro2::TokenStream {
|
||||||
|
let group_name_camel = util::to_ident(&group.name.to_camel());
|
||||||
|
let group_name_snake = util::to_ident(&group.name.to_snake());
|
||||||
|
let auth = auth_ident();
|
||||||
|
let methods = generate_methods(group, &auth, &group_name_camel);
|
||||||
|
|
||||||
|
let group_method = util::add_docs(
|
||||||
|
&group.description,
|
||||||
|
quote! {
|
||||||
|
pub fn #group_name_snake(&self) -> #group_name_camel {
|
||||||
|
#group_name_camel::new(self)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
pub struct #group_name_camel<'a> {
|
||||||
|
auth: &'a #auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
#methods
|
||||||
|
|
||||||
|
impl #auth {
|
||||||
|
#group_method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
qbittorrent-web-api-gen/src/generate/mod.rs
Normal file
29
qbittorrent-web-api-gen/src/generate/mod.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
mod group;
|
||||||
|
mod skeleton;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use case::CaseExt;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use crate::{md_parser, parser};
|
||||||
|
|
||||||
|
use self::{group::generate_groups, skeleton::generate_skeleton};
|
||||||
|
|
||||||
|
pub fn generate(ast: &syn::DeriveInput, api_content: &str) -> TokenStream {
|
||||||
|
let ident = &ast.ident;
|
||||||
|
|
||||||
|
let token_tree = md_parser::TokenTreeFactory::create(api_content);
|
||||||
|
let api_groups = parser::parse_api_groups(token_tree);
|
||||||
|
|
||||||
|
let skeleton = generate_skeleton(ident);
|
||||||
|
let groups = generate_groups(api_groups);
|
||||||
|
let impl_ident = syn::Ident::new(&format!("{}_impl", ident).to_snake(), ident.span());
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
pub mod #impl_ident {
|
||||||
|
#skeleton
|
||||||
|
#groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
use crate::util;
|
use super::util;
|
||||||
|
|
||||||
pub const AUTH_IDENT: &str = "Authenticated";
|
|
||||||
|
|
||||||
pub fn auth_ident() -> proc_macro2::Ident {
|
pub fn auth_ident() -> proc_macro2::Ident {
|
||||||
util::to_ident(AUTH_IDENT)
|
util::to_ident("Authenticated")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_skeleton(ident: &syn::Ident) -> proc_macro2::TokenStream {
|
pub fn generate_skeleton(ident: &syn::Ident) -> proc_macro2::TokenStream {
|
|
@ -1,458 +0,0 @@
|
||||||
use std::{collections::HashMap, vec::Vec};
|
|
||||||
|
|
||||||
use case::CaseExt;
|
|
||||||
use quote::{format_ident, quote};
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
parser::{self, types::TypeInfo},
|
|
||||||
skeleton::auth_ident,
|
|
||||||
util,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn generate_groups(groups: Vec<parser::ApiGroup>) -> proc_macro2::TokenStream {
|
|
||||||
let gr = groups
|
|
||||||
.iter()
|
|
||||||
// implemented manually
|
|
||||||
.filter(|group| group.name != "authentication")
|
|
||||||
.map(generate_group);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#(#gr)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_group(group: &parser::ApiGroup) -> proc_macro2::TokenStream {
|
|
||||||
let group_name_camel = util::to_ident(&group.name.to_camel());
|
|
||||||
let group_name_snake = util::to_ident(&group.name.to_snake());
|
|
||||||
let auth = auth_ident();
|
|
||||||
let methods = generate_methods(group, &auth, &group_name_camel);
|
|
||||||
|
|
||||||
let group_method = util::add_docs(
|
|
||||||
&group.description,
|
|
||||||
quote! {
|
|
||||||
pub fn #group_name_snake(&self) -> #group_name_camel {
|
|
||||||
#group_name_camel::new(self)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
pub struct #group_name_camel<'a> {
|
|
||||||
auth: &'a #auth,
|
|
||||||
}
|
|
||||||
|
|
||||||
#methods
|
|
||||||
|
|
||||||
impl #auth {
|
|
||||||
#group_method
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_methods(
|
|
||||||
group: &parser::ApiGroup,
|
|
||||||
auth: &syn::Ident,
|
|
||||||
group_name_camel: &syn::Ident,
|
|
||||||
) -> proc_macro2::TokenStream {
|
|
||||||
let methods_and_param_structs = group
|
|
||||||
.methods
|
|
||||||
.iter()
|
|
||||||
.map(|method| generate_method(group, method));
|
|
||||||
|
|
||||||
let methods = methods_and_param_structs.clone().map(|(method, ..)| method);
|
|
||||||
let structs = methods_and_param_structs.flat_map(|(_, s)| s);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
impl <'a> #group_name_camel<'a> {
|
|
||||||
pub fn new(auth: &'a #auth) -> Self {
|
|
||||||
Self { auth }
|
|
||||||
}
|
|
||||||
|
|
||||||
#(#methods)*
|
|
||||||
}
|
|
||||||
|
|
||||||
#(#structs)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_method(
|
|
||||||
group: &parser::ApiGroup,
|
|
||||||
method: &parser::ApiMethod,
|
|
||||||
) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
|
|
||||||
let method_name = util::to_ident(&method.name.to_snake());
|
|
||||||
let url = format!("/api/v2/{}/{}", group.url, method.url);
|
|
||||||
|
|
||||||
match &method.parameters {
|
|
||||||
Some(params) => create_method_with_params(group, method, params, &method_name, &url),
|
|
||||||
None => create_method_without_params(group, method, method_name, &url),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_method_without_params(
|
|
||||||
group: &parser::ApiGroup,
|
|
||||||
method: &parser::ApiMethod,
|
|
||||||
method_name: proc_macro2::Ident,
|
|
||||||
url: &str,
|
|
||||||
) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
|
|
||||||
match create_return_type(group, method) {
|
|
||||||
Some((return_type_name, return_type)) => (
|
|
||||||
util::add_docs(
|
|
||||||
&method.description,
|
|
||||||
quote! {
|
|
||||||
pub async fn #method_name(&self) -> Result<#return_type_name> {
|
|
||||||
let res = self.auth
|
|
||||||
.authenticated_client(#url)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<#return_type_name>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Some(return_type),
|
|
||||||
),
|
|
||||||
None => (
|
|
||||||
util::add_docs(
|
|
||||||
&method.description,
|
|
||||||
quote! {
|
|
||||||
pub async fn #method_name(&self) -> Result<String> {
|
|
||||||
let res = self.auth
|
|
||||||
.authenticated_client(#url)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
), // assume that all methods without a return type returns a string
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_method_with_params(
|
|
||||||
group: &parser::ApiGroup,
|
|
||||||
method: &parser::ApiMethod,
|
|
||||||
params: &[parser::types::Type],
|
|
||||||
method_name: &proc_macro2::Ident,
|
|
||||||
url: &str,
|
|
||||||
) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
|
|
||||||
let parameter_type = util::to_ident(&format!(
|
|
||||||
"{}{}Parameters",
|
|
||||||
group.name.to_camel(),
|
|
||||||
method.name.to_camel()
|
|
||||||
));
|
|
||||||
|
|
||||||
let mandatory_params = params
|
|
||||||
.iter()
|
|
||||||
.filter(|param| !param.get_type_info().is_optional);
|
|
||||||
|
|
||||||
let mandatory_param_args = mandatory_params.clone().map(|param| {
|
|
||||||
let name = util::to_ident(¶m.get_type_info().name.to_snake());
|
|
||||||
let t = util::to_ident(¶m.to_borrowed_type());
|
|
||||||
|
|
||||||
if param.should_borrow() {
|
|
||||||
quote! {
|
|
||||||
#name: &#t
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {
|
|
||||||
#name: #t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mandatory_param_names = mandatory_params.clone().map(|param| {
|
|
||||||
let name = util::to_ident(¶m.get_type_info().name.to_snake());
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#name
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mandatory_param_args_clone = mandatory_param_args.clone();
|
|
||||||
let mandatory_param_form_build = mandatory_params.map(|param| {
|
|
||||||
let n = ¶m.get_type_info().name;
|
|
||||||
let name = util::to_ident(&n.to_snake());
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
let form = form.text(#n, #name.to_string());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let optional_params = params
|
|
||||||
.iter()
|
|
||||||
.filter(|param| param.get_type_info().is_optional)
|
|
||||||
.map(|param| {
|
|
||||||
let n = ¶m.get_type_info().name;
|
|
||||||
let name = util::to_ident(&n.to_snake());
|
|
||||||
let t = util::to_ident(¶m.to_borrowed_type());
|
|
||||||
|
|
||||||
let method = if param.should_borrow() {
|
|
||||||
quote! {
|
|
||||||
pub fn #name(mut self, value: &#t) -> Self {
|
|
||||||
self.form = self.form.text(#n, value.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {
|
|
||||||
pub fn #name(mut self, value: #t) -> Self {
|
|
||||||
self.form = self.form.text(#n, value.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
util::add_docs(¶m.get_type_info().description, method)
|
|
||||||
});
|
|
||||||
|
|
||||||
let group_name = util::to_ident(&group.name.to_camel());
|
|
||||||
|
|
||||||
let send = match create_return_type(group, method) {
|
|
||||||
Some((return_type_name, return_type)) => {
|
|
||||||
quote! {
|
|
||||||
impl<'a> #parameter_type<'a> {
|
|
||||||
fn new(group: &'a #group_name, #(#mandatory_param_args),*) -> Self {
|
|
||||||
let form = reqwest::multipart::Form::new();
|
|
||||||
#(#mandatory_param_form_build)*
|
|
||||||
Self { group, form }
|
|
||||||
}
|
|
||||||
|
|
||||||
#(#optional_params)*
|
|
||||||
|
|
||||||
pub async fn send(self) -> Result<#return_type_name> {
|
|
||||||
let res = self.group
|
|
||||||
.auth
|
|
||||||
.authenticated_client(#url)
|
|
||||||
.multipart(self.form)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<#return_type_name>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#return_type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
quote! {
|
|
||||||
impl<'a> #parameter_type<'a> {
|
|
||||||
fn new(group: &'a #group_name, #(#mandatory_param_args),*) -> Self {
|
|
||||||
let form = reqwest::multipart::Form::new();
|
|
||||||
#(#mandatory_param_form_build)*
|
|
||||||
Self { group, form }
|
|
||||||
}
|
|
||||||
|
|
||||||
#(#optional_params)*
|
|
||||||
|
|
||||||
pub async fn send(self) -> Result<String> {
|
|
||||||
let res = self.group
|
|
||||||
.auth
|
|
||||||
.authenticated_client(#url)
|
|
||||||
.multipart(self.form)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
util::add_docs(
|
|
||||||
&method.description,
|
|
||||||
quote! {
|
|
||||||
pub fn #method_name(&self, #(#mandatory_param_args_clone),*) -> #parameter_type {
|
|
||||||
#parameter_type::new(self, #(#mandatory_param_names),*)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Some(quote! {
|
|
||||||
pub struct #parameter_type<'a> {
|
|
||||||
group: &'a #group_name<'a>,
|
|
||||||
form: reqwest::multipart::Form,
|
|
||||||
}
|
|
||||||
|
|
||||||
#send
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_return_type(
|
|
||||||
group: &parser::ApiGroup,
|
|
||||||
method: &parser::ApiMethod,
|
|
||||||
) -> Option<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
|
|
||||||
let return_type = match &method.return_type {
|
|
||||||
Some(t) => t,
|
|
||||||
None => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let to_enum_name = |name: &str| {
|
|
||||||
format!(
|
|
||||||
"{}{}{}",
|
|
||||||
group.name.to_camel(),
|
|
||||||
method.name.to_camel(),
|
|
||||||
name.to_camel()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let enum_types_with_names =
|
|
||||||
return_type
|
|
||||||
.parameters
|
|
||||||
.iter()
|
|
||||||
.flat_map(|parameter| match ¶meter.return_type {
|
|
||||||
parser::types::Type::Number(TypeInfo {
|
|
||||||
ref name,
|
|
||||||
type_description: Some(type_description),
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
let enum_fields = type_description.values.iter().map(|value| {
|
|
||||||
let v = &value.value;
|
|
||||||
let re = Regex::new(r#"\(.*\)"#).unwrap();
|
|
||||||
let desc = &value
|
|
||||||
.description
|
|
||||||
.replace(' ', "_")
|
|
||||||
.replace('-', "_")
|
|
||||||
.replace(',', "_");
|
|
||||||
let desc_without_parentheses = re.replace_all(desc, "");
|
|
||||||
let ident = util::to_ident(&desc_without_parentheses.to_camel());
|
|
||||||
|
|
||||||
util::add_docs(
|
|
||||||
&Some(value.description.clone()),
|
|
||||||
quote! {
|
|
||||||
#[serde(rename = #v)]
|
|
||||||
#ident
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let enum_name = util::to_ident(&to_enum_name(name));
|
|
||||||
|
|
||||||
Some((
|
|
||||||
name,
|
|
||||||
quote! {
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
||||||
pub enum #enum_name {
|
|
||||||
#(#enum_fields,)*
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
parser::types::Type::String(TypeInfo {
|
|
||||||
ref name,
|
|
||||||
type_description: Some(type_description),
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
let enum_fields = type_description.values.iter().map(|type_description| {
|
|
||||||
let value = &type_description.value;
|
|
||||||
let value_as_ident = util::to_ident(&value.to_camel());
|
|
||||||
|
|
||||||
util::add_docs(
|
|
||||||
&Some(type_description.description.clone()),
|
|
||||||
quote! {
|
|
||||||
#[serde(rename = #value)]
|
|
||||||
#value_as_ident
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let enum_name = util::to_ident(&to_enum_name(name));
|
|
||||||
|
|
||||||
Some((
|
|
||||||
name,
|
|
||||||
quote! {
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
||||||
pub enum #enum_name {
|
|
||||||
#(#enum_fields,)*
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
let enum_names: HashMap<&String, String> = enum_types_with_names
|
|
||||||
.clone()
|
|
||||||
.map(|(enum_name, _)| (enum_name, to_enum_name(enum_name)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let enum_types = enum_types_with_names.map(|(_, enum_type)| enum_type);
|
|
||||||
|
|
||||||
let parameters = return_type.parameters.iter().map(|parameter| {
|
|
||||||
let namestr = ¶meter.name;
|
|
||||||
let name = util::to_ident(&namestr.to_snake().replace("__", "_"));
|
|
||||||
let rtype = if let Some(enum_type) = enum_names.get(namestr) {
|
|
||||||
util::to_ident(enum_type)
|
|
||||||
} else {
|
|
||||||
util::to_ident(¶meter.return_type.to_owned_type())
|
|
||||||
};
|
|
||||||
let type_info = parameter.return_type.get_type_info();
|
|
||||||
|
|
||||||
let rtype_as_quote = if type_info.is_list {
|
|
||||||
quote! {
|
|
||||||
std::vec::Vec<#rtype>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {
|
|
||||||
#rtype
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// "type" is a reserved keyword in Rust, so we use a different name.
|
|
||||||
if namestr == "type" {
|
|
||||||
let non_reserved_name = format_ident!("t_{}", name);
|
|
||||||
quote! {
|
|
||||||
#[serde(rename = #namestr)]
|
|
||||||
pub #non_reserved_name: #rtype_as_quote
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {
|
|
||||||
#[serde(rename = #namestr)]
|
|
||||||
pub #name: #rtype_as_quote
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let return_type_name = util::to_ident(&format!(
|
|
||||||
"{}{}Result",
|
|
||||||
&group.name.to_camel(),
|
|
||||||
&method.name.to_camel()
|
|
||||||
));
|
|
||||||
|
|
||||||
let result_type = if return_type.is_list {
|
|
||||||
quote! {
|
|
||||||
std::vec::Vec<#return_type_name>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {
|
|
||||||
#return_type_name
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Some((
|
|
||||||
result_type,
|
|
||||||
quote! {
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct #return_type_name {
|
|
||||||
#(#parameters,)*
|
|
||||||
}
|
|
||||||
|
|
||||||
#(#enum_types)*
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -1,35 +1,15 @@
|
||||||
mod group;
|
mod generate;
|
||||||
mod md_parser;
|
mod md_parser;
|
||||||
mod parser;
|
mod parser;
|
||||||
mod skeleton;
|
mod types;
|
||||||
mod util;
|
|
||||||
|
|
||||||
use case::CaseExt;
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
|
||||||
use skeleton::generate_skeleton;
|
|
||||||
use syn::parse_macro_input;
|
use syn::parse_macro_input;
|
||||||
|
|
||||||
use crate::group::generate_groups;
|
|
||||||
|
|
||||||
const API_CONTENT: &str = include_str!("../api-4_1.md");
|
const API_CONTENT: &str = include_str!("../api-4_1.md");
|
||||||
|
|
||||||
#[proc_macro_derive(QBittorrentApiGen, attributes(api_gen))]
|
#[proc_macro_derive(QBittorrentApiGen, attributes(api_gen))]
|
||||||
pub fn derive(input: TokenStream) -> TokenStream {
|
pub fn derive(input: TokenStream) -> TokenStream {
|
||||||
let ast = parse_macro_input!(input as syn::DeriveInput);
|
let ast = parse_macro_input!(input as syn::DeriveInput);
|
||||||
let ident = &ast.ident;
|
generate::generate(&ast, API_CONTENT).into()
|
||||||
|
|
||||||
let api_groups = parser::parse_api_groups(API_CONTENT);
|
|
||||||
|
|
||||||
let skeleton = generate_skeleton(ident);
|
|
||||||
let groups = generate_groups(api_groups);
|
|
||||||
let impl_ident = syn::Ident::new(&format!("{}_impl", ident).to_snake(), ident.span());
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
pub mod #impl_ident {
|
|
||||||
#skeleton
|
|
||||||
#groups
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
17
qbittorrent-web-api-gen/src/parser/group/description.rs
Normal file
17
qbittorrent-web-api-gen/src/parser/group/description.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use crate::md_parser;
|
||||||
|
|
||||||
|
pub fn parse_group_description(content: &[md_parser::MdContent]) -> Option<String> {
|
||||||
|
let return_desc = content
|
||||||
|
.iter()
|
||||||
|
.map(|row| row.inner_value_as_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n")
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if return_desc.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(return_desc)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +1,6 @@
|
||||||
use crate::md_parser::MdContent;
|
use crate::md_parser::MdContent;
|
||||||
|
|
||||||
pub fn get_group_description(content: &[MdContent]) -> Option<String> {
|
pub fn parse_method_description(content: &[MdContent]) -> Option<String> {
|
||||||
let return_desc = content
|
|
||||||
.iter()
|
|
||||||
.map(|row| row.inner_value_as_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n")
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if return_desc.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(return_desc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_method_description(content: &[MdContent]) -> Option<String> {
|
|
||||||
let return_desc = content
|
let return_desc = content
|
||||||
.iter()
|
.iter()
|
||||||
// skip until we get to the "Returns:" text
|
// skip until we get to the "Returns:" text
|
47
qbittorrent-web-api-gen/src/parser/group/method/mod.rs
Normal file
47
qbittorrent-web-api-gen/src/parser/group/method/mod.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
mod description;
|
||||||
|
mod parameters;
|
||||||
|
mod return_type;
|
||||||
|
mod url;
|
||||||
|
|
||||||
|
use crate::{md_parser, parser::util, types};
|
||||||
|
|
||||||
|
pub use return_type::ReturnType;
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
description::parse_method_description, parameters::parse_parameters,
|
||||||
|
return_type::parse_return_type, url::get_method_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApiMethod {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub parameters: Option<Vec<types::Type>>,
|
||||||
|
pub return_type: Option<ReturnType>,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_api_method(child: &md_parser::TokenTree) -> Option<ApiMethod> {
|
||||||
|
util::find_content_starts_with(&child.content, "Name: ")
|
||||||
|
.map(|name| {
|
||||||
|
name.trim_start_matches("Name: ")
|
||||||
|
.trim_matches('`')
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.map(|name| to_api_method(child, &name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_api_method(child: &md_parser::TokenTree, name: &str) -> ApiMethod {
|
||||||
|
let method_description = parse_method_description(&child.content);
|
||||||
|
let return_type = parse_return_type(&child.content);
|
||||||
|
let parameters = parse_parameters(&child.content);
|
||||||
|
let method_url = get_method_url(&child.content);
|
||||||
|
|
||||||
|
ApiMethod {
|
||||||
|
name: name.to_string(),
|
||||||
|
description: method_description,
|
||||||
|
parameters,
|
||||||
|
return_type,
|
||||||
|
url: method_url,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{md_parser, parser::types};
|
||||||
|
|
||||||
|
pub fn parse_parameters(content: &[md_parser::MdContent]) -> Option<Vec<types::Type>> {
|
||||||
|
let mut it = content
|
||||||
|
.iter()
|
||||||
|
.skip_while(|row| match row {
|
||||||
|
md_parser::MdContent::Asterix(content) | md_parser::MdContent::Text(content) => {
|
||||||
|
!content.starts_with("Parameters:")
|
||||||
|
}
|
||||||
|
_ => true,
|
||||||
|
})
|
||||||
|
// Parameters: <-- skip
|
||||||
|
// <-- skip
|
||||||
|
// table with parameters <-- take
|
||||||
|
.skip(2);
|
||||||
|
|
||||||
|
let parameter_table = match it.next() {
|
||||||
|
Some(md_parser::MdContent::Table(table)) => table,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// empty for now
|
||||||
|
let type_map = HashMap::default();
|
||||||
|
|
||||||
|
let table = parameter_table
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.flat_map(|row| parse_parameter(row, &type_map))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_parameter(
|
||||||
|
row: &md_parser::TableRow,
|
||||||
|
type_map: &HashMap<String, types::TypeDescription>,
|
||||||
|
) -> Option<types::Type> {
|
||||||
|
let description = row.columns.get(2).cloned();
|
||||||
|
|
||||||
|
match &row.columns.get(2) {
|
||||||
|
// If the description contains a default value it means that the parameter is optional.
|
||||||
|
Some(desc) if desc.contains("default: ") => {
|
||||||
|
// type defines a variable as default if it contains: _optional_
|
||||||
|
let name_with_optional = format!("{} {}", row.columns[0], types::OPTIONAL);
|
||||||
|
types::Type::from(&row.columns[1], &name_with_optional, description, type_map)
|
||||||
|
}
|
||||||
|
_ => types::Type::from(&row.columns[1], &row.columns[0], description, type_map),
|
||||||
|
}
|
||||||
|
}
|
100
qbittorrent-web-api-gen/src/parser/group/method/return_type.rs
Normal file
100
qbittorrent-web-api-gen/src/parser/group/method/return_type.rs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
md_parser::{self, MdContent},
|
||||||
|
parser::{types, ReturnTypeParameter},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ReturnType {
|
||||||
|
pub is_list: bool,
|
||||||
|
pub parameters: Vec<ReturnTypeParameter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_return_type(content: &[MdContent]) -> Option<ReturnType> {
|
||||||
|
let table = content
|
||||||
|
.iter()
|
||||||
|
// The response is a ... <-- Trying to find this line
|
||||||
|
// <-- The next line is empty
|
||||||
|
// Table with the return type <-- And then extract the following type table
|
||||||
|
.skip_while(|row| match row {
|
||||||
|
MdContent::Text(text) => !text.starts_with("The response is a"),
|
||||||
|
_ => true,
|
||||||
|
})
|
||||||
|
.find_map(|row| match row {
|
||||||
|
MdContent::Table(table) => Some(table),
|
||||||
|
_ => None,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let types = parse_object_types(content);
|
||||||
|
|
||||||
|
let parameters = table
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.map(|parameter| ReturnTypeParameter {
|
||||||
|
name: parameter.columns[0].clone(),
|
||||||
|
description: parameter.columns[2].clone(),
|
||||||
|
return_type: types::Type::from(
|
||||||
|
¶meter.columns[1],
|
||||||
|
¶meter.columns[0],
|
||||||
|
Some(parameter.columns[2].clone()),
|
||||||
|
&types,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|| panic!("Failed to parse type {}", ¶meter.columns[1])),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let is_list = content
|
||||||
|
.iter()
|
||||||
|
.find_map(|row| match row {
|
||||||
|
MdContent::Text(text) if text.starts_with("The response is a") => Some(text),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|found| found.contains("array"))
|
||||||
|
.unwrap_or_else(|| false);
|
||||||
|
|
||||||
|
Some(ReturnType {
|
||||||
|
parameters,
|
||||||
|
is_list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_object_types(
|
||||||
|
content: &[md_parser::MdContent],
|
||||||
|
) -> HashMap<String, types::TypeDescription> {
|
||||||
|
let mut output = HashMap::new();
|
||||||
|
let mut content_it = content.iter();
|
||||||
|
|
||||||
|
while let Some(entry) = content_it.next() {
|
||||||
|
if let md_parser::MdContent::Text(content) = entry {
|
||||||
|
const POSSIBLE_VALUES_OF: &str = "Possible values of ";
|
||||||
|
if content.contains(POSSIBLE_VALUES_OF) {
|
||||||
|
// is empty
|
||||||
|
content_it.next();
|
||||||
|
if let Some(md_parser::MdContent::Table(table)) = content_it.next() {
|
||||||
|
let enum_types = to_type_descriptions(table);
|
||||||
|
|
||||||
|
let name = content
|
||||||
|
.trim_start_matches(POSSIBLE_VALUES_OF)
|
||||||
|
.replace('`', "")
|
||||||
|
.replace(':', "");
|
||||||
|
|
||||||
|
output.insert(name, types::TypeDescription { values: enum_types });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_type_descriptions(table: &md_parser::Table) -> Vec<types::TypeDescriptions> {
|
||||||
|
table
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.map(|row| types::TypeDescriptions {
|
||||||
|
value: row.columns[0].to_string(),
|
||||||
|
description: row.columns[1].to_string(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
9
qbittorrent-web-api-gen/src/parser/group/method/url.rs
Normal file
9
qbittorrent-web-api-gen/src/parser/group/method/url.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use crate::{md_parser, parser::util};
|
||||||
|
|
||||||
|
pub fn get_method_url(content: &[md_parser::MdContent]) -> String {
|
||||||
|
const START: &str = "Name: ";
|
||||||
|
|
||||||
|
util::find_content_starts_with(content, START)
|
||||||
|
.map(|text| text.trim_start_matches(START).trim_matches('`').to_string())
|
||||||
|
.expect("Could find method url")
|
||||||
|
}
|
39
qbittorrent-web-api-gen/src/parser/group/mod.rs
Normal file
39
qbittorrent-web-api-gen/src/parser/group/mod.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
mod description;
|
||||||
|
mod method;
|
||||||
|
mod url;
|
||||||
|
|
||||||
|
use crate::md_parser;
|
||||||
|
|
||||||
|
use self::{description::parse_group_description, method::parse_api_method, url::get_group_url};
|
||||||
|
pub use method::{ApiMethod, ReturnType};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApiGroup {
|
||||||
|
pub name: String,
|
||||||
|
pub methods: Vec<ApiMethod>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_api_group(tree: &md_parser::TokenTree) -> ApiGroup {
|
||||||
|
let methods = tree.children.iter().flat_map(parse_api_method).collect();
|
||||||
|
|
||||||
|
let group_description = parse_group_description(&tree.content);
|
||||||
|
let group_url = get_group_url(&tree.content);
|
||||||
|
|
||||||
|
let name = tree
|
||||||
|
.title
|
||||||
|
.clone()
|
||||||
|
.unwrap()
|
||||||
|
.to_lowercase()
|
||||||
|
.trim_end_matches("(experimental)")
|
||||||
|
.trim()
|
||||||
|
.replace(' ', "_");
|
||||||
|
|
||||||
|
ApiGroup {
|
||||||
|
name,
|
||||||
|
methods,
|
||||||
|
description: group_description,
|
||||||
|
url: group_url,
|
||||||
|
}
|
||||||
|
}
|
14
qbittorrent-web-api-gen/src/parser/group/url.rs
Normal file
14
qbittorrent-web-api-gen/src/parser/group/url.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::{md_parser, parser::util};
|
||||||
|
|
||||||
|
pub fn get_group_url(content: &[md_parser::MdContent]) -> String {
|
||||||
|
let row = util::find_content_contains(content, "API methods are under")
|
||||||
|
.expect("Could not find api method");
|
||||||
|
|
||||||
|
let re = Regex::new(r#"All (?:\w+\s?)+ API methods are under "(\w+)", e.g."#)
|
||||||
|
.expect("Failed to create regex");
|
||||||
|
|
||||||
|
let res = re.captures(&row).expect("Failed find capture");
|
||||||
|
res[1].to_string()
|
||||||
|
}
|
|
@ -1,65 +0,0 @@
|
||||||
use crate::md_parser::TokenTree;
|
|
||||||
|
|
||||||
use self::{parameters::get_parameters, return_type::get_return_type};
|
|
||||||
|
|
||||||
use super::{util, ApiGroup, ApiMethod};
|
|
||||||
|
|
||||||
mod description;
|
|
||||||
mod parameters;
|
|
||||||
mod return_type;
|
|
||||||
mod url_parser;
|
|
||||||
|
|
||||||
pub fn parse_groups(trees: Vec<TokenTree>) -> Vec<ApiGroup> {
|
|
||||||
trees.into_iter().map(parse_api_group).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_api_group(tree: TokenTree) -> ApiGroup {
|
|
||||||
let methods = tree
|
|
||||||
.children
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(parse_api_method)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let group_description = description::get_group_description(&tree.content);
|
|
||||||
let group_url = url_parser::get_group_url(&tree.content);
|
|
||||||
|
|
||||||
let name = tree
|
|
||||||
.title
|
|
||||||
.unwrap()
|
|
||||||
.to_lowercase()
|
|
||||||
.trim_end_matches("(experimental)")
|
|
||||||
.trim()
|
|
||||||
.replace(' ', "_");
|
|
||||||
|
|
||||||
ApiGroup {
|
|
||||||
name,
|
|
||||||
methods,
|
|
||||||
description: group_description,
|
|
||||||
url: group_url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_api_method(child: TokenTree) -> Option<ApiMethod> {
|
|
||||||
util::find_content_starts_with(&child.content, "Name: ")
|
|
||||||
.map(|name| {
|
|
||||||
name.trim_start_matches("Name: ")
|
|
||||||
.trim_matches('`')
|
|
||||||
.to_string()
|
|
||||||
})
|
|
||||||
.map(|name| to_api_method(&child, &name))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_api_method(child: &TokenTree, name: &str) -> ApiMethod {
|
|
||||||
let method_description = description::get_method_description(&child.content);
|
|
||||||
let return_type = get_return_type(&child.content);
|
|
||||||
let parameters = get_parameters(&child.content);
|
|
||||||
let method_url = url_parser::get_method_url(&child.content);
|
|
||||||
|
|
||||||
ApiMethod {
|
|
||||||
name: name.to_string(),
|
|
||||||
description: method_description,
|
|
||||||
parameters,
|
|
||||||
return_type,
|
|
||||||
url: method_url,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
md_parser::MdContent,
|
|
||||||
parser::types::{Type, OPTIONAL},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get_parameters(content: &[MdContent]) -> Option<Vec<Type>> {
|
|
||||||
let mut it = content
|
|
||||||
.iter()
|
|
||||||
.skip_while(|row| match row {
|
|
||||||
MdContent::Asterix(content) | MdContent::Text(content) => {
|
|
||||||
!content.starts_with("Parameters:")
|
|
||||||
}
|
|
||||||
_ => true,
|
|
||||||
})
|
|
||||||
// Parameters: <-- skip
|
|
||||||
// <-- skip
|
|
||||||
// table with parameters <-- take
|
|
||||||
.skip(2);
|
|
||||||
|
|
||||||
let parameter_table = match it.next() {
|
|
||||||
Some(MdContent::Table(table)) => table,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// empty for now
|
|
||||||
let type_map = HashMap::default();
|
|
||||||
|
|
||||||
let table = parameter_table
|
|
||||||
.rows
|
|
||||||
.iter()
|
|
||||||
.flat_map(|row| {
|
|
||||||
let description = row.columns.get(2).cloned();
|
|
||||||
|
|
||||||
match &row.columns.get(2) {
|
|
||||||
// If the description contains a default value it means that the parameter is optional.
|
|
||||||
Some(desc) if desc.contains("default: ") => {
|
|
||||||
// type defines a variable as default if it contains: _optional_
|
|
||||||
let name_with_optional = format!("{} {}", row.columns[0], OPTIONAL);
|
|
||||||
Type::from(&row.columns[1], &name_with_optional, description, &type_map)
|
|
||||||
}
|
|
||||||
_ => Type::from(&row.columns[1], &row.columns[0], description, &type_map),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Some(table)
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
use crate::{
|
|
||||||
md_parser::MdContent,
|
|
||||||
parser::{object_types::get_object_types, types::Type, ReturnType, ReturnTypeParameter},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get_return_type(content: &[MdContent]) -> Option<ReturnType> {
|
|
||||||
let table = content
|
|
||||||
.iter()
|
|
||||||
// The response is a ... <-- Trying to find this line
|
|
||||||
// <-- The next line is empty
|
|
||||||
// Table with the return type <-- And then extract the following type table
|
|
||||||
.skip_while(|row| match row {
|
|
||||||
MdContent::Text(text) => !text.starts_with("The response is a"),
|
|
||||||
_ => true,
|
|
||||||
})
|
|
||||||
.find_map(|row| match row {
|
|
||||||
MdContent::Table(table) => Some(table),
|
|
||||||
_ => None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let types = get_object_types(content);
|
|
||||||
|
|
||||||
let parameters = table
|
|
||||||
.rows
|
|
||||||
.iter()
|
|
||||||
.map(|parameter| ReturnTypeParameter {
|
|
||||||
name: parameter.columns[0].clone(),
|
|
||||||
description: parameter.columns[2].clone(),
|
|
||||||
return_type: Type::from(
|
|
||||||
¶meter.columns[1],
|
|
||||||
¶meter.columns[0],
|
|
||||||
Some(parameter.columns[2].clone()),
|
|
||||||
&types,
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|| panic!("Failed to parse type {}", ¶meter.columns[1])),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let is_list = content
|
|
||||||
.iter()
|
|
||||||
.find_map(|row| match row {
|
|
||||||
MdContent::Text(text) if text.starts_with("The response is a") => Some(text),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.map(|found| found.contains("array"))
|
|
||||||
.unwrap_or_else(|| false);
|
|
||||||
|
|
||||||
Some(ReturnType {
|
|
||||||
parameters,
|
|
||||||
is_list,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::{md_parser::MdContent, parser::util};
|
|
||||||
|
|
||||||
pub fn get_group_url(content: &[MdContent]) -> String {
|
|
||||||
let row = util::find_content_contains(content, "API methods are under")
|
|
||||||
.expect("Could not find api method");
|
|
||||||
|
|
||||||
let re = Regex::new(r#"All (?:\w+\s?)+ API methods are under "(\w+)", e.g."#)
|
|
||||||
.expect("Failed to create regex");
|
|
||||||
|
|
||||||
let res = re.captures(&row).expect("Failed find capture");
|
|
||||||
res[1].to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_method_url(content: &[MdContent]) -> String {
|
|
||||||
const START: &str = "Name: ";
|
|
||||||
|
|
||||||
util::find_content_starts_with(content, START)
|
|
||||||
.map(|text| text.trim_start_matches(START).trim_matches('`').to_string())
|
|
||||||
.expect("Could find method url")
|
|
||||||
}
|
|
|
@ -1,45 +1,32 @@
|
||||||
mod group_parser;
|
use crate::{md_parser, types};
|
||||||
mod object_types;
|
|
||||||
pub mod types;
|
use self::group::parse_api_group;
|
||||||
|
|
||||||
|
mod group;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use group_parser::parse_groups;
|
|
||||||
use types::Type;
|
|
||||||
|
|
||||||
use crate::md_parser::{self, TokenTree};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ApiGroup {
|
|
||||||
pub name: String,
|
|
||||||
pub methods: Vec<ApiMethod>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ApiMethod {
|
|
||||||
pub name: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub parameters: Option<Vec<Type>>,
|
|
||||||
pub return_type: Option<ReturnType>,
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ReturnType {
|
|
||||||
pub is_list: bool,
|
|
||||||
pub parameters: Vec<ReturnTypeParameter>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ReturnTypeParameter {
|
pub struct ReturnTypeParameter {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub return_type: Type,
|
pub return_type: types::Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_relevant_parts(tree: TokenTree) -> Vec<TokenTree> {
|
pub use group::{ApiGroup, ApiMethod, ReturnType};
|
||||||
let relevant: Vec<TokenTree> = tree
|
|
||||||
|
pub fn parse_api_groups(token_tree: md_parser::TokenTree) -> Vec<ApiGroup> {
|
||||||
|
parse_groups(extract_relevant_parts(token_tree))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_groups(trees: Vec<md_parser::TokenTree>) -> Vec<ApiGroup> {
|
||||||
|
trees
|
||||||
|
.into_iter()
|
||||||
|
.map(|tree| parse_api_group(&tree))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_relevant_parts(tree: md_parser::TokenTree) -> Vec<md_parser::TokenTree> {
|
||||||
|
let relevant: Vec<md_parser::TokenTree> = tree
|
||||||
.children
|
.children
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.skip_while(|row| match &row.title {
|
.skip_while(|row| match &row.title {
|
||||||
|
@ -55,18 +42,12 @@ fn extract_relevant_parts(tree: TokenTree) -> Vec<TokenTree> {
|
||||||
relevant
|
relevant
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_api_groups(content: &str) -> Vec<ApiGroup> {
|
|
||||||
parse_groups(extract_relevant_parts(md_parser::TokenTreeFactory::create(
|
|
||||||
content,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
fn parse() -> TokenTree {
|
fn parse() -> md_parser::TokenTree {
|
||||||
let content = include_str!("../../api-4_1.md");
|
let content = include_str!("../../api-4_1.md");
|
||||||
let md_tree = md_parser::TokenTreeFactory::create(content);
|
let md_tree = md_parser::TokenTreeFactory::create(content);
|
||||||
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::{md_parser::MdContent, parser::types::TypeDescriptions};
|
|
||||||
|
|
||||||
use super::types::TypeDescription;
|
|
||||||
|
|
||||||
pub fn get_object_types(content: &[MdContent]) -> HashMap<String, TypeDescription> {
|
|
||||||
let mut output = HashMap::new();
|
|
||||||
let mut content_it = content.iter();
|
|
||||||
while let Some(entry) = content_it.next() {
|
|
||||||
if let MdContent::Text(content) = entry {
|
|
||||||
const POSSIBLE_VALUES_OF: &str = "Possible values of ";
|
|
||||||
if content.contains(POSSIBLE_VALUES_OF) {
|
|
||||||
// is empty
|
|
||||||
content_it.next();
|
|
||||||
if let Some(MdContent::Table(table)) = content_it.next() {
|
|
||||||
let enum_types = table
|
|
||||||
.rows
|
|
||||||
.iter()
|
|
||||||
.map(|row| TypeDescriptions {
|
|
||||||
value: row.columns[0].to_string(),
|
|
||||||
description: row.columns[1].to_string(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let name = content
|
|
||||||
.trim_start_matches(POSSIBLE_VALUES_OF)
|
|
||||||
.replace('`', "")
|
|
||||||
.replace(':', "");
|
|
||||||
|
|
||||||
output.insert(name, TypeDescription { values: enum_types });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
|
@ -2,26 +2,14 @@ use crate::md_parser::MdContent;
|
||||||
|
|
||||||
pub fn find_content_starts_with(content: &[MdContent], starts_with: &str) -> Option<String> {
|
pub fn find_content_starts_with(content: &[MdContent], starts_with: &str) -> Option<String> {
|
||||||
content.iter().find_map(|row| match row {
|
content.iter().find_map(|row| match row {
|
||||||
MdContent::Text(content) => {
|
MdContent::Text(content) if content.starts_with(starts_with) => Some(content.into()),
|
||||||
if content.starts_with(starts_with) {
|
|
||||||
Some(content.into())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_content_contains(content: &[MdContent], contains: &str) -> Option<String> {
|
pub fn find_content_contains(content: &[MdContent], contains: &str) -> Option<String> {
|
||||||
content.iter().find_map(|row| match row {
|
content.iter().find_map(|row| match row {
|
||||||
MdContent::Text(content) => {
|
MdContent::Text(content) if content.contains(contains) => Some(content.into()),
|
||||||
if content.contains(contains) {
|
|
||||||
Some(content.into())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
16
qbittorrent-web-api-gen/tests/access_impl_types.rs
Normal file
16
qbittorrent-web-api-gen/tests/access_impl_types.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
mod foo {
|
||||||
|
use qbittorrent_web_api_gen::QBittorrentApiGen;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(QBittorrentApiGen)]
|
||||||
|
struct Api {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let _ = foo::api_impl::ApplicationPreferencesBittorrentProtocol::TCP;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -18,4 +18,5 @@ fn tests() {
|
||||||
// --- Misc ---
|
// --- Misc ---
|
||||||
t.pass("tests/add_torrent.rs");
|
t.pass("tests/add_torrent.rs");
|
||||||
t.pass("tests/another_struct_name.rs");
|
t.pass("tests/another_struct_name.rs");
|
||||||
|
t.pass("tests/access_impl_types.rs");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user