This commit is contained in:
Joel Wachsler 2022-07-22 14:29:01 +00:00
parent a93e8e7a02
commit 68bb159dc3
10 changed files with 4391 additions and 5758 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,14 +1,11 @@
mod method;
use crate::parser;
use crate::{parser, types};
use case::CaseExt;
use proc_macro2::{Ident, TokenStream};
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 {
pub fn generate_groups(groups: Vec<parser::ApiGroup>) -> TokenStream {
let gr = groups
.iter()
// implemented manually
@ -20,30 +17,410 @@ pub fn generate_groups(groups: Vec<parser::ApiGroup>) -> proc_macro2::TokenStrea
}
}
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);
fn generate_group(group: &parser::ApiGroup) -> TokenStream {
let group = group.generate();
let group_method = util::add_docs(
&group.description,
quote! {
pub fn #group_name_snake(&self) -> #group_name_camel {
#group_name_camel::new(self)
#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<TokenStream> {
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! {
pub struct #group_name_camel<'a> {
auth: &'a #auth,
#[derive(Debug)]
pub struct #struct_name<'a> {
auth: &'a super::#auth,
}
}
}
#methods
fn group_methods(&self) -> Vec<GroupMethod> {
self.methods
.iter()
.map(|method| GroupMethod::new(self, method))
.collect()
}
impl #auth {
#group_method
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_camel(&self) -> Ident {
util::to_ident(&self.name.to_camel())
}
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 generate_struct_field(&self) -> TokenStream {
let name_snake = self.name_snake();
let type_name = util::to_ident(&self.to_owned_type());
let orig_name = self.name();
util::add_docs(
&self.get_type_info().description,
quote! {
#[serde(rename = #orig_name)]
#name_snake: #type_name
},
)
}
fn name(&self) -> String {
self.get_type_info().name.clone()
}
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::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())
}
fn name_snake(&self) -> Ident {
util::to_ident(&self.value.to_snake())
}
}
#[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_response_struct(&self) -> TokenStream {
let response = match self.method.types.response() {
Some(res) => res,
None => return quote! {},
};
let struct_fields = response.iter().map(|field| field.generate_struct_field());
quote! {
#[derive(Debug, serde::Deserialize)]
pub struct Response {
#(#struct_fields,)*
}
}
}
fn generate_optional_builder(&self) -> TokenStream {
let optional_params = match self.method.types.optional_parameters() {
Some(params) => params,
None => 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 mandatory_params = self.mandatory_parameters();
let mandatory_param_form_builder = self.mandatory_parameters_as_form_builder();
let send_method = self.generate_optional_builder_send_method();
quote! {
pub struct Builder<'a> {
group: &'a super::#group_name<'a>,
form: reqwest::multipart::Form,
}
impl<'a> Builder<'a> {
pub fn new(group: &'a super::#group_name, #mandatory_params) -> Self {
let form = reqwest::multipart::Form::new();
#mandatory_param_form_builder
Self { group, form }
}
#send_method
#(#builder_methods)*
}
}
}
fn generate_optional_builder_send_method(&self) -> TokenStream {
let method_url = format!("/api/v2/{}/{}", self.group.url, self.method.url);
match self.method.types.response() {
Some(_) => {
quote! {
pub async fn send(self) -> super::super::Result<Response> {
let res = self
.group
.auth
.authenticated_client(#method_url)
.multipart(self.form)
.send()
.await?
.json::<Response>()
.await?;
Ok(res)
}
}
}
None => {
quote! {
pub async fn send(self) -> super::super::Result<String> {
let res = self
.group
.auth
.authenticated_client(#method_url)
.multipart(self.form)
.send()
.await?
.text()
.await?;
Ok(res)
}
}
}
}
}
fn mandatory_parameters(&self) -> TokenStream {
let mandatory_params = match self.method.types.mandatory_params() {
Some(p) => p,
None => return quote! {},
};
let params = mandatory_params.iter().map(|param| param.to_parameter());
quote! {
#(#params),*
}
}
fn mandatory_parameters_as_form_builder(&self) -> TokenStream {
let mandatory_params = match self.method.types.mandatory_params() {
Some(p) => p,
None => return quote! {},
};
let builder = mandatory_params
.iter()
.map(|param| param.generate_form_builder(quote! { form }));
quote! {
#(let #builder)*
}
}
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_optional_builder();
let response_struct = self.generate_response_struct();
quote! {
pub mod #method_name {
#structs
#enums
#builder
#response_struct
}
}
}
}
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 {
if self.should_borrow() {
let type_ = self.borrowed_type_ident();
quote! { &#type_ }
} else {
let type_ = self.borrowed_type_ident();
quote! { #type_ }
}
}
}

View File

@ -10,13 +10,7 @@ pub fn generate_skeleton(ident: &syn::Ident) -> proc_macro2::TokenStream {
let auth = auth_ident();
quote! {
use reqwest::RequestBuilder;
use serde::Deserialize;
use thiserror::Error;
use super::#ident;
impl #ident {
impl super::#ident {
/// Creates an authenticated client.
/// base_url is the url to the qbittorrent instance, i.e. http://localhost:8080
pub async fn login(
@ -61,7 +55,7 @@ pub fn generate_skeleton(ident: &syn::Ident) -> proc_macro2::TokenStream {
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Error)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to parse auth cookie")]
AuthCookieParseError,
@ -81,7 +75,7 @@ pub fn generate_skeleton(ident: &syn::Ident) -> proc_macro2::TokenStream {
}
impl #auth {
fn authenticated_client(&self, url: &str) -> RequestBuilder {
fn authenticated_client(&self, url: &str) -> reqwest::RequestBuilder {
let url = format!("{}{}", self.base_url, url);
let cookie = self.auth_cookie.clone();

View File

@ -0,0 +1,37 @@
ApiMethod {
name: "foo",
description: None,
url: "foo",
types: CompositeTypes {
composite_types: [
Enum(
Enum {
name: "ScanDirs",
values: [
EnumValue {
description: Some(
"Download to the monitored folder",
),
value: "DownloadToTheMonitoredFolder",
original_value: "0",
},
EnumValue {
description: Some(
"Download to the default save path",
),
value: "DownloadToTheDefaultSavePath",
original_value: "1",
},
EnumValue {
description: Some(
"Download to this path",
),
value: "\"/path/to/download/to\"",
original_value: "\"/path/to/download/to\"",
},
],
},
),
],
},
}

View File

@ -0,0 +1,12 @@
## Testing
Name: `foo`
Possible values of `scan_dirs`:
Value | Description
----------------------------|------------
`0` | Download to the monitored folder
`1` | Download to the default save path
`"/path/to/download/to"` | Download to this path

View File

@ -0,0 +1,67 @@
TokenTree {
title: None,
content: [],
children: [
TokenTree {
title: Some(
"Testing",
),
content: [
Text(
"",
),
Text(
"Name: `foo`",
),
Text(
"",
),
Text(
"Possible values of `scan_dirs`:",
),
Text(
"",
),
Table(
Table {
header: TableRow {
raw: "Value | Description",
columns: [
"Value",
"Description",
],
},
split: "----------------------------|------------",
rows: [
TableRow {
raw: "`0` | Download to the monitored folder",
columns: [
"0",
"Download to the monitored folder",
],
},
TableRow {
raw: "`1` | Download to the default save path",
columns: [
"1",
"Download to the default save path",
],
},
TableRow {
raw: "`\"/path/to/download/to\"` | Download to this path",
columns: [
"\"/path/to/download/to\"",
"Download to this path",
],
},
],
},
),
Text(
"",
),
],
children: [],
},
],
}

View File

@ -3,9 +3,12 @@ ApiMethod {
description: Some(
"The response is a JSON object with the following fields\n\n\n\n\nExample:\n\n```JSON\n{\n \"results\": [\n {\n \"descrLink\": \"http://www.legittorrents.info/index.php?page=torrent-details&id=8d5f512e1acb687029b8d7cc6c5a84dce51d7a41\",\n \"fileName\": \"Ubuntu-10.04-32bit-NeTV.ova\",\n \"fileSize\": -1,\n \"fileUrl\": \"http://www.legittorrents.info/download.php?id=8d5f512e1acb687029b8d7cc6c5a84dce51d7a41&f=Ubuntu-10.04-32bit-NeTV.ova.torrent\",\n \"nbLeechers\": 1,\n \"nbSeeders\": 0,\n \"siteUrl\": \"http://www.legittorrents.info\"\n },\n {\n \"descrLink\": \"http://www.legittorrents.info/index.php?page=torrent-details&id=d5179f53e105dc2c2401bcfaa0c2c4936a6aa475\",\n \"fileName\": \"mangOH-Legato-17_06-Ubuntu-16_04.ova\",\n \"fileSize\": -1,\n \"fileUrl\": \"http://www.legittorrents.info/download.php?id=d5179f53e105dc2c2401bcfaa0c2c4936a6aa475&f=mangOH-Legato-17_06-Ubuntu-16_04.ova.torrent\",\n \"nbLeechers\": 0,\n \"nbSeeders\": 59,\n \"siteUrl\": \"http://www.legittorrents.info\"\n }\n ],\n \"status\": \"Running\",\n \"total\": 2\n}\n```",
),
parameters: Some(
ApiParameters {
mandatory: [
url: "results",
types: CompositeTypes {
composite_types: [
Parameters(
TypeWithoutName {
types: [
Number(
TypeInfo {
name: "id",
@ -17,8 +20,6 @@ ApiMethod {
type_description: None,
},
),
],
optional: [
Number(
TypeInfo {
name: "limit",
@ -44,15 +45,96 @@ ApiMethod {
],
},
),
return_type: Some(
ReturnType {
is_list: false,
parameters: [
ReturnTypeParameter {
name: "results",
description: "Array of result objects- see table below",
return_type: StringArray(
Object(
TypeWithName {
name: "Result",
types: [
String(
TypeInfo {
name: "descrLink",
is_optional: false,
is_list: false,
description: Some(
"URL of the torrent's description page",
),
type_description: None,
},
),
String(
TypeInfo {
name: "fileName",
is_optional: false,
is_list: false,
description: Some(
"Name of the file",
),
type_description: None,
},
),
Number(
TypeInfo {
name: "fileSize",
is_optional: false,
is_list: false,
description: Some(
"Size of the file in Bytes",
),
type_description: None,
},
),
String(
TypeInfo {
name: "fileUrl",
is_optional: false,
is_list: false,
description: Some(
"Torrent download link (usually either .torrent file or magnet link)",
),
type_description: None,
},
),
Number(
TypeInfo {
name: "nbLeechers",
is_optional: false,
is_list: false,
description: Some(
"Number of leechers",
),
type_description: None,
},
),
Number(
TypeInfo {
name: "nbSeeders",
is_optional: false,
is_list: false,
description: Some(
"Number of seeders",
),
type_description: None,
},
),
String(
TypeInfo {
name: "siteUrl",
is_optional: false,
is_list: false,
description: Some(
"URL of the torrent site",
),
type_description: None,
},
),
],
},
),
Response(
TypeWithoutName {
types: [
Object(
Object {
type_info: TypeInfo {
name: "results",
is_optional: false,
is_list: false,
@ -61,12 +143,10 @@ ApiMethod {
),
type_description: None,
},
),
ref_type: "Result",
},
ReturnTypeParameter {
name: "status",
description: "Current status of the search job (either Running or Stopped)",
return_type: String(
),
String(
TypeInfo {
name: "status",
is_optional: false,
@ -77,11 +157,7 @@ ApiMethod {
type_description: None,
},
),
},
ReturnTypeParameter {
name: "total",
description: "Total number of results. If the status is Running this number may continue to increase",
return_type: Number(
Number(
TypeInfo {
name: "total",
is_optional: false,
@ -92,9 +168,9 @@ ApiMethod {
type_description: None,
},
),
},
],
},
),
url: "results",
],
},
}

View File

@ -1,18 +1,75 @@
mod description;
mod return_type;
// mod return_type;
mod url;
use crate::{md_parser, types};
pub use return_type::ReturnType;
use std::collections::HashMap;
use case::CaseExt;
use regex::Regex;
use std::collections::{BTreeMap, HashMap};
#[derive(Debug)]
pub struct ApiMethod {
pub name: String,
pub description: Option<String>,
pub parameters: Option<ApiParameters>,
pub return_type: Option<ReturnType>,
pub url: String,
pub types: CompositeTypes,
}
#[derive(Debug)]
pub struct CompositeTypes {
pub composite_types: Vec<CompositeType>,
}
impl CompositeTypes {
pub fn new(tables: &Tables) -> Self {
Self {
composite_types: tables.get_all_tables_as_types(),
}
}
pub fn parameters(&self) -> Option<&Vec<types::Type>> {
self.composite_types.iter().find_map(|type_| match type_ {
CompositeType::Parameters(p) => Some(&p.types),
_ => None,
})
}
pub fn optional_parameters(&self) -> Option<Vec<&types::Type>> {
self.parameters()
.map(|params| params.iter().filter(|param| param.is_optional()).collect())
}
pub fn mandatory_params(&self) -> Option<Vec<&types::Type>> {
self.parameters()
.map(|params| params.iter().filter(|param| !param.is_optional()).collect())
}
pub fn response(&self) -> Option<&Vec<types::Type>> {
self.composite_types.iter().find_map(|type_| match type_ {
CompositeType::Response(p) => Some(&p.types),
_ => None,
})
}
pub fn objects(&self) -> Vec<&TypeWithName> {
self.composite_types
.iter()
.filter_map(|type_| match type_ {
CompositeType::Object(p) => Some(p),
_ => None,
})
.collect()
}
pub fn enums(&self) -> Vec<&Enum> {
self.composite_types
.iter()
.filter_map(|type_| match type_ {
CompositeType::Enum(p) => Some(p),
_ => None,
})
.collect()
}
}
#[derive(Debug)]
@ -21,24 +78,86 @@ pub struct ApiParameters {
pub optional: Vec<types::Type>,
}
impl ApiParameters {
fn new(params: Vec<types::Type>) -> Self {
let (mandatory, optional) = params.into_iter().fold(
(vec![], vec![]),
|(mut mandatory, mut optional), parameter| {
if parameter.get_type_info().is_optional {
optional.push(parameter);
} else {
mandatory.push(parameter);
#[derive(Debug)]
pub enum CompositeType {
Enum(Enum),
Object(TypeWithName),
Response(TypeWithoutName),
Parameters(TypeWithoutName),
}
(mandatory, optional)
},
);
#[derive(Debug)]
pub struct TypeWithName {
pub name: String,
pub types: Vec<types::Type>,
}
#[derive(Debug)]
pub struct TypeWithoutName {
pub types: Vec<types::Type>,
}
impl TypeWithoutName {
pub fn new(types: Vec<types::Type>) -> Self {
Self { types }
}
}
impl TypeWithName {
pub fn new(name: &str, types: Vec<types::Type>) -> Self {
Self {
mandatory,
optional,
name: name.to_string(),
types,
}
}
}
#[derive(Debug)]
pub struct Enum {
pub name: String,
pub values: Vec<EnumValue>,
}
#[derive(Debug)]
pub struct EnumValue {
pub description: Option<String>,
pub value: String,
pub original_value: String,
}
impl Enum {
fn new(name: &str, table: &md_parser::Table) -> Self {
let values = table.rows.iter().map(EnumValue::from).collect();
Enum {
name: name.to_string(),
values,
}
}
}
impl From<&md_parser::TableRow> for EnumValue {
fn from(row: &md_parser::TableRow) -> Self {
let description = row.columns.get(1).cloned();
let original_value = row.columns[0].clone();
let value = if original_value.parse::<i32>().is_ok() {
let name = description
.clone()
.unwrap()
.replace(' ', "_")
.replace('-', "_")
.replace(',', "_");
let re = Regex::new(r#"\(.*\)"#).unwrap();
re.replace_all(&name, "").to_camel()
} else {
original_value.to_camel()
};
EnumValue {
description,
value,
original_value,
}
}
}
@ -56,19 +175,13 @@ impl ApiMethod {
fn new(child: &md_parser::TokenTree, name: &str) -> Self {
let tables = Tables::from(child);
let method_description = child.parse_method_description();
let return_type = child.parse_return_type();
// let return_type = tables.return_type().map(|r| ReturnType::new(r));
let parameters = tables
.get_type_containing("Parameters")
.map(ApiParameters::new);
let method_url = child.get_method_url();
ApiMethod {
name: name.to_string(),
description: method_description,
parameters,
return_type,
url: method_url,
types: CompositeTypes::new(&tables),
}
}
}
@ -86,7 +199,7 @@ impl md_parser::TokenTree {
impl<'a> From<&'a md_parser::TokenTree> for Tables<'a> {
fn from(token_tree: &'a md_parser::TokenTree) -> Self {
let mut tables = HashMap::new();
let mut tables = BTreeMap::new();
let mut prev_prev: Option<&md_parser::MdContent> = None;
let mut prev: Option<&md_parser::MdContent> = None;
@ -110,28 +223,80 @@ impl<'a> From<&'a md_parser::TokenTree> for Tables<'a> {
}
#[derive(Debug)]
struct Tables<'a> {
tables: HashMap<String, &'a md_parser::Table>,
pub struct Tables<'a> {
tables: BTreeMap<String, &'a md_parser::Table>,
}
impl md_parser::Table {
fn to_enum(&self, input_name: &str) -> Option<CompositeType> {
let re = Regex::new(r"^Possible values of `(\w+)`$").unwrap();
if !re.is_match(input_name) {
return None;
}
Some(CompositeType::Enum(Enum::new(
&Self::regex_to_name(&re, input_name),
self,
)))
}
fn to_object(&self, input_name: &str) -> Option<CompositeType> {
let re = Regex::new(r"^(\w+) object$").unwrap();
if !re.is_match(input_name) {
return None;
}
Some(CompositeType::Object(TypeWithName::new(
&Self::regex_to_name(&re, input_name),
self.to_types(),
)))
}
fn to_response(&self, input_name: &str) -> Option<CompositeType> {
if !input_name.starts_with("The response is a") {
return None;
}
Some(CompositeType::Response(TypeWithoutName::new(
self.to_types(),
)))
}
fn to_parameters(&self, input_name: &str) -> Option<CompositeType> {
if !input_name.starts_with("Parameters") {
return None;
}
Some(CompositeType::Parameters(TypeWithoutName::new(
self.to_types(),
)))
}
fn to_composite_type(&self, input_name: &str) -> Option<CompositeType> {
self.to_enum(input_name)
.or_else(|| self.to_response(input_name))
.or_else(|| self.to_object(input_name))
.or_else(|| self.to_parameters(input_name))
}
fn regex_to_name(re: &Regex, input_name: &str) -> String {
re.captures(input_name)
.unwrap()
.get(1)
.unwrap()
.as_str()
.to_string()
.to_camel()
}
}
impl<'a> Tables<'a> {
fn get_type_containing(&self, name: &str) -> Option<Vec<types::Type>> {
self.get_type_containing_as_table(name)
.map(|table| table.to_types())
}
fn get_type_containing_as_table(&self, name: &str) -> Option<&md_parser::Table> {
self.get_all_type_containing_as_table(name)
.iter()
.map(|(_, table)| *table)
.find(|_| true)
}
fn get_all_type_containing_as_table(&self, name: &str) -> HashMap<String, &md_parser::Table> {
fn get_all_tables_as_types(&self) -> Vec<CompositeType> {
self.tables
.iter()
.filter(|(key, _)| key.contains(name))
.map(|(k, table)| (k.clone(), *table))
.flat_map(|(k, v)| v.to_composite_type(k))
.collect()
}
}
@ -207,8 +372,8 @@ mod tests {
use std::path::Path;
let input = include_str!(concat!(TEST_DIR!(), "/", $test_file, ".md"));
let tree = ApiMethod::try_new(input);
let api_method = parse_api_method(&tree.children[0]).unwrap();
let tree = TokenTreeFactory::create(input);
let api_method = ApiMethod::try_new(&tree.children[0]).unwrap();
let tree_as_str = format!("{tree:#?}");
let api_method_as_str = format!("{api_method:#?}");
@ -242,4 +407,9 @@ mod tests {
fn search_result() {
run_test!("search_result");
}
#[test]
fn enum_test() {
run_test!("enum");
}
}

View File

@ -1,5 +1,8 @@
use std::collections::HashMap;
use case::CaseExt;
use regex::RegexBuilder;
#[derive(Debug, Clone)]
pub struct TypeDescriptions {
pub value: String,
@ -39,15 +42,22 @@ impl TypeInfo {
}
#[derive(Debug, Clone)]
pub struct TypeWithRef {
pub struct Object {
pub type_info: TypeInfo,
pub ref_type: String,
}
#[derive(Debug, Clone)]
pub struct ComplexObject {
pub struct Enum {
pub type_info: TypeInfo,
pub fields: Vec<Type>,
pub values: Vec<EnumValue>,
}
#[derive(Debug, Clone)]
pub struct EnumValue {
pub description: Option<String>,
pub key: String,
pub value: String,
}
pub const OPTIONAL: &str = "_optional_";
@ -59,8 +69,7 @@ pub enum Type {
Bool(TypeInfo),
String(TypeInfo),
StringArray(TypeInfo),
Object(TypeInfo),
// ComplexObject(ComplexObject),
Object(Object),
}
impl Type {
@ -72,7 +81,6 @@ impl Type {
Type::String(_) => "String".into(),
Type::StringArray(_) => "String".into(),
Type::Object(_) => "String".into(),
// Type::ComplexObject(_) => panic!("Not implemented for ComplexObject"),
}
}
@ -88,7 +96,6 @@ impl Type {
Type::String(_) => "str".into(),
Type::StringArray(_) => "&[str]".into(),
Type::Object(_) => "str".into(),
// Type::ComplexObject(_) => panic!("Not implemented for ComplexObject"),
}
}
@ -96,6 +103,10 @@ impl Type {
matches!(self, Type::String(_) | Type::Object(_))
}
pub fn is_optional(&self) -> bool {
self.get_type_info().is_optional
}
pub fn get_type_info(&self) -> &TypeInfo {
match self {
Type::Number(t) => t,
@ -103,8 +114,7 @@ impl Type {
Type::Bool(t) => t,
Type::String(t) => t,
Type::StringArray(t) => t,
Type::Object(t) => t,
// Type::ComplexObject(ComplexObject { type_info, .. }) => type_info,
Type::Object(t) => &t.type_info,
}
}
@ -132,46 +142,54 @@ impl Type {
)
};
let create_object_type = |name: &str| {
Some(Type::Object(Object {
type_info: create_type_info(),
ref_type: name.to_camel(),
}))
};
match type_as_str {
"bool" => Some(Type::Bool(create_type_info())),
"integer" | "number" | "int" => Some(Type::Number(create_type_info())),
"string" => Some(Type::String(create_type_info())),
"array" => Some(Type::StringArray(create_type_info())),
// "array" => description
// .clone()
// .and_then(|ref desc| get_ref_type(desc))
// .map(|ref_type| {
// Type::ObjectArray(TypeWithRef {
// type_info: create_type_info(),
// ref_type,
// })
// })
// .or_else(|| Some(Type::StringArray(create_type_info()))),
"object" => Some(Type::Object(create_type_info())),
"array" => description
.extract_type()
.and_then(|t| create_object_type(&t))
.or_else(|| Some(Type::StringArray(create_type_info()))),
"float" => Some(Type::Float(create_type_info())),
_ => None,
name => create_object_type(name),
}
}
}
// fn get_ref_type(desc: &str) -> Option<String> {
// let re = RegexBuilder::new(r".*array of (\w+)\s?.*")
// .case_insensitive(true)
// .build()
// .unwrap();
trait ExtractType {
fn extract_type(&self) -> Option<String>;
}
// re.captures(desc)
// .and_then(|captures| captures.get(1))
// .map(|m| m.as_str().to_owned())
// }
impl ExtractType for Option<String> {
fn extract_type(&self) -> Option<String> {
self.as_ref().and_then(|t| {
let re = RegexBuilder::new(r".*Array of (\w+) objects.*")
.case_insensitive(true)
.build()
.unwrap();
let cap = re.captures(t)?;
cap.get(1).map(|m| m.as_str().to_camel())
})
}
}
#[cfg(test)]
mod tests {
// use super::*;
use super::*;
// #[test]
// fn should_parse_object_array() {
// let ref_type = get_ref_type("Array of result objects- see table below");
// assert_eq!("result", ref_type.unwrap());
// }
#[test]
fn test_regex() {
let input = Some("Array of result objects- see table below".to_string());
let res = input.extract_type();
assert_eq!(res.unwrap(), "Result");
}
}

View File

@ -11,10 +11,11 @@ struct Api {}
async fn main() -> Result<()> {
let api = Api::login(BASE_URL, USERNAME, PASSWORD).await?;
let _ = api.search().plugins().await?;
let _ = api.search().results(1).send().await?;
let _ = api.search().delete(1).await?;
let _ = api.search().install_plugin("https://raw.githubusercontent.com/qbittorrent/search-plugins/master/nova3/engines/legittorrents.py").await?;
let plugins = api.search().plugins().await?;
eprintln!("{:?}", plugins);
// let _ = api.search().results(1).send().await?;
// let _ = api.search().delete(1).await?;
Ok(())
}