Skip to content

Commit ec669f8

Browse files
authored
Turn Resource into a macro (Astavie#1)
1 parent 316c808 commit ec669f8

File tree

19 files changed

+588
-568
lines changed

19 files changed

+588
-568
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
name = "cardmaster"
33
version = "0.1.0"
44
edition = "2021"
5+
authors = [ "Astavie <astavie@pm.me>" ]
56

67
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
78

discord/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
name = "discord"
33
version = "0.1.0"
44
edition = "2021"
5+
authors = [ "Astavie <astavie@pm.me>" ]
56

67
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
78

@@ -11,7 +12,6 @@ isahc = { version = "1.7.2", features = ["serde_json", "json"] }
1112
serde = { version = "1.0.126", features = ["derive"] }
1213
serde_json = "1.0.96"
1314
tokio = { version = "1.27.0", features = ["full"] }
14-
partial_id = { path = "partial_id" }
1515
serde_repr = "0.1.12"
1616
tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] }
1717
futures-util = "0.3.28"
@@ -21,5 +21,8 @@ async-trait = "0.1.68"
2121
enumset = { version = "1.1.2", features = ["serde"] }
2222
monostate = "0.1.6"
2323

24+
partial_id = { path = "partial_id" }
25+
resource = { path = "resource" }
26+
2427
[patch.crates-io]
2528
serde = { git = "https://github.com/Astavie/serde.git", branch = "integer-tags-for-enums-noprecompile" }

discord/partial_id/src/lib.rs

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ pub fn derive_partial(input: proc_macro::TokenStream) -> proc_macro::TokenStream
2424
} else {
2525
proc_macro2::TokenStream::from_iter(derives.into_iter())
2626
};
27-
let partial_ident = Ident::new(&format!("Partial{}", ty), Span::call_site());
27+
let partial_ty = Ident::new(&format!("Partial{}", ty), Span::call_site());
2828

2929
let fields = filter_fields(match data {
3030
syn::Data::Struct(ref s) => &s.fields,
3131
_ => panic!("Field can only be derived for structs"),
3232
});
3333

34+
let id_typ = &fields
35+
.iter()
36+
.find(|(_, ident, _)| ident.to_string() == "id")
37+
.unwrap()
38+
.2;
39+
3440
let field_var = fields.iter().map(|(vis, ident, ty)| {
3541
if ident.to_string() == "id" {
3642
quote! {
@@ -53,60 +59,75 @@ pub fn derive_partial(input: proc_macro::TokenStream) -> proc_macro::TokenStream
5359
}
5460
}
5561
});
56-
57-
let get_field = fields.iter().map(|(vis, ident, ty)| {
62+
let convert_none_branch = fields.iter().map(|(_vis, ident, _ty)| {
5863
if ident.to_string() == "id" {
59-
quote!{}
64+
quote! {
65+
#ident: src
66+
}
6067
} else {
61-
let get_ident = Ident::new(&format!("get_{}", ident), ident.span());
62-
let get_ident_mut = Ident::new(&format!("get_{}_mut", ident), ident.span());
6368
quote! {
64-
#vis async fn #get_ident(&mut self, client: &crate::request::Discord) -> crate::request::Result<&#ty> {
65-
crate::request::Result::Ok(match self.#ident {
66-
::core::option::Option::Some(ref v) => v,
67-
::core::option::Option::None => {
68-
crate::resource::Updatable::update(self, client).await?;
69-
self.#ident.as_ref().unwrap()
70-
}
71-
})
72-
}
73-
#vis async fn #get_ident_mut(&mut self, client: &crate::request::Discord) -> crate::request::Result<&mut #ty> {
74-
crate::request::Result::Ok(match self.#ident {
75-
::core::option::Option::Some(ref mut v) => v,
76-
::core::option::Option::None => {
77-
crate::resource::Updatable::update(self, client).await?;
78-
self.#ident.as_mut().unwrap()
79-
}
80-
})
81-
}
69+
#ident: ::core::option::Option::None
8270
}
8371
}
8472
});
8573

8674
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
8775

