26: Refactor r=JoelWachsler a=JoelWachsler



Co-authored-by: Joel Wachsler <JoelWachsler@users.noreply.github.com>
This commit is contained in:
bors[bot] 2022-07-12 16:06:28 +00:00 committed by GitHub
commit 85bb6aee43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 915 additions and 783 deletions

View File

@ -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 = &param.get_type_info().name;
let name = util::to_ident(&n.to_snake());
let t = util::to_ident(&param.to_borrowed_type());
let builder_param = if param.should_borrow() {
quote! { &#t }
} else {
quote! { #t }
};
util::add_docs(
&param.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(&param.to_borrowed_type());
let (name, ..) = param_name(param);
let t = if param.should_borrow() {
quote! { &#t }
} else {
quote! { #t }
};
quote! { #name: #t }
}

View File

@ -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,
),
}
}

View 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),
}
}

View 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 &parameter.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 = &parameter.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()
)
}

View File

@ -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)
}
},
)
}
}

View 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
}
}
}

View 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
}
}
}

View File

@ -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 {

View File

@ -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(&param.get_type_info().name.to_snake());
let t = util::to_ident(&param.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(&param.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 = &param.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 = &param.get_type_info().name;
let name = util::to_ident(&n.to_snake());
let t = util::to_ident(&param.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(&param.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 &parameter.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 = &parameter.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(&parameter.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)*
},
))
}

View File

@ -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()
}

View 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)
}
}

View File

@ -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

View 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,
}
}

View File

@ -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),
}
}

View 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(
&parameter.columns[1],
&parameter.columns[0],
Some(parameter.columns[2].clone()),
&types,
)
.unwrap_or_else(|| panic!("Failed to parse type {}", &parameter.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()
}

View 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")
}

View 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,
}
}

View 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()
}

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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(
&parameter.columns[1],
&parameter.columns[0],
Some(parameter.columns[2].clone()),
&types,
)
.unwrap_or_else(|| panic!("Failed to parse type {}", &parameter.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,
})
}

View File

@ -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")
}

View File

@ -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);

View File

@ -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
}

View File

@ -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,
})
}

View 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(())
}

View File

@ -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");
}