Progress
This commit is contained in:
parent
a93e8e7a02
commit
68bb159dc3
File diff suppressed because one or more lines are too long
|
@ -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_ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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\"",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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: [],
|
||||
},
|
||||
],
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
},
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user