From c633a6c33fc6d188aec17d3446d24ce995fb3709 Mon Sep 17 00:00:00 2001 From: Joel Wachsler Date: Tue, 12 Jul 2022 11:59:13 +0000 Subject: [PATCH] Split group method generation --- .../group/method/method_with_params.rs | 162 ++++++++ .../group/method/method_without_params.rs | 50 +++ .../src/generate/group/method/mod.rs | 382 +----------------- .../src/generate/group/method/return_type.rs | 173 ++++++++ 4 files changed, 395 insertions(+), 372 deletions(-) create mode 100644 qbittorrent-web-api-gen/src/generate/group/method/method_with_params.rs create mode 100644 qbittorrent-web-api-gen/src/generate/group/method/method_without_params.rs create mode 100644 qbittorrent-web-api-gen/src/generate/group/method/return_type.rs diff --git a/qbittorrent-web-api-gen/src/generate/group/method/method_with_params.rs b/qbittorrent-web-api-gen/src/generate/group/method/method_with_params.rs new file mode 100644 index 0000000..4e3f80c --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/method/method_with_params.rs @@ -0,0 +1,162 @@ +use case::CaseExt; +use quote::quote; + +use crate::{generate::util, parser, types}; + +use super::return_type::create_return_type; + +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) { + 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 { + 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 + }), + ) +} diff --git a/qbittorrent-web-api-gen/src/generate/group/method/method_without_params.rs b/qbittorrent-web-api-gen/src/generate/group/method/method_without_params.rs new file mode 100644 index 0000000..3f91f7f --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/method/method_without_params.rs @@ -0,0 +1,50 @@ +use crate::{generate::util, parser}; +use quote::quote; + +use super::return_type::create_return_type; + +pub fn create_method_without_params( + group: &parser::ApiGroup, + method: &parser::ApiMethod, + method_name: proc_macro2::Ident, + url: &str, +) -> (proc_macro2::TokenStream, Option) { + 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 { + 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, + ), + } +} diff --git a/qbittorrent-web-api-gen/src/generate/group/method/mod.rs b/qbittorrent-web-api-gen/src/generate/group/method/mod.rs index 449695f..764f8e5 100644 --- a/qbittorrent-web-api-gen/src/generate/group/method/mod.rs +++ b/qbittorrent-web-api-gen/src/generate/group/method/mod.rs @@ -1,9 +1,15 @@ -use std::collections::HashMap; +mod method_with_params; +mod method_without_params; +mod return_type; -use crate::{generate::util, parser, types}; +use crate::{generate::util, parser}; use case::CaseExt; -use quote::{format_ident, quote}; -use regex::Regex; +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, @@ -43,371 +49,3 @@ fn generate_method( 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) { - 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 { - 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: &[types::Type], - method_name: &proc_macro2::Ident, - url: &str, -) -> (proc_macro2::TokenStream, Option) { - 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 { - 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 { - types::Type::Number(types::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,)* - } - }, - )) - } - types::Type::String(types::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)* - }, - )) -} diff --git a/qbittorrent-web-api-gen/src/generate/group/method/return_type.rs b/qbittorrent-web-api-gen/src/generate/group/method/return_type.rs new file mode 100644 index 0000000..55ea88c --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/method/return_type.rs @@ -0,0 +1,173 @@ +use std::collections::HashMap; + +use case::CaseExt; +use quote::{quote, format_ident}; +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| { + 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 { + types::Type::Number(types::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,)* + } + }, + )) + } + types::Type::String(types::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)* + }, + )) +}