8876
let tokens = quote! {
77+
8978
#derive
90-
#vis struct #partial_ident #ty_generics
91-
#where_clause
92-
{
79+
#vis struct #partial_ty #generics {
9380
#(#field_var),*
9481
}
9582

96-
impl #impl_generics ::core::convert::From<#ty #ty_generics> for #partial_ident #ty_generics
83+
impl #impl_generics ::core::convert::From<#ty #ty_generics> for #partial_ty #ty_generics
9784
#where_clause
9885
{
99-
fn from(src: #ty #ty_generics) -> #partial_ident #ty_generics {
100-
#partial_ident {
86+
fn from(src: #ty #ty_generics) -> Self {
87+
Self {
10188
#(#convert_branch),*
10289
}
10390
}
10491
}
10592

106-
impl #impl_generics #partial_ident #ty_generics
93+
impl #impl_generics ::core::convert::From<#id_typ> for #partial_ty #ty_generics
10794
#where_clause
10895
{
109-
#(#get_field)*
96+
fn from(src: #id_typ) -> Self {
97+
Self {
98+
#(#convert_none_branch),*
99+
}
100+
}
101+
}
102+
103+
impl #impl_generics #ty #ty_generics
104+
#where_clause
105+
{
106+
#vis async fn update(&mut self, client: &crate::request::Discord) -> crate::request::Result<()> {
107+
*self = crate::request::Request::request(crate::request::HttpRequest::get(crate::resource::Endpoint::uri(&self.id)), client).await?;
108+
crate::request::Result::Ok(())
109+
}
110+
}
111+
112+
impl #impl_generics #partial_ty #ty_generics
113+
#where_clause
114+
{
115+
#vis async fn update(&mut self, client: &crate::request::Discord) -> crate::request::Result<()> {
116+
let full: #ty = crate::request::Request::request(crate::request::HttpRequest::get(crate::resource::Endpoint::uri(&self.id)), client).await?;
117+
*self = full.into();
118+
crate::request::Result::Ok(())
119+
}
120+
#vis async fn get_field<T>(&mut self, client: &crate::request::Discord, f: fn(&Self) -> &::core::option::Option<T>) -> crate::request::Result<&T> {
121+
crate::request::Result::Ok(match f(self) {
122+
::core::option::Option::Some(_) => {
123+
f(self).as_ref().unwrap()
124+
}
125+
::core::option::Option::None => {
126+
self.update(client).await?;
127+
f(self).as_ref().unwrap()
128+
}
129+
})
130+
}
110131
}
111132
};
112133
tokens.into()

discord/resource/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "resource"
3+
version = "0.1.0"
4+
edition = "2021"
5+
authors = [ "Astavie <astavie@pm.me>" ]
6+
7+
[lib]
8+
proc-macro = true
9+
10+
[dependencies]
11+
syn = { version = "~1.0", features = ["full"] }
12+
quote = "~1.0"
13+
proc-macro2 = "~1.0"
14+
15+
[dev-dependencies]
16+
trybuild = "~1.0"

