From 62f48cf3cd543c560e4ab3feef371d21a8c61bf1 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 14 Jul 2025 18:23:24 +0300 Subject: [PATCH] feat: add `FromFile` implementation --- Cargo.lock | 169 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 ++++ src/from_file.rs | 151 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 22 +++--- 4 files changed, 345 insertions(+), 10 deletions(-) create mode 100644 Cargo.lock create mode 100644 src/from_file.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..91add08 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,169 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "from_file" +version = "0.1.0" +dependencies = [ + "merge", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "syn 2.0.104", +] + +[[package]] +name = "merge" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e520ba58faea3487f75df198b1d079644ec226ea3b0507d002c6fa4b8cf93a" +dependencies = [ + "merge_derive", + "num-traits", +] + +[[package]] +name = "merge_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f8ce6efff81cbc83caf4af0905c46e58cb46892f63ad3835e81b47eaf7968" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/Cargo.toml b/Cargo.toml index 9e351e4..628262c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,17 @@ name = "from_file" version = "0.1.0" edition = "2024" +[features] +default = [] +merge = ["dep:merge"] + [dependencies] +proc-macro2 = "1.0" +quote = "1.0" +proc-macro-error = "1.0" +syn = { version = "2.0", features = ["extra-traits", "parsing"] } +serde = { version = "1.0", features = ["derive"] } +merge = { version = "0.2", optional = true } + +[lib] +proc-macro = true diff --git a/src/from_file.rs b/src/from_file.rs new file mode 100644 index 0000000..9f3db8d --- /dev/null +++ b/src/from_file.rs @@ -0,0 +1,151 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + Attribute, Data, DeriveInput, Error, Expr, Fields, GenericParam, Generics, Meta, Result, + parse_quote, +}; + +const WITH_MERGE: bool = cfg!(feature = "merge"); + +pub fn impl_from_file(input: &DeriveInput) -> Result { + let name = &input.ident; + let vis = &input.vis; + let generics = add_trait_bouts(input.generics.clone()); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let file_ident = format_ident!("{name}File"); + + let fields = match &input.data { + Data::Struct(ds) => match &ds.fields { + Fields::Named(fields) => &fields.named, + _ => { + return Err(Error::new_spanned( + &input.ident, + "FromFile can only be derived for structs with named fields", + )); + } + }, + _ => { + return Err(Error::new_spanned( + &input.ident, + "FromFile can only be derived for structs", + )); + } + }; + + let mut field_assignments = Vec::new(); + let mut file_fields = Vec::new(); + let mut default_bounds = Vec::new(); + + for field in fields { + let ident = field.ident.as_ref().unwrap(); + let ty = &field.ty; + + let mut default_expr = None; + for attr in &field.attrs { + if let Some(expr) = parse_default_attr(attr)? { + default_expr = Some(expr); + break; + } + } + + let field_attrs = if WITH_MERGE { + quote! { + #[merge(strategy = merge::option::overwrite_none)] + } + } else { + quote! {} + }; + file_fields.push(quote! { + #field_attrs + pub #ident: Option<#ty> + }); + + if let Some(expr) = default_expr { + field_assignments.push(quote! { + #ident: file.#ident.unwrap_or_else(|| #expr) + }); + } else { + default_bounds.push(quote! { #ty: Default }); + field_assignments.push(quote! { + #ident: file.#ident.unwrap_or_default() + }); + } + } + + let where_clause = if default_bounds.is_empty() { + where_clause.cloned() + } else { + let mut where_clause = where_clause.cloned(); + if let Some(wc) = &mut where_clause { + wc.predicates + .extend(default_bounds.into_iter().map(|bound| parse_quote!(#bound))); + } else { + where_clause = Some(parse_quote!(where #(#default_bounds),*)); + } + where_clause + }; + + // Conditionally include Merge derive based on feature + let derive_clause = if WITH_MERGE { + quote! { + #[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize, merge::Merge)] + } + } else { + quote! { + #[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] + } + }; + + Ok(quote! { + #derive_clause + #vis struct #file_ident { + #(#file_fields),* + } + + impl #impl_generics #name #ty_generics #where_clause { + pub fn from_file(file: Option<#file_ident #ty_generics>) -> Self { + let file = file.unwrap_or_default(); + Self { + #(#field_assignments),* + } + } + } + + impl #impl_generics From> for #name #ty_generics #where_clause { + fn from(value: Option<#file_ident #ty_generics>) -> Self { + Self::from_file(value) + } + } + } + .into()) +} + +fn add_trait_bouts(mut generics: Generics) -> Generics { + for param in &mut generics.params { + if let GenericParam::Type(type_param) = param { + type_param.bounds.push(parse_quote!(Default)); + } + } + generics +} + +fn parse_default_attr(attr: &Attribute) -> Result> { + if !attr.path().is_ident("default") { + return Ok(None); + } + + let meta = attr.parse_args::()?; + let name_value = match meta { + Meta::NameValue(nv) => nv, + _ => return Err(Error::new_spanned(attr, "Expected #[default = \"value\"]")), + }; + + match name_value.value { + Expr::Lit(expr_lit) => Ok(Some(Expr::Lit(expr_lit))), + _ => Err(Error::new_spanned( + &name_value.value, + "Default value must be a literal", + )), + } +} diff --git a/src/lib.rs b/src/lib.rs index b93cf3f..80ba3b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,16 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +mod from_file; -#[cfg(test)] -mod tests { - use super::*; +use from_file::impl_from_file; +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; +use syn::{DeriveInput, parse_macro_input}; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +#[proc_macro_error] +#[proc_macro_derive(FromFile, attributes(from_file))] +pub fn derive_from_file(input: TokenStream) -> TokenStream { + let inp = parse_macro_input!(input as DeriveInput); + match impl_from_file(&inp) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), } }