diff --git a/qbittorrent-web-api-gen/src/generate/api_group.rs b/qbittorrent-web-api-gen/src/generate/api_group.rs new file mode 100644 index 0000000..26abae1 --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/api_group.rs @@ -0,0 +1,89 @@ +use crate::parser; +use case::CaseExt; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +use super::{group_method::GroupMethod, skeleton::auth_ident, util}; + +impl parser::ApiGroup { + pub fn generate(&self) -> TokenStream { + let struct_name = self.struct_name(); + let group_name_snake = self.name_snake(); + let group_methods = self.generate_group_methods(); + + let group_struct = self.group_struct(); + let group_factory = self.group_factory(); + let auth = auth_ident(); + + quote! { + pub mod #group_name_snake { + impl <'a> #struct_name<'a> { + pub fn new(auth: &'a super::#auth) -> Self { + Self { auth } + } + } + + #group_struct + #group_factory + + #(#group_methods)* + } + } + } + + fn generate_group_methods(&self) -> Vec { + let group_methods = self.group_methods(); + group_methods + .iter() + .map(|group_method| group_method.generate_method()) + .collect() + } + + fn group_factory(&self) -> TokenStream { + let struct_name = self.struct_name(); + let name_snake = self.name_snake(); + let auth = auth_ident(); + + util::add_docs( + &self.description, + quote! { + impl super::#auth { + pub fn #name_snake(&self) -> #struct_name { + #struct_name::new(self) + } + } + }, + ) + } + + fn group_struct(&self) -> TokenStream { + let struct_name = self.struct_name(); + let auth = auth_ident(); + + quote! { + #[derive(Debug)] + pub struct #struct_name<'a> { + auth: &'a super::#auth, + } + } + } + + fn group_methods(&self) -> Vec { + self.methods + .iter() + .map(|method| GroupMethod::new(self, method)) + .collect() + } + + pub fn struct_name(&self) -> Ident { + self.name_camel() + } + + fn name_camel(&self) -> Ident { + util::to_ident(&self.name.to_camel()) + } + + fn name_snake(&self) -> Ident { + util::to_ident(&self.name.to_snake()) + } +} diff --git a/qbittorrent-web-api-gen/src/generate/api_method.rs b/qbittorrent-web-api-gen/src/generate/api_method.rs new file mode 100644 index 0000000..abdb8cf --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/api_method.rs @@ -0,0 +1,31 @@ +use case::CaseExt; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +use crate::parser; + +use super::util; + +impl parser::ApiMethod { + pub fn structs(&self) -> TokenStream { + let objects = self.types.objects(); + let structs = objects.iter().map(|obj| obj.generate_struct()); + + quote! { + #(#structs)* + } + } + + pub fn enums(&self) -> TokenStream { + let enums = self.types.enums(); + let generated_enums = enums.iter().map(|e| e.generate()); + + quote! { + #(#generated_enums)* + } + } + + pub fn name_snake(&self) -> Ident { + util::to_ident(&self.name.to_snake()) + } +} diff --git a/qbittorrent-web-api-gen/src/generate/group.rs b/qbittorrent-web-api-gen/src/generate/group.rs new file mode 100644 index 0000000..f2988bf --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group.rs @@ -0,0 +1,184 @@ +use crate::{parser, types}; +use case::CaseExt; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +use super::util; + +pub fn generate_groups(groups: Vec) -> TokenStream { + let gr = groups + .iter() + // implemented manually + .filter(|group| group.name != "authentication") + .map(generate_group); + + quote! { + #(#gr)* + } +} + +fn generate_group(group: &parser::ApiGroup) -> TokenStream { + let group = group.generate(); + + quote! { + #group + } +} + +impl parser::TypeWithName { + pub fn generate_struct(&self) -> TokenStream { + let fields = self.types.iter().map(|obj| obj.generate_struct_field()); + let name = util::to_ident(&self.name); + + quote! { + #[derive(Debug, serde::Deserialize)] + pub struct #name { + #(#fields,)* + } + } + } +} + +impl types::Type { + pub fn generate_struct_field(&self) -> TokenStream { + let name_snake = self.name_snake(); + let type_ = self.owned_type_ident(); + let orig_name = self.name(); + + util::add_docs( + &self.get_type_info().description, + quote! { + #[serde(rename = #orig_name)] + pub #name_snake: #type_ + }, + ) + } + + fn owned_type_ident(&self) -> TokenStream { + let owned_type = match self { + types::Type::Number(_) => quote! { i128 }, + types::Type::Float(_) => quote! { f32 }, + types::Type::Bool(_) => quote! { bool }, + types::Type::String(_) => quote! { String }, + types::Type::StringArray(_) => quote! { String }, + types::Type::Object(obj) => match &obj.ref_type { + types::RefType::String(str) => { + let str_ident = &util::to_ident(str); + quote! { #str_ident } + } + types::RefType::Map(key, value) => { + let key_ident = util::to_ident(key); + let value_ident = util::to_ident(value); + quote! { std::collections::HashMap<#key_ident, #value_ident> } + } + }, + }; + + if self.is_list() { + quote! { std::vec::Vec<#owned_type> } + } else { + owned_type + } + } + + fn name(&self) -> String { + self.get_type_info().name.clone() + } + + fn name_snake(&self) -> Ident { + util::to_ident(&self.name().to_snake()) + } +} + +impl parser::Enum { + pub fn generate(&self) -> TokenStream { + let values = self.values.iter().map(|enum_value| enum_value.generate()); + let name = util::to_ident(&self.name); + + quote! { + #[allow(clippy::enum_variant_names)] + #[derive(Debug, serde::Deserialize, PartialEq, Eq)] + pub enum #name { + #(#values,)* + } + } + } +} + +impl parser::EnumValue { + fn generate(&self) -> TokenStream { + util::add_docs(&self.description, self.generate_field()) + } + + fn generate_field(&self) -> TokenStream { + let orig_name = self.original_value.clone(); + + // special enum value which does not follow conventions + if orig_name == "\"/path/to/download/to\"" { + quote! { + PathToDownloadTo(String) + } + } else { + let name_camel = self.name_camel(); + quote! { + #[serde(rename = #orig_name)] + #name_camel + } + } + } + + fn name_camel(&self) -> Ident { + util::to_ident(&self.value.to_camel()) + } +} + +impl types::Type { + pub fn generate_optional_builder_method_with_docs(&self) -> TokenStream { + util::add_docs( + &self.get_type_info().description, + self.generate_optional_builder_method(), + ) + } + + fn borrowed_type_ident(&self) -> Ident { + util::to_ident(&self.to_borrowed_type()) + } + + pub fn to_parameter(&self) -> TokenStream { + let name_snake = self.name_snake(); + let borrowed_type = self.borrowed_type(); + + quote! { #name_snake: #borrowed_type } + } + + pub fn generate_form_builder(&self, add_to: TokenStream) -> TokenStream { + let name_str = self.name(); + let name_snake = self.name_snake(); + + quote! { + #add_to = #add_to.text(#name_str, #name_snake.to_string()); + } + } + + fn generate_optional_builder_method(&self) -> TokenStream { + let name_snake = self.name_snake(); + let borrowed_type = self.borrowed_type(); + let form_builder = self.generate_form_builder(quote! { self.form }); + + quote! { + pub fn #name_snake(mut self, #name_snake: #borrowed_type) -> Self { + #form_builder; + self + } + } + } + + fn borrowed_type(&self) -> TokenStream { + let type_ = self.borrowed_type_ident(); + if self.should_borrow() { + quote! { &#type_ } + } else { + quote! { #type_ } + } + } +} diff --git a/qbittorrent-web-api-gen/src/generate/group/mod.rs b/qbittorrent-web-api-gen/src/generate/group/mod.rs deleted file mode 100644 index 111b0fc..0000000 --- a/qbittorrent-web-api-gen/src/generate/group/mod.rs +++ /dev/null @@ -1,472 +0,0 @@ -use crate::{parser, types}; -use case::CaseExt; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; - -use super::{skeleton::auth_ident, util}; - -pub fn generate_groups(groups: Vec) -> TokenStream { - let gr = groups - .iter() - // implemented manually - .filter(|group| group.name != "authentication") - .map(generate_group); - - quote! { - #(#gr)* - } -} - -fn generate_group(group: &parser::ApiGroup) -> TokenStream { - let group = group.generate(); - - quote! { - #group - } -} - -impl parser::ApiGroup { - fn generate(&self) -> TokenStream { - let struct_name = self.struct_name(); - let group_name_snake = self.name_snake(); - let group_methods = self.generate_group_methods(); - - let group_struct = self.group_struct(); - let group_factory = self.group_factory(); - let auth = auth_ident(); - - quote! { - pub mod #group_name_snake { - impl <'a> #struct_name<'a> { - pub fn new(auth: &'a super::#auth) -> Self { - Self { auth } - } - } - - #group_struct - #group_factory - - #(#group_methods)* - } - } - } - - fn generate_group_methods(&self) -> Vec { - let group_methods = self.group_methods(); - group_methods - .iter() - .map(|group_method| group_method.generate_method()) - .collect() - } - - fn group_factory(&self) -> TokenStream { - let struct_name = self.struct_name(); - let name_snake = self.name_snake(); - let auth = auth_ident(); - - util::add_docs( - &self.description, - quote! { - impl super::#auth { - pub fn #name_snake(&self) -> #struct_name { - #struct_name::new(self) - } - } - }, - ) - } - - fn group_struct(&self) -> TokenStream { - let struct_name = self.struct_name(); - let auth = auth_ident(); - - quote! { - #[derive(Debug)] - pub struct #struct_name<'a> { - auth: &'a super::#auth, - } - } - } - - fn group_methods(&self) -> Vec { - self.methods - .iter() - .map(|method| GroupMethod::new(self, method)) - .collect() - } - - fn struct_name(&self) -> Ident { - self.name_camel() - } - - fn name_camel(&self) -> Ident { - util::to_ident(&self.name.to_camel()) - } - - fn name_snake(&self) -> Ident { - util::to_ident(&self.name.to_snake()) - } -} - -impl parser::ApiMethod { - fn structs(&self) -> TokenStream { - let objects = self.types.objects(); - let structs = objects.iter().map(|obj| obj.generate_struct()); - - quote! { - #(#structs)* - } - } - - fn enums(&self) -> TokenStream { - let enums = self.types.enums(); - let generated_enums = enums.iter().map(|e| e.generate()); - - quote! { - #(#generated_enums)* - } - } - - fn name_snake(&self) -> Ident { - util::to_ident(&self.name.to_snake()) - } -} - -impl parser::TypeWithName { - fn generate_struct(&self) -> TokenStream { - let fields = self.types.iter().map(|obj| obj.generate_struct_field()); - let name = util::to_ident(&self.name); - - quote! { - #[derive(Debug, serde::Deserialize)] - pub struct #name { - #(#fields,)* - } - } - } -} - -impl types::Type { - fn owned_type_ident(&self) -> TokenStream { - let owned_type = match self { - types::Type::Number(_) => quote! { i128 }, - types::Type::Float(_) => quote! { f32 }, - types::Type::Bool(_) => quote! { bool }, - types::Type::String(_) => quote! { String }, - types::Type::StringArray(_) => quote! { String }, - types::Type::Object(obj) => match &obj.ref_type { - types::RefType::String(str) => { - let str_ident = &util::to_ident(str); - quote! { #str_ident } - } - types::RefType::Map(key, value) => { - let key_ident = util::to_ident(key); - let value_ident = util::to_ident(value); - quote! { std::collections::HashMap<#key_ident, #value_ident> } - } - }, - }; - - if self.is_list() { - quote! { std::vec::Vec<#owned_type> } - } else { - owned_type - } - } - - fn generate_struct_field(&self) -> TokenStream { - let name_snake = self.name_snake(); - let type_ = self.owned_type_ident(); - let orig_name = self.name(); - - util::add_docs( - &self.get_type_info().description, - quote! { - #[serde(rename = #orig_name)] - pub #name_snake: #type_ - }, - ) - } - - fn name(&self) -> String { - self.get_type_info().name.clone() - } - - fn name_snake(&self) -> Ident { - util::to_ident(&self.name().to_snake()) - } -} - -impl parser::Enum { - fn generate(&self) -> TokenStream { - let values = self.values.iter().map(|enum_value| enum_value.generate()); - let name = util::to_ident(&self.name); - - quote! { - #[allow(clippy::enum_variant_names)] - #[derive(Debug, serde::Deserialize, PartialEq, Eq)] - pub enum #name { - #(#values,)* - } - } - } -} - -impl parser::EnumValue { - fn generate(&self) -> TokenStream { - util::add_docs(&self.description, self.generate_field()) - } - - fn generate_field(&self) -> TokenStream { - let orig_name = self.original_value.clone(); - - // special enum value which does not follow conventions - if orig_name == "\"/path/to/download/to\"" { - quote! { - PathToDownloadTo(String) - } - } else { - let name_camel = self.name_camel(); - quote! { - #[serde(rename = #orig_name)] - #name_camel - } - } - } - - fn name_camel(&self) -> Ident { - util::to_ident(&self.value.to_camel()) - } -} - -#[derive(Debug)] -struct GroupMethod<'a> { - group: &'a parser::ApiGroup, - method: &'a parser::ApiMethod, -} - -impl<'a> GroupMethod<'a> { - fn new(group: &'a parser::ApiGroup, method: &'a parser::ApiMethod) -> Self { - Self { group, method } - } - - fn generate_method(&self) -> TokenStream { - let method_name = self.method.name_snake(); - let structs = self.method.structs(); - let enums = self.method.enums(); - let builder = self.generate_request_builder(); - let response_struct = self.generate_response_struct(); - let request_method = self.generate_request_method(); - - quote! { - pub mod #method_name { - #structs - #enums - #builder - #response_struct - #request_method - } - } - } - - fn generate_request_method(&self) -> TokenStream { - let method_name = self.method.name_snake(); - - let parameters = self - .method - .types - .mandatory_params() - .iter() - .map(|param| param.to_parameter()) - .collect(); - - let form_builder = self.mandatory_parameters_as_form_builder(); - - let method_impl = if self.method.types.optional_parameters().is_empty() { - self.generate_send_method( - &method_name, - parameters, - quote! { self.auth }, - quote! { form }, - quote! { - let form = reqwest::multipart::Form::new(); - #form_builder - }, - ) - } else { - quote! { - pub fn #method_name(&self, #(#parameters),*) -> Builder<'_> { - let form = reqwest::multipart::Form::new(); - #form_builder - Builder { group: self, form } - } - } - }; - - let group_struct_name = self.group.struct_name(); - let method_impl_with_docs = util::add_docs(&self.method.description, method_impl); - - quote! { - impl<'a> super::#group_struct_name<'a> { - #method_impl_with_docs - } - } - } - - fn generate_response_struct(&self) -> TokenStream { - let response = match self.method.types.response() { - Some(res) => res, - None => return quote! {}, - }; - - let struct_fields = response - .types - .iter() - .map(|field| field.generate_struct_field()); - - quote! { - #[derive(Debug, serde::Deserialize)] - pub struct Response { - #(#struct_fields,)* - } - } - } - - /// Returns a TokenStream containing a request builder if there are optional - /// parameters, otherwise an empty TokenStream is returned. - fn generate_request_builder(&self) -> TokenStream { - let optional_params = self.method.types.optional_parameters(); - if optional_params.is_empty() { - return quote! {}; - } - - let builder_methods = optional_params - .iter() - .map(|param| param.generate_optional_builder_method_with_docs()); - - let group_name = self.group.struct_name(); - let send_method = self.generate_send_method( - &util::to_ident("send"), - vec![], - quote! { self.group.auth }, - quote! { self.form }, - quote! {}, - ); - - quote! { - pub struct Builder<'a> { - group: &'a super::#group_name<'a>, - form: reqwest::multipart::Form, - } - - impl<'a> Builder<'a> { - #send_method - #(#builder_methods)* - } - } - } - - fn generate_send_method( - &self, - method_name: &Ident, - parameters: Vec, - auth_access: TokenStream, - form_access: TokenStream, - form_factory: TokenStream, - ) -> TokenStream { - let method_url = format!("/api/v2/{}/{}", self.group.url, self.method.url); - - let (response_type, response_parse) = match self.method.types.response() { - Some(resp) => { - if resp.is_list { - ( - quote! { std::vec::Vec }, - quote! { .json::>() }, - ) - } else { - (quote! { Response }, quote! { .json::() }) - } - } - None => (quote! { String }, quote! { .text() }), - }; - - quote! { - pub async fn #method_name(self, #(#parameters),*) -> super::super::Result<#response_type> { - #form_factory - let res = #auth_access - .authenticated_client(#method_url) - .multipart(#form_access) - .send() - .await? - #response_parse - .await?; - - Ok(res) - } - } - } - - fn mandatory_parameters_as_form_builder(&self) -> TokenStream { - let builder = self - .method - .types - .mandatory_params() - .into_iter() - .map(|param| param.generate_form_builder(quote! { form })); - - quote! { - #(let #builder)* - } - } -} - -impl types::Type { - fn generate_optional_builder_method_with_docs(&self) -> TokenStream { - util::add_docs( - &self.get_type_info().description, - self.generate_optional_builder_method(), - ) - } - - fn borrowed_type_ident(&self) -> Ident { - util::to_ident(&self.to_borrowed_type()) - } - - fn to_parameter(&self) -> TokenStream { - let name_snake = self.name_snake(); - let borrowed_type = self.borrowed_type(); - - quote! { #name_snake: #borrowed_type } - } - - fn generate_form_builder(&self, add_to: TokenStream) -> TokenStream { - let name_str = self.name(); - let name_snake = self.name_snake(); - - quote! { - #add_to = #add_to.text(#name_str, #name_snake.to_string()); - } - } - - fn generate_optional_builder_method(&self) -> TokenStream { - let name_snake = self.name_snake(); - let borrowed_type = self.borrowed_type(); - let form_builder = self.generate_form_builder(quote! { self.form }); - - quote! { - pub fn #name_snake(mut self, #name_snake: #borrowed_type) -> Self { - #form_builder; - self - } - } - } - - fn borrowed_type(&self) -> TokenStream { - let type_ = self.borrowed_type_ident(); - if self.should_borrow() { - quote! { &#type_ } - } else { - quote! { #type_ } - } - } -} diff --git a/qbittorrent-web-api-gen/src/generate/group_method.rs b/qbittorrent-web-api-gen/src/generate/group_method.rs new file mode 100644 index 0000000..e8fb818 --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group_method.rs @@ -0,0 +1,186 @@ +use crate::parser; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +use super::util; + +#[derive(Debug)] +pub struct GroupMethod<'a> { + group: &'a parser::ApiGroup, + method: &'a parser::ApiMethod, +} + +impl<'a> GroupMethod<'a> { + pub fn new(group: &'a parser::ApiGroup, method: &'a parser::ApiMethod) -> Self { + Self { group, method } + } + + pub fn generate_method(&self) -> TokenStream { + let method_name = self.method.name_snake(); + let structs = self.method.structs(); + let enums = self.method.enums(); + let builder = self.generate_request_builder(); + let response_struct = self.generate_response_struct(); + let request_method = self.generate_request_method(); + + quote! { + pub mod #method_name { + #structs + #enums + #builder + #response_struct + #request_method + } + } + } + + fn generate_request_method(&self) -> TokenStream { + let method_name = self.method.name_snake(); + + let parameters = self + .method + .types + .mandatory_params() + .iter() + .map(|param| param.to_parameter()) + .collect(); + + let form_builder = self.mandatory_parameters_as_form_builder(); + + let method_impl = if self.method.types.optional_parameters().is_empty() { + self.generate_send_method( + &method_name, + parameters, + quote! { self.auth }, + quote! { form }, + quote! { + let form = reqwest::multipart::Form::new(); + #form_builder + }, + ) + } else { + quote! { + pub fn #method_name(&self, #(#parameters),*) -> Builder<'_> { + let form = reqwest::multipart::Form::new(); + #form_builder + Builder { group: self, form } + } + } + }; + + let group_struct_name = self.group.struct_name(); + let method_impl_with_docs = util::add_docs(&self.method.description, method_impl); + + quote! { + impl<'a> super::#group_struct_name<'a> { + #method_impl_with_docs + } + } + } + + fn generate_response_struct(&self) -> TokenStream { + let response = match self.method.types.response() { + Some(res) => res, + None => return quote! {}, + }; + + let struct_fields = response + .types + .iter() + .map(|field| field.generate_struct_field()); + + quote! { + #[derive(Debug, serde::Deserialize)] + pub struct Response { + #(#struct_fields,)* + } + } + } + + /// Returns a TokenStream containing a request builder if there are optional + /// parameters, otherwise an empty TokenStream is returned. + fn generate_request_builder(&self) -> TokenStream { + let optional_params = self.method.types.optional_parameters(); + if optional_params.is_empty() { + return quote! {}; + } + + let builder_methods = optional_params + .iter() + .map(|param| param.generate_optional_builder_method_with_docs()); + + let group_name = self.group.struct_name(); + let send_method = self.generate_send_method( + &util::to_ident("send"), + vec![], + quote! { self.group.auth }, + quote! { self.form }, + quote! {}, + ); + + quote! { + pub struct Builder<'a> { + group: &'a super::#group_name<'a>, + form: reqwest::multipart::Form, + } + + impl<'a> Builder<'a> { + #send_method + #(#builder_methods)* + } + } + } + + fn generate_send_method( + &self, + method_name: &Ident, + parameters: Vec, + auth_access: TokenStream, + form_access: TokenStream, + form_factory: TokenStream, + ) -> TokenStream { + let method_url = format!("/api/v2/{}/{}", self.group.url, self.method.url); + + let (response_type, response_parse) = match self.method.types.response() { + Some(resp) => { + if resp.is_list { + ( + quote! { std::vec::Vec }, + quote! { .json::>() }, + ) + } else { + (quote! { Response }, quote! { .json::() }) + } + } + None => (quote! { String }, quote! { .text() }), + }; + + quote! { + pub async fn #method_name(self, #(#parameters),*) -> super::super::Result<#response_type> { + #form_factory + let res = #auth_access + .authenticated_client(#method_url) + .multipart(#form_access) + .send() + .await? + #response_parse + .await?; + + Ok(res) + } + } + } + + fn mandatory_parameters_as_form_builder(&self) -> TokenStream { + let builder = self + .method + .types + .mandatory_params() + .into_iter() + .map(|param| param.generate_form_builder(quote! { form })); + + quote! { + #(let #builder)* + } + } +} diff --git a/qbittorrent-web-api-gen/src/generate/mod.rs b/qbittorrent-web-api-gen/src/generate/mod.rs index 9e68fa9..f6fb78d 100644 --- a/qbittorrent-web-api-gen/src/generate/mod.rs +++ b/qbittorrent-web-api-gen/src/generate/mod.rs @@ -1,6 +1,9 @@ mod group; +mod group_method; mod skeleton; mod util; +mod api_method; +mod api_group; use case::CaseExt; use proc_macro2::TokenStream;