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..5af6ef0 --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/method/method_with_params.rs @@ -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) { + 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 { + mandatory_params + .iter() + .map(|param| param_with_name(param)) + .collect() +} + +fn generate_mandatory_param_builder( + mandatory_params: &[&types::Type], +) -> Vec { + 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 { + 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 } +} 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..b705257 --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/method/method_without_params.rs @@ -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) { + 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, + ), + } +} diff --git a/qbittorrent-web-api-gen/src/generate/group/method/mod.rs b/qbittorrent-web-api-gen/src/generate/group/method/mod.rs new file mode 100644 index 0000000..3c2ea73 --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/method/mod.rs @@ -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) { + 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), + } +} 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..4fc68f5 --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/method/return_type.rs @@ -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 = 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, + 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)> { + 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, +) -> 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( + type_description: &types::TypeDescription, + name: &str, + f: F, +) -> Option<(String, Vec)> +where + F: Fn(&types::TypeDescriptions) -> proc_macro2::TokenStream, +{ + let enum_fields: Vec = type_description + .values + .iter() + .map(f) + .collect::>(); + + 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() + ) +} diff --git a/qbittorrent-web-api-gen/src/generate/group/method/send_method_builder.rs b/qbittorrent-web-api-gen/src/generate/group/method/send_method_builder.rs new file mode 100644 index 0000000..da0c44a --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/method/send_method_builder.rs @@ -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, + description: Option, + 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) -> 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) + } + }, + ) + } +} diff --git a/qbittorrent-web-api-gen/src/generate/group/mod.rs b/qbittorrent-web-api-gen/src/generate/group/mod.rs new file mode 100644 index 0000000..bf4c911 --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/group/mod.rs @@ -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) -> 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 + } + } +} diff --git a/qbittorrent-web-api-gen/src/generate/mod.rs b/qbittorrent-web-api-gen/src/generate/mod.rs new file mode 100644 index 0000000..9e68fa9 --- /dev/null +++ b/qbittorrent-web-api-gen/src/generate/mod.rs @@ -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 + } + } +} diff --git a/qbittorrent-web-api-gen/src/skeleton.rs b/qbittorrent-web-api-gen/src/generate/skeleton.rs similarity index 97% rename from qbittorrent-web-api-gen/src/skeleton.rs rename to qbittorrent-web-api-gen/src/generate/skeleton.rs index e08050e..fb74c23 100644 --- a/qbittorrent-web-api-gen/src/skeleton.rs +++ b/qbittorrent-web-api-gen/src/generate/skeleton.rs @@ -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 { diff --git a/qbittorrent-web-api-gen/src/util.rs b/qbittorrent-web-api-gen/src/generate/util.rs similarity index 100% rename from qbittorrent-web-api-gen/src/util.rs rename to qbittorrent-web-api-gen/src/generate/util.rs diff --git a/qbittorrent-web-api-gen/src/group.rs b/qbittorrent-web-api-gen/src/group.rs deleted file mode 100644 index b7809eb..0000000 --- a/qbittorrent-web-api-gen/src/group.rs +++ /dev/null @@ -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) -> 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) { - 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) { - 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: &[parser::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 { - 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)* - }, - )) -} diff --git a/qbittorrent-web-api-gen/src/lib.rs b/qbittorrent-web-api-gen/src/lib.rs index ae63a37..5953ad2 100644 --- a/qbittorrent-web-api-gen/src/lib.rs +++ b/qbittorrent-web-api-gen/src/lib.rs @@ -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() } diff --git a/qbittorrent-web-api-gen/src/md_parser/mod.rs b/qbittorrent-web-api-gen/src/md_parser.rs similarity index 100% rename from qbittorrent-web-api-gen/src/md_parser/mod.rs rename to qbittorrent-web-api-gen/src/md_parser.rs diff --git a/qbittorrent-web-api-gen/src/parser/group/description.rs b/qbittorrent-web-api-gen/src/parser/group/description.rs new file mode 100644 index 0000000..c47d531 --- /dev/null +++ b/qbittorrent-web-api-gen/src/parser/group/description.rs @@ -0,0 +1,17 @@ +use crate::md_parser; + +pub fn parse_group_description(content: &[md_parser::MdContent]) -> Option { + let return_desc = content + .iter() + .map(|row| row.inner_value_as_string()) + .collect::>() + .join("\n") + .trim() + .to_string(); + + if return_desc.is_empty() { + None + } else { + Some(return_desc) + } +} diff --git a/qbittorrent-web-api-gen/src/parser/group_parser/description.rs b/qbittorrent-web-api-gen/src/parser/group/method/description.rs similarity index 68% rename from qbittorrent-web-api-gen/src/parser/group_parser/description.rs rename to qbittorrent-web-api-gen/src/parser/group/method/description.rs index 0819ec2..73f3fce 100644 --- a/qbittorrent-web-api-gen/src/parser/group_parser/description.rs +++ b/qbittorrent-web-api-gen/src/parser/group/method/description.rs @@ -1,22 +1,6 @@ use crate::md_parser::MdContent; -pub fn get_group_description(content: &[MdContent]) -> Option { - let return_desc = content - .iter() - .map(|row| row.inner_value_as_string()) - .collect::>() - .join("\n") - .trim() - .to_string(); - - if return_desc.is_empty() { - None - } else { - Some(return_desc) - } -} - -pub fn get_method_description(content: &[MdContent]) -> Option { +pub fn parse_method_description(content: &[MdContent]) -> Option { let return_desc = content .iter() // skip until we get to the "Returns:" text diff --git a/qbittorrent-web-api-gen/src/parser/group/method/mod.rs b/qbittorrent-web-api-gen/src/parser/group/method/mod.rs new file mode 100644 index 0000000..3d374f8 --- /dev/null +++ b/qbittorrent-web-api-gen/src/parser/group/method/mod.rs @@ -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, + pub parameters: Option>, + pub return_type: Option, + pub url: String, +} + +pub fn parse_api_method(child: &md_parser::TokenTree) -> Option { + 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, + } +} diff --git a/qbittorrent-web-api-gen/src/parser/group/method/parameters.rs b/qbittorrent-web-api-gen/src/parser/group/method/parameters.rs new file mode 100644 index 0000000..ebbfb07 --- /dev/null +++ b/qbittorrent-web-api-gen/src/parser/group/method/parameters.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; + +use crate::{md_parser, parser::types}; + +pub fn parse_parameters(content: &[md_parser::MdContent]) -> Option> { + 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, +) -> Option { + 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), + } +} diff --git a/qbittorrent-web-api-gen/src/parser/group/method/return_type.rs b/qbittorrent-web-api-gen/src/parser/group/method/return_type.rs new file mode 100644 index 0000000..6ff8342 --- /dev/null +++ b/qbittorrent-web-api-gen/src/parser/group/method/return_type.rs @@ -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, +} + +pub fn parse_return_type(content: &[MdContent]) -> Option { + 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 { + 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 { + table + .rows + .iter() + .map(|row| types::TypeDescriptions { + value: row.columns[0].to_string(), + description: row.columns[1].to_string(), + }) + .collect() +} diff --git a/qbittorrent-web-api-gen/src/parser/group/method/url.rs b/qbittorrent-web-api-gen/src/parser/group/method/url.rs new file mode 100644 index 0000000..5358362 --- /dev/null +++ b/qbittorrent-web-api-gen/src/parser/group/method/url.rs @@ -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") +} diff --git a/qbittorrent-web-api-gen/src/parser/group/mod.rs b/qbittorrent-web-api-gen/src/parser/group/mod.rs new file mode 100644 index 0000000..b7b4036 --- /dev/null +++ b/qbittorrent-web-api-gen/src/parser/group/mod.rs @@ -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, + pub description: Option, + 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, + } +} diff --git a/qbittorrent-web-api-gen/src/parser/group/url.rs b/qbittorrent-web-api-gen/src/parser/group/url.rs new file mode 100644 index 0000000..993c9b8 --- /dev/null +++ b/qbittorrent-web-api-gen/src/parser/group/url.rs @@ -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() +} diff --git a/qbittorrent-web-api-gen/src/parser/group_parser/mod.rs b/qbittorrent-web-api-gen/src/parser/group_parser/mod.rs deleted file mode 100644 index 7b8d799..0000000 --- a/qbittorrent-web-api-gen/src/parser/group_parser/mod.rs +++ /dev/null @@ -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) -> Vec { - 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 { - 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, - } -} diff --git a/qbittorrent-web-api-gen/src/parser/group_parser/parameters.rs b/qbittorrent-web-api-gen/src/parser/group_parser/parameters.rs deleted file mode 100644 index 2b6bc12..0000000 --- a/qbittorrent-web-api-gen/src/parser/group_parser/parameters.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::collections::HashMap; - -use crate::{ - md_parser::MdContent, - parser::types::{Type, OPTIONAL}, -}; - -pub fn get_parameters(content: &[MdContent]) -> Option> { - 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) -} diff --git a/qbittorrent-web-api-gen/src/parser/group_parser/return_type.rs b/qbittorrent-web-api-gen/src/parser/group_parser/return_type.rs deleted file mode 100644 index c79c929..0000000 --- a/qbittorrent-web-api-gen/src/parser/group_parser/return_type.rs +++ /dev/null @@ -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 { - 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, - }) -} diff --git a/qbittorrent-web-api-gen/src/parser/group_parser/url_parser.rs b/qbittorrent-web-api-gen/src/parser/group_parser/url_parser.rs deleted file mode 100644 index 43a5ac7..0000000 --- a/qbittorrent-web-api-gen/src/parser/group_parser/url_parser.rs +++ /dev/null @@ -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") -} diff --git a/qbittorrent-web-api-gen/src/parser/mod.rs b/qbittorrent-web-api-gen/src/parser/mod.rs index 9c4c0ae..8974168 100644 --- a/qbittorrent-web-api-gen/src/parser/mod.rs +++ b/qbittorrent-web-api-gen/src/parser/mod.rs @@ -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, - pub description: Option, - pub url: String, -} - -#[derive(Debug)] -pub struct ApiMethod { - pub name: String, - pub description: Option, - pub parameters: Option>, - pub return_type: Option, - pub url: String, -} - -#[derive(Debug)] -pub struct ReturnType { - pub is_list: bool, - pub parameters: Vec, -} - #[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 { - let relevant: Vec = tree +pub use group::{ApiGroup, ApiMethod, ReturnType}; + +pub fn parse_api_groups(token_tree: md_parser::TokenTree) -> Vec { + parse_groups(extract_relevant_parts(token_tree)) +} + +pub fn parse_groups(trees: Vec) -> Vec { + trees + .into_iter() + .map(|tree| parse_api_group(&tree)) + .collect() +} + +fn extract_relevant_parts(tree: md_parser::TokenTree) -> Vec { + let relevant: Vec = tree .children .into_iter() .skip_while(|row| match &row.title { @@ -55,18 +42,12 @@ fn extract_relevant_parts(tree: TokenTree) -> Vec { relevant } -pub fn parse_api_groups(content: &str) -> Vec { - 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); diff --git a/qbittorrent-web-api-gen/src/parser/object_types.rs b/qbittorrent-web-api-gen/src/parser/object_types.rs deleted file mode 100644 index a935c53..0000000 --- a/qbittorrent-web-api-gen/src/parser/object_types.rs +++ /dev/null @@ -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 { - 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 -} diff --git a/qbittorrent-web-api-gen/src/parser/util.rs b/qbittorrent-web-api-gen/src/parser/util.rs index 8cd9b9d..71e9160 100644 --- a/qbittorrent-web-api-gen/src/parser/util.rs +++ b/qbittorrent-web-api-gen/src/parser/util.rs @@ -2,26 +2,14 @@ use crate::md_parser::MdContent; pub fn find_content_starts_with(content: &[MdContent], starts_with: &str) -> Option { 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 { 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, }) } diff --git a/qbittorrent-web-api-gen/src/parser/types.rs b/qbittorrent-web-api-gen/src/types.rs similarity index 100% rename from qbittorrent-web-api-gen/src/parser/types.rs rename to qbittorrent-web-api-gen/src/types.rs diff --git a/qbittorrent-web-api-gen/tests/access_impl_types.rs b/qbittorrent-web-api-gen/tests/access_impl_types.rs new file mode 100644 index 0000000..cc59017 --- /dev/null +++ b/qbittorrent-web-api-gen/tests/access_impl_types.rs @@ -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(()) +} diff --git a/qbittorrent-web-api-gen/tests/tests.rs b/qbittorrent-web-api-gen/tests/tests.rs index b2bbc2b..61f2cd2 100644 --- a/qbittorrent-web-api-gen/tests/tests.rs +++ b/qbittorrent-web-api-gen/tests/tests.rs @@ -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"); }