diff --git a/Cargo.lock b/Cargo.lock index 58606a9..13d8f3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2294,7 +2294,7 @@ dependencies = [ [[package]] name = "torn-api" -version = "1.7.2" +version = "1.7.1" dependencies = [ "bon", "bytes", diff --git a/Cargo.toml b/Cargo.toml index eee948d..e7fa4ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["torn-api", "torn-api-codegen", "torn-key-pool"] [workspace.package] -license = "MIT" +license-file = "./LICENSE" repository = "https://git.elimination.me/pyrite/torn-api.rs.git" homepage = "https://git.elimination.me/pyrite/torn-api.rs" diff --git a/torn-api-codegen/Cargo.toml b/torn-api-codegen/Cargo.toml index d9621ec..25ad5ee 100644 --- a/torn-api-codegen/Cargo.toml +++ b/torn-api-codegen/Cargo.toml @@ -4,7 +4,7 @@ authors = ["Pyrit [2111649]"] version = "0.7.2" edition = "2021" description = "Contains the v2 torn API model descriptions and codegen for the bindings" -license = { workspace = true } +license-file = { workspace = true } repository = { workspace = true } homepage = { workspace = true } diff --git a/torn-api-macros/Cargo.toml b/torn-api-macros/Cargo.toml new file mode 100644 index 0000000..24c1f04 --- /dev/null +++ b/torn-api-macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "torn-api-macros" +version = "0.3.2" +edition = "2021" +authors = ["Pyrit [2111649]"] +license = "MIT" +repository = "https://github.com/TotallyNot/torn-api.rs.git" +homepage = "https://github.com/TotallyNot/torn-api.rs.git" +description = "Macros implementation of #[derive(ApiCategory)]" + +[lib] +proc-macro = true + +[dependencies] +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 new file mode 100644 index 0000000..3d6f48f --- /dev/null +++ b/torn-api-macros/src/lib.rs @@ -0,0 +1,314 @@ +use convert_case::{Case, Casing}; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; + +#[proc_macro_derive(ApiCategory, attributes(api))] +pub fn derive_api_category(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + + impl_api_category(&ast) +} + +#[derive(Debug)] +enum ApiField { + Property(syn::Ident), + Flattened, +} + +#[derive(Debug)] +struct ApiAttribute { + field: ApiField, + name: syn::Ident, + raw_value: String, + variant: syn::Ident, + type_name: proc_macro2::TokenStream, + with: Option, +} + +fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + + let enum_ = match &ast.data { + syn::Data::Enum(data) => data, + _ => panic!("ApiCategory can only be derived for enums"), + }; + + let mut category: Option = None; + 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`"); + + let fields: Vec<_> = enum_ + .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") { + 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")) + } + }) + .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 + }) + .collect(); + + let accessors = fields.iter().map( + |ApiAttribute { + field, + name, + type_name, + with, + .. + }| match (field, with) { + (ApiField::Property(prop), None) => { + let prop_str = prop.to_string(); + quote! { + pub fn #name(&self) -> serde_json::Result<#type_name> { + self.0.decode_field(#prop_str) + } + } + } + (ApiField::Property(prop), Some(f)) => { + let prop_str = prop.to_string(); + quote! { + pub fn #name(&self) -> serde_json::Result<#type_name> { + self.0.decode_field_with(#prop_str, #f) + } + } + } + (ApiField::Flattened, None) => quote! { + pub fn #name(&self) -> serde_json::Result<#type_name> { + self.0.decode() + } + }, + (ApiField::Flattened, Some(_)) => todo!(), + }, + ); + + let raw_values = fields.iter().map( + |ApiAttribute { + variant, raw_value, .. + }| { + quote! { + #name::#variant => #raw_value + } + }, + ); + + let gen = quote! { + pub struct Response(pub crate::ApiResponse); + + impl Response { + #(#accessors)* + } + + impl From for Response { + fn from(value: crate::ApiResponse) -> Self { + Self(value) + } + } + + impl crate::ApiSelectionResponse for Response { + fn into_inner(self) -> crate::ApiResponse { + self.0 + } + } + + impl crate::ApiSelection for #name { + type Response = Response; + + fn raw_value(self) -> &'static str { + match self { + #(#raw_values,)* + } + } + + fn category() -> &'static str { + #category + } + } + }; + + 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 2311d27..b0b0a14 100644 --- a/torn-api/Cargo.toml +++ b/torn-api/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "torn-api" -version = "1.7.2" +version = "1.7.1" edition = "2021" description = "Auto-generated bindings for the v2 torn api" -license = { workspace = true } +license-file = { workspace = true } repository = { workspace = true } homepage = { workspace = true } diff --git a/torn-key-pool/Cargo.toml b/torn-key-pool/Cargo.toml index 2bf3edd..2164fd4 100644 --- a/torn-key-pool/Cargo.toml +++ b/torn-key-pool/Cargo.toml @@ -3,7 +3,7 @@ name = "torn-key-pool" version = "1.1.3" edition = "2021" authors = ["Pyrit [2111649]"] -license = { workspace = true } +license-file = { workspace = true } repository = { workspace = true } homepage = { workspace = true } description = "A generalised API key pool for torn-api"