From 9b41a9844fa8070b9ae21dddbdefb8a264454dc4 Mon Sep 17 00:00:00 2001 From: TotallyNot <44345987+TotallyNot@users.noreply.github.com> Date: Tue, 10 Oct 2023 19:58:15 +0200 Subject: [PATCH] added `IntoOwned` trait and derive macro --- torn-api-macros/Cargo.toml | 4 +- torn-api-macros/src/lib.rs | 258 ++++++++++++++++++++++++------------- torn-api/Cargo.toml | 4 +- torn-api/src/common.rs | 3 +- torn-api/src/faction.rs | 8 +- torn-api/src/into_owned.rs | 79 ++++++++++++ torn-api/src/lib.rs | 5 +- torn-api/src/user.rs | 14 +- 8 files changed, 268 insertions(+), 107 deletions(-) create mode 100644 torn-api/src/into_owned.rs diff --git a/torn-api-macros/Cargo.toml b/torn-api-macros/Cargo.toml index 239845d..6a7b6c4 100644 --- a/torn-api-macros/Cargo.toml +++ b/torn-api-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "torn-api-macros" -version = "0.1.1" +version = "0.1.2" edition = "2021" authors = ["Pyrit [2111649]"] license = "MIT" @@ -12,7 +12,7 @@ description = "Macros implementation of #[derive(ApiCategory)]" proc-macro = true [dependencies] -syn = { version = "1", features = [ "extra-traits" ] } +syn = { version = "2", features = [ "parsing" ] } proc-macro2 = "1" quote = "1" convert_case = "0.5" diff --git a/torn-api-macros/src/lib.rs b/torn-api-macros/src/lib.rs index c31ace1..1a0086c 100644 --- a/torn-api-macros/src/lib.rs +++ b/torn-api-macros/src/lib.rs @@ -25,13 +25,6 @@ struct ApiAttribute { with: Option, } -fn get_lit_string(lit: syn::Lit) -> String { - match lit { - syn::Lit::Str(lit) => lit.value(), - _ => panic!("Expected api attribute to be a string"), - } -} - fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; @@ -41,23 +34,19 @@ fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream { }; let mut category: Option = None; - ast.attrs - .iter() - .filter(|a| a.path.is_ident("api")) - .for_each(|a| { - if let Ok(syn::Meta::List(l)) = a.parse_meta() { - for nested in l.nested { - match nested { - syn::NestedMeta::Meta(syn::Meta::NameValue(m)) - if m.path.is_ident("category") => - { - category = Some(get_lit_string(m.lit)) - } - _ => panic!("unknown api attribute"), - } + for attr in &ast.attrs { + if attr.path().is_ident("api") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("category") { + let c: syn::LitStr = meta.value()?.parse()?; + category = Some(c.value()); + Ok(()) + } else { + Err(meta.error("unknown attribute")) } - } - }); + }).unwrap(); + } + } let category = category.expect("`category`"); @@ -65,76 +54,41 @@ fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream { .variants .iter() .filter_map(|variant| { + let mut r#type: Option = None; + let mut field: Option = None; + let mut with: Option = None; for attr in &variant.attrs { - if attr.path.is_ident("api") { - let meta = attr.parse_meta(); - match meta { - Ok(syn::Meta::List(l)) => { - let mut type_: Option = None; - let mut field: Option = None; - let mut with: Option = None; - for nested in l.nested.into_iter() { - match nested { - syn::NestedMeta::Meta(syn::Meta::NameValue(m)) - if m.path.is_ident("type") => - { - if type_.is_none() { - type_ = Some(get_lit_string(m.lit)); - } else { - panic!("type can only be specified once"); - } - } - syn::NestedMeta::Meta(syn::Meta::NameValue(m)) - if m.path.is_ident("with") => - { - if with.is_none() { - with = Some(get_lit_string(m.lit)); - } else { - panic!("with can only be specified once"); - } - } - syn::NestedMeta::Meta(syn::Meta::NameValue(m)) - if m.path.is_ident("field") => - { - if field.is_none() { - field = Some(ApiField::Property(quote::format_ident!( - "{}", - get_lit_string(m.lit) - ))); - } else { - panic!("field/flatten can only be specified once"); - } - } - syn::NestedMeta::Meta(syn::Meta::Path(m)) - if m.is_ident("flatten") => - { - if field.is_none() { - field = Some(ApiField::Flattened); - } else { - panic!("field/flatten can only be specified once"); - } - } - _ => panic!("Couldn't parse api attribute"), - } - } - let name = - format_ident!("{}", variant.ident.to_string().to_case(Case::Snake)); - let raw_value = variant.ident.to_string().to_lowercase(); - - return Some(ApiAttribute { - field: field.expect("one of field/flatten"), - name, - raw_value, - variant: variant.ident.clone(), - type_name: type_ - .expect("Need to specify type name") - .parse() - .expect("failed to parse type name"), - with: with.map(|w| format_ident!("{}", w)), - }); + if attr.path().is_ident("api") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("type") { + let t: syn::LitStr = meta.value()?.parse()?; + r#type = Some(t.value()); + Ok(()) + } else if meta.path.is_ident("with") { + let w: syn::LitStr = meta.value()?.parse()?; + with = Some(quote::format_ident!("{}", w.value())); + Ok(()) + } else if meta.path.is_ident("field") { + let f: syn::LitStr = meta.value()?.parse()?; + field = Some(ApiField::Property(quote::format_ident!("{}", f.value()))); + Ok(()) + } else if meta.path.is_ident("flatten") { + field = Some(ApiField::Flattened); + Ok(()) + } else { + Err(meta.error("unsupported attribute")) } - _ => panic!("Couldn't parse api attribute"), - } + }).unwrap(); + let name = format_ident!("{}", variant.ident.to_string().to_case(Case::Snake)); + let raw_value = variant.ident.to_string().to_lowercase(); + return Some(ApiAttribute { + field: field.expect("field or flatten attribute must be specified"), + raw_value, + variant: variant.ident.clone(), + type_name: r#type.expect("type must be specified").parse().unwrap(), + name, + with + }) } } None @@ -214,3 +168,125 @@ fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream { gen.into() } + +#[proc_macro_derive(IntoOwned, attributes(into_owned))] +pub fn derive_into_owned(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + + impl_into_owned(&ast) +} + +fn to_static_lt(ty: &mut syn::Type) -> bool { + let mut res = false; + match ty { + syn::Type::Path(path) => { + if let Some(syn::PathArguments::AngleBracketed(ab)) = path.path.segments.last_mut().map(|s| &mut s.arguments).as_mut() { + for mut arg in &mut ab.args { + match &mut arg { + syn::GenericArgument::Type(ty) => { + if to_static_lt(ty) { + res = true; + } + }, + syn::GenericArgument::Lifetime(lt) => { + lt.ident = syn::Ident::new("static", proc_macro2::Span::call_site()); + res = true; + } + _ => () + } + } + } + } + syn::Type::Reference(r) => { + if let Some(lt) = r.lifetime.as_mut() { + lt.ident = syn::Ident::new("static", proc_macro2::Span::call_site()); + res = true; + } + to_static_lt(&mut r.elem); + } + _ => () + }; + res +} + +fn impl_into_owned(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + + let mut identity = false; + for attr in &ast.attrs { + if attr.path().is_ident("into_owned") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("identity") { + identity = true; + Ok(()) + } else { + Err(meta.error("unknown attribute")) + } + }).unwrap(); + } + } + + if identity { + return quote! { + impl #impl_generics crate::into_owned::IntoOwned for #name #ty_generics #where_clause { + type Owned = Self; + fn into_owned(self) -> Self::Owned { + self + } + } + }.into() + } + + let syn::Data::Struct(r#struct) = &ast.data else { + panic!("Only stucts are supported"); + }; + + let syn::Fields::Named(named_fields) = &r#struct.fields else { + panic!("Only named fields are supported"); + }; + + let vis = &ast.vis; + + for attr in &ast.attrs { + if attr.path().is_ident("identity") { + // + } + } + + let mut owned_fields = Vec::with_capacity(named_fields.named.len()); + let mut fields = Vec::with_capacity(named_fields.named.len()); + + for field in &named_fields.named { + let field_name = &field.ident.as_ref().unwrap(); + let mut ty = field.ty.clone(); + let vis = &field.vis; + + if to_static_lt(&mut ty) { + owned_fields.push(quote! { #vis #field_name: <#ty as crate::into_owned::IntoOwned>::Owned }); + fields.push(quote! { #field_name: crate::into_owned::IntoOwned::into_owned(self.#field_name) }); + } else { + owned_fields.push(quote! { #vis #field_name: #ty }); + fields.push(quote! { #field_name: self.#field_name }); + }; + } + + let owned_name = syn::Ident::new(&format!("{}Owned", ast.ident), proc_macro2::Span::call_site()); + + let gen = quote! { + #[derive(Debug, Clone)] + #vis struct #owned_name { + #(#owned_fields,)* + } + impl #impl_generics crate::into_owned::IntoOwned for #name #ty_generics #where_clause { + type Owned = #owned_name; + fn into_owned(self) -> Self::Owned { + #owned_name { + #(#fields,)* + } + } + } + }; + + gen.into() +} diff --git a/torn-api/Cargo.toml b/torn-api/Cargo.toml index 507a82c..ba77a93 100644 --- a/torn-api/Cargo.toml +++ b/torn-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "torn-api" -version = "0.5.27" +version = "0.5.28" edition = "2021" authors = ["Pyrit [2111649]"] license = "MIT" @@ -37,7 +37,7 @@ reqwest = { version = "0.11", default-features = false, features = [ "json" ], o awc = { version = "3", default-features = false, optional = true } rust_decimal = { version = "1", default-features = false, optional = true, features = [ "serde" ] } -torn-api-macros = { path = "../torn-api-macros", version = "0.1.1" } +torn-api-macros = { path = "../torn-api-macros", version = "0.1.2" } [dev-dependencies] actix-rt = { version = "2.7.0" } diff --git a/torn-api/src/common.rs b/torn-api/src/common.rs index c69a22a..e461dd7 100644 --- a/torn-api/src/common.rs +++ b/torn-api/src/common.rs @@ -1,5 +1,6 @@ use chrono::{serde::ts_seconds, DateTime, Utc}; use serde::Deserialize; +use torn_api_macros::IntoOwned; use crate::de_util; @@ -36,7 +37,7 @@ pub enum StateColour { Blue, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, IntoOwned, Deserialize)] pub struct Status<'a> { pub description: &'a str, #[serde(deserialize_with = "de_util::empty_string_is_none")] diff --git a/torn-api/src/faction.rs b/torn-api/src/faction.rs index 6e0c5e3..fdebbd3 100644 --- a/torn-api/src/faction.rs +++ b/torn-api/src/faction.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeMap, HashMap}; use chrono::{DateTime, Utc}; use serde::Deserialize; -use torn_api_macros::ApiCategory; +use torn_api_macros::{ApiCategory, IntoOwned}; use crate::de_util::{self, null_is_empty_dict}; @@ -32,7 +32,7 @@ pub enum FactionSelection { pub type Selection = FactionSelection; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, IntoOwned, Deserialize)] pub struct Member<'a> { pub name: &'a str, pub level: i16, @@ -42,7 +42,7 @@ pub struct Member<'a> { pub last_action: LastAction, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, IntoOwned, Deserialize)] pub struct FactionTerritoryWar<'a> { pub territory_war_id: i32, pub territory: &'a str, @@ -58,7 +58,7 @@ pub struct FactionTerritoryWar<'a> { pub end_time: DateTime, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, IntoOwned, Deserialize)] pub struct Basic<'a> { #[serde(rename = "ID")] pub id: i32, diff --git a/torn-api/src/into_owned.rs b/torn-api/src/into_owned.rs new file mode 100644 index 0000000..78e4abc --- /dev/null +++ b/torn-api/src/into_owned.rs @@ -0,0 +1,79 @@ +pub use torn_api_macros::IntoOwned; + +pub trait IntoOwned { + type Owned; + + fn into_owned(self) -> Self::Owned; +} + +impl IntoOwned for Option +where + T: IntoOwned, +{ + type Owned = Option; + + fn into_owned(self) -> Self::Owned { + self.map(IntoOwned::into_owned) + } +} + +impl IntoOwned for Vec where T: IntoOwned { + type Owned = Vec<::Owned>; + + fn into_owned(self) -> Self::Owned { + let mut owned = Vec::with_capacity(self.len()); + for elem in self { + owned.push(elem.into_owned()); + } + owned + } +} + +impl IntoOwned for std::collections::HashMap where V: IntoOwned, K: Eq + std::hash::Hash { + type Owned = std::collections::HashMap::Owned>; + + fn into_owned(self) -> Self::Owned { + self.into_iter().map(|(k, v)| (k, v.into_owned())).collect() + } +} + +impl IntoOwned for std::collections::BTreeMap where V: IntoOwned, K: Eq + Ord + std::hash::Hash { + type Owned = std::collections::BTreeMap::Owned>; + + fn into_owned(self) -> Self::Owned { + self.into_iter().map(|(k, v)| (k, v.into_owned())).collect() + } +} + +impl IntoOwned for chrono::DateTime where Z: chrono::TimeZone { + type Owned = Self; + + fn into_owned(self) -> Self::Owned { + self + } +} + +impl<'a> IntoOwned for &'a str { + type Owned = String; + + fn into_owned(self) -> Self::Owned { + self.to_owned() + } +} + +macro_rules! impl_ident { + ($name:path) => { + impl IntoOwned for $name { + type Owned = $name; + fn into_owned(self) -> Self::Owned { + self + } + } + }; +} + +impl_ident!(i64); +impl_ident!(i32); +impl_ident!(i16); +impl_ident!(i8); +impl_ident!(String); diff --git a/torn-api/src/lib.rs b/torn-api/src/lib.rs index 25e7b2f..835a93f 100644 --- a/torn-api/src/lib.rs +++ b/torn-api/src/lib.rs @@ -1,5 +1,6 @@ #![warn(clippy::all, clippy::perf, clippy::style, clippy::suspicious)] +pub mod into_owned; pub mod local; pub mod send; @@ -32,6 +33,8 @@ use chrono::{DateTime, Utc}; use serde::{de::Error as DeError, Deserialize}; use thiserror::Error; +pub use into_owned::IntoOwned; + pub struct ApiResponse { pub value: serde_json::Value, } @@ -156,7 +159,7 @@ where from: None, to: None, comment: None, - phantom: std::marker::PhantomData::default(), + phantom: Default::default(), } } } diff --git a/torn-api/src/user.rs b/torn-api/src/user.rs index 579b8e8..bfb04bf 100644 --- a/torn-api/src/user.rs +++ b/torn-api/src/user.rs @@ -4,7 +4,7 @@ use serde::{ }; use std::collections::{BTreeMap, HashMap}; -use torn_api_macros::ApiCategory; +use torn_api_macros::{ApiCategory, IntoOwned}; use crate::de_util; @@ -41,7 +41,7 @@ pub enum Gender { Enby, } -#[derive(Debug, Clone)] +#[derive(Debug, IntoOwned)] pub struct Faction<'a> { pub faction_id: i32, pub faction_name: &'a str, @@ -133,7 +133,7 @@ where deserializer.deserialize_struct("Faction", FIELDS, FactionVisitor) } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, IntoOwned, Deserialize)] pub struct Basic<'a> { pub player_id: i32, pub name: &'a str, @@ -142,7 +142,8 @@ pub struct Basic<'a> { pub status: Status<'a>, } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, IntoOwned, PartialEq, Eq, Deserialize)] +#[into_owned(identity)] pub struct Discord { #[serde(rename = "userID")] pub user_id: i32, @@ -188,7 +189,8 @@ pub enum EliminationTeam { CapsLockCrew, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, IntoOwned)] +#[into_owned(identity)] pub enum Competition { Elimination { score: i32, @@ -327,7 +329,7 @@ where deserializer.deserialize_option(CompetitionVisitor) } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, IntoOwned, Deserialize)] pub struct Profile<'a> { pub player_id: i32, pub name: &'a str,