feat: add FromFile implementation

This commit is contained in:
Kristofers Solo 2025-07-14 18:23:24 +03:00
parent bed2a26476
commit 62f48cf3cd
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
4 changed files with 345 additions and 10 deletions

169
Cargo.lock generated Normal file
View File

@ -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"

View File

@ -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

151
src/from_file.rs Normal file
View File

@ -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<TokenStream> {
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<Option<#file_ident #ty_generics>> 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<Option<Expr>> {
if !attr.path().is_ident("default") {
return Ok(None);
}
let meta = attr.parse_args::<Meta>()?;
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",
)),
}
}

View File

@ -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(),
}
}