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 crate::util;
|
||||
|
||||
pub const AUTH_IDENT: &str = "Authenticated";
|
||||
use super::util;
|
||||
|
||||
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 {
|
|
@ -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 parser;
|
||||
mod skeleton;
|
||||
mod util;
|
||||
mod types;
|
||||
|
||||
use case::CaseExt;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use skeleton::generate_skeleton;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
use crate::group::generate_groups;
|
||||
|
||||
const API_CONTENT: &str = include_str!("../api-4_1.md");
|
||||
|
||||
#[proc_macro_derive(QBittorrentApiGen, attributes(api_gen))]
|
||||
pub fn derive(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as syn::DeriveInput);
|
||||
let ident = &ast.ident;
|
||||
|
||||
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()
|
||||
generate::generate(&ast, API_CONTENT).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;
|
||||
|
||||
pub fn get_group_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> {
|
||||
pub fn parse_method_description(content: &[MdContent]) -> Option<String> {
|
||||
let return_desc = content
|
||||
.iter()
|
||||
// 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;
|
||||
mod object_types;
|
||||
pub mod types;
|
||||
use crate::{md_parser, types};
|
||||
|
||||
use self::group::parse_api_group;
|
||||
|
||||
mod group;
|
||||
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)]
|
||||
pub struct ReturnTypeParameter {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub return_type: Type,
|
||||
pub return_type: types::Type,
|
||||
}
|
||||
|
||||
fn extract_relevant_parts(tree: TokenTree) -> Vec<TokenTree> {
|
||||
let relevant: Vec<TokenTree> = tree
|
||||
pub use group::{ApiGroup, ApiMethod, ReturnType};
|
||||
|
||||
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
|
||||
.into_iter()
|
||||
.skip_while(|row| match &row.title {
|
||||
|
@ -55,18 +42,12 @@ fn extract_relevant_parts(tree: TokenTree) -> Vec<TokenTree> {
|
|||
relevant
|
||||
}
|
||||
|
||||
pub fn parse_api_groups(content: &str) -> Vec<ApiGroup> {
|
||||
parse_groups(extract_relevant_parts(md_parser::TokenTreeFactory::create(
|
||||
content,
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
fn parse() -> TokenTree {
|
||||
fn parse() -> md_parser::TokenTree {
|
||||
let content = include_str!("../../api-4_1.md");
|
||||
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> {
|
||||
content.iter().find_map(|row| match row {
|
||||
MdContent::Text(content) => {
|
||||
if content.starts_with(starts_with) {
|
||||
Some(content.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
MdContent::Text(content) if content.starts_with(starts_with) => Some(content.into()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_content_contains(content: &[MdContent], contains: &str) -> Option<String> {
|
||||
content.iter().find_map(|row| match row {
|
||||
MdContent::Text(content) => {
|
||||
if content.contains(contains) {
|
||||
Some(content.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
MdContent::Text(content) if content.contains(contains) => Some(content.into()),
|
||||
_ => 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 ---
|
||||
t.pass("tests/add_torrent.rs");
|
||||
t.pass("tests/another_struct_name.rs");
|
||||
t.pass("tests/access_impl_types.rs");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user