discord/resource/src/lib.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use proc_macro::TokenStream;
2+
use proc_macro2::{Ident, TokenStream as TokenStream2};
3+
use quote::quote;
4+
use syn::{
5+
parse::{Parse, ParseStream},
6+
parse_quote,
7+
spanned::Spanned,
8+
FnArg, ItemFn, ReturnType, Token, Type,
9+
};
10+
11+
struct ResourceParams {
12+
result: Type,
13+
client: Option<Type>,
14+
}
15+
16+
impl Parse for ResourceParams {
17+
fn parse(input: ParseStream) -> syn::Result<Self> {
18+
let result = input.parse()?;
19+
let client = if input.peek(Token![,]) {
20+
input.parse::<Token![,]>()?;
21+
22+
let ident: Ident = input.parse()?;
23+
if ident.to_string() != "client" {
24+
return Err(syn::Error::new(ident.span(), "invalid resource property"));
25+
}
26+
27+
input.parse::<Token![=]>()?;
28+
Some(input.parse()?)
29+
} else {
30+
None
31+
};
32+
33+
Ok(Self { result, client })
34+
}
35+
}
36+
37+
fn resource_impl(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream2> {
38+
let params: ResourceParams = syn::parse(attr)?;
39+
let return_type = params.result;
40+
let client_type = match params.client {
41+
Some(t) => t,
42+
None => parse_quote!(crate::request::Discord),
43+
};
44+
45+
let result_type = parse_quote!(
46+
::std::pin::Pin<::std::boxed::Box<dyn ::futures_util::Future<Output = crate::request::Result<#return_type>> + ::std::marker::Send + 'resource>>
47+
);
48+
49+
let mut get_request_fn: ItemFn = syn::parse(item)?;
50+
51+
// get immediate fn signature
52+
let vis = get_request_fn.vis.clone();
53+
54+
let mut sig = get_request_fn.sig.clone();
55+
let generic_params = sig.generics.params;
56+
sig.generics = parse_quote!(<'resource, #generic_params>);
57+
match &mut sig.output {
58+
ReturnType::Default => panic!(),
59+
ReturnType::Type(_, typ) => **typ = result_type,
60+
}
61+
62+
// remove pattern matching from inputs and get names
63+
let inputs = sig
64+
.inputs
65+
.iter_mut()
66+
.skip(1) // skip self
67+
.enumerate()
68+
.map(|(n, arg)| match arg {
69+
FnArg::Receiver(_) => panic!(),
70+
FnArg::Typed(pt) => {
71+
let ty = &*pt.ty;
72+
match &*pt.pat {
73+
syn::Pat::Ident(ident) => {
74+
let id = &ident.ident;
75+
let id_cloned = id.clone();
76+
*arg = parse_quote!(#id: #ty);
77+
id_cloned
78+
}
79+
_ => {
80+
let id = Ident::new(&format!("arg__{}", n), pt.pat.span());
81+
let id_cloned = id.clone();
82+
*arg = parse_quote!(#id: #ty);
83+
id_cloned
84+
}
85+
}
86+
}
87+
})
88+
.collect::<Vec<_>>();
89+
90+
// insert client as input
91+
sig.inputs
92+
.insert(1, parse_quote!(client: &'resource #client_type));
93+
94+
// set original fn name to {}_request
95+
let get_ident = get_request_fn.sig.ident;
96+
let get_request_ident = Ident::new(&format!("{}_request", get_ident), get_ident.span());
97+
get_request_fn.sig.ident = get_request_ident.clone();
98+
99+
// return functions
100+
let tokens = quote! {
101+
#get_request_fn
102+
#vis #sig {
103+
crate::request::Request::request(self.#get_request_ident(#(#inputs),*), client)
104+
}
105+
};
106+
Ok(tokens)
107+
}
108+
109+
#[proc_macro_attribute]
110+
pub fn resource(attr: TokenStream, item: TokenStream) -> TokenStream {
111+
match resource_impl(attr, item) {
112+
Ok(params) => params.into(),
113+
Err(e) => return e.into_compile_error().into(),
114+
}
115+
}

discord/src/application.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use partial_id::Partial;
22
use serde::Deserialize;
33

4-
use crate::guild::Guild;
5-
use crate::resource::{Endpoint, Resource};
4+
use crate::guild::GuildResource;
5+
use crate::request::HttpRequest;
6+
use crate::resource::{resource, Endpoint};
67

78
use super::{command::Commands, resource::Snowflake};
89

@@ -18,7 +19,7 @@ pub trait ApplicationResource {
1819
fn global_commands(&self) -> Commands {
1920
Commands::new(self.endpoint().clone(), None)
2021
}
21-
fn guild_commands(&self, guild: &impl Resource<Endpoint = Snowflake<Guild>>) -> Commands {
22+
fn guild_commands(&self, guild: &impl GuildResource) -> Commands {
2223
Commands::new(self.endpoint().clone(), Some(guild.endpoint().clone()))
2324
}
2425
}
@@ -34,11 +35,18 @@ impl ApplicationResource for Application {
3435
}
3536
}
3637

38+
impl Endpoint for Snowflake<Application> {
39+
fn uri(&self) -> String {
40+
// you can only ever get your own application data
41+
"/applications/@me".into()
42+
}
43+
}
44+
3745
pub struct Me;
3846

39-
impl Endpoint for Me {
40-
type Result = Application;
41-
fn uri(&self) -> String {
42-
"/oauth2/applications/@me".into()
47+
impl Me {
48+
#[resource(Application)]
49+
pub fn get(&self) -> HttpRequest<Application> {
50+
HttpRequest::get("/applications/@me")
4351
}
4452
}

0 commit comments

Comments
 (0)