diff --git a/filecaster-derive/src/from_file.rs b/filecaster-derive/src/from_file.rs index b3c6b31..c04dd9c 100644 --- a/filecaster-derive/src/from_file.rs +++ b/filecaster-derive/src/from_file.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ Attribute, Data, DeriveInput, Error, Expr, Field, Fields, FieldsNamed, GenericParam, Generics, - Lit, Meta, MetaList, Result, Type, TypePath, WhereClause, WherePredicate, parse_quote, parse2, + Ident, Lit, Meta, MetaList, Result, Type, parse_quote, }; const WITH_MERGE: bool = cfg!(feature = "merge"); @@ -17,9 +17,8 @@ pub fn impl_from_file(input: &DeriveInput) -> Result { let file_ident = format_ident!("{name}File"); let fields = extract_named_fields(input)?; - let (field_assignments, file_fields, default_bounds) = process_fields(fields)?; + let (field_assignments, file_fields) = process_fields(fields)?; - let where_clause = build_where_clause(where_clause.cloned(), default_bounds)?; let derive_clause = build_derive_clause(); Ok(quote! { @@ -71,13 +70,15 @@ fn extract_named_fields(input: &DeriveInput) -> Result<&FieldsNamed> { } /// Build the shadow field + assignment for one original field -fn build_file_field(field: &Field) -> Result<(TokenStream, TokenStream, Option)> { +fn build_file_field(field: &Field) -> Result<(TokenStream, TokenStream)> { let ident = field .ident .as_ref() .ok_or_else(|| Error::new_spanned(field, "Expected named fields"))?; let ty = &field.ty; + let default_override = parse_from_file_default_attr(&field.attrs)?; + let field_attrs = if WITH_MERGE { quote! { #[merge(strategy = merge::option::overwrite_none)] } } else { @@ -85,59 +86,41 @@ fn build_file_field(field: &Field) -> Result<(TokenStream, TokenStream, Option delegate to its own `FromFile` impl + let shadow_ty = quote! { <#ty as filecaster::FromFile>::Shadow }; let field_decl = quote! { #field_attrs - pub #ident: Option<#ty> - }; - let assign = quote! { - #ident: <#ty as filecaster::FromFile>::from_file(file.#ident) + pub #ident: Option<#shadow_ty> }; - let default_bound = Some(quote! { #ty: Default }); + let assign = build_file_assing(ident, ty, default_override); - Ok((field_decl, assign, default_bound)) + Ok((field_decl, assign)) +} + +fn build_file_assing(ident: &Ident, ty: &Type, default_override: Option) -> TokenStream { + if let Some(expr) = default_override { + return quote! { + #ident: file.#ident.map(|inner| <#ty as filecaster::FromFile>::from_file(Some(inner))).unwrap_or(#expr) + }; + } + quote! { + #ident: <#ty as filecaster::FromFile>::from_file(file.#ident) + } } /// Process all fields -fn process_fields( - fields: &FieldsNamed, -) -> Result<(Vec, Vec, Vec)> { +fn process_fields(fields: &FieldsNamed) -> Result<(Vec, Vec)> { fields.named.iter().try_fold( - (Vec::new(), Vec::new(), Vec::new()), - |(mut assignments, mut file_fields, mut defaults), field| { - let (file_field, assignment, default_value) = build_file_field(field)?; + (Vec::new(), Vec::new()), + |(mut assignments, mut file_fields), field| { + let (file_field, assignment) = build_file_field(field)?; file_fields.push(file_field); assignments.push(assignment); - if let Some(value) = default_value { - defaults.push(value); - } - Ok((assignments, file_fields, defaults)) + Ok((assignments, file_fields)) }, ) } -/// Where-clause helpers -fn build_where_clause( - where_clause: Option, - default_bounds: Vec, -) -> Result> { - if default_bounds.is_empty() { - return Ok(where_clause); - } - - let mut where_clause = where_clause; - if let Some(wc) = &mut where_clause { - for bound in default_bounds { - let predicate = parse2::(bound.clone()) - .map_err(|_| Error::new_spanned(&bound, "Failed to parse where predicate"))?; - wc.predicates.push(predicate); - } - } else { - where_clause = Some(parse_quote!(where #(#default_bounds),*)); - } - Ok(where_clause) -} - /// Derive clause for the shadow struct fn build_derive_clause() -> TokenStream { quote! { @@ -150,8 +133,8 @@ fn build_derive_clause() -> TokenStream { /// Add Default bound to every generic parameter fn add_trait_bounds(mut generics: Generics) -> Generics { for param in &mut generics.params { - if let GenericParam::Type(type_param) = param { - type_param.bounds.push(parse_quote!(Default)); + if let GenericParam::Type(ty) = param { + ty.bounds.push(parse_quote!(Default)); } } generics @@ -250,63 +233,10 @@ mod tests { b: String, } }; - let (assign, file_fields, bounds) = process_fields(&fields).unwrap(); + let (assign, file_fields) = process_fields(&fields).unwrap(); // two fields assert_eq!(assign.len(), 2); assert_eq!(file_fields.len(), 2); - // a uses unwrap_or_else - assert!( - assign[0] - .to_string() - .contains("a : file . a . unwrap_or_else") - ); - // b uses unwrap_or_default - assert!( - assign[1] - .to_string() - .contains("b : file . b . unwrap_or_default") - ); - // default-bound should only mention String - assert_eq!(bounds.len(), 1); - assert!(bounds[0].to_string().contains("String : Default")); - } - - #[test] - fn build_where_clause_to_new() { - let bounds = vec![quote! { A: Default }, quote! { B: Default }]; - let wc = build_where_clause(None, bounds).unwrap().unwrap(); - let s = wc.to_token_stream().to_string(); - assert!(s.contains("where A : Default , B : Default")); - } - - #[test] - fn build_where_clause_append_existing() { - let orig: WhereClause = parse_quote!(where X: Clone); - let bounds = vec![quote! { Y: Default }]; - let wc = build_where_clause(Some(orig.clone()), bounds) - .unwrap() - .unwrap(); - let preds: Vec<_> = wc - .predicates - .iter() - .map(|p| p.to_token_stream().to_string()) - .collect(); - assert!(preds.contains(&"X : Clone".to_string())); - assert!(preds.contains(&"Y : Default".to_string())); - } - - #[test] - fn build_where_clause_no_bounds_keeps_original() { - let orig: WhereClause = parse_quote!(where Z: Eq); - let wc = build_where_clause(Some(orig.clone()), vec![]) - .unwrap() - .unwrap(); - let preds: Vec<_> = wc - .predicates - .iter() - .map(|p| p.to_token_stream().to_string()) - .collect(); - assert_eq!(preds, vec!["Z : Eq".to_string()]); } #[test] diff --git a/filecaster-derive/src/lib.rs b/filecaster-derive/src/lib.rs index 5a48fcb..d2ded52 100644 --- a/filecaster-derive/src/lib.rs +++ b/filecaster-derive/src/lib.rs @@ -83,7 +83,7 @@ use proc_macro::TokenStream; use proc_macro_error2::proc_macro_error; use syn::{DeriveInput, parse_macro_input}; -/// Implements the `FromFile` derive macro. +/// Implements the [`FromFile`] trait. /// /// This macro processes the `#[from_file]` attribute on structs to generate /// code for loading data from files. diff --git a/filecaster/src/lib.rs b/filecaster/src/lib.rs index 65454f5..d110217 100644 --- a/filecaster/src/lib.rs +++ b/filecaster/src/lib.rs @@ -2,26 +2,10 @@ pub use filecaster_derive::FromFile; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -/// Marker for types that can be built from an `Option` produced by the macro. +/// Marker for types that can be built from an [`Option`] produced by the macro. pub trait FromFile: Sized { - fn from_file(file: Option) -> Self; - - /// Associated shadow type generated by the macro. - #[cfg(feature = "serde")] - type Shadow: Default + Serialize + for<'de> Deserialize<'de>; - #[cfg(not(feature = "serde"))] type Shadow: Default; -} - -#[cfg(feature = "serde")] -impl FromFile for T -where - T: Default + Serialize + for<'de> Deserialize<'de>, -{ - type Shadow = T; - fn from_file(file: Option) -> Self { - file.unwrap_or_default() - } + fn from_file(file: Option) -> Self; } #[cfg(not(feature = "serde"))] @@ -30,7 +14,18 @@ where T: Default, { type Shadow = T; - fn from_file(file: Option) -> Self { + fn from_file(file: Option) -> Self { + file.unwrap_or_default() + } +} + +#[cfg(feature = "serde")] +impl FromFile for T +where + T: Default + Serialize + for<'de> Deserialize<'de>, +{ + type Shadow = T; + fn from_file(file: Option) -> Self { file.unwrap_or_default() } } diff --git a/filecaster/tests/nested_structure.rs b/filecaster/tests/nested_structure.rs index 622fdd1..e4c16a9 100644 --- a/filecaster/tests/nested_structure.rs +++ b/filecaster/tests/nested_structure.rs @@ -1,14 +1,141 @@ use filecaster::FromFile; -#[derive(Debug, Default, Clone, PartialEq, FromFile)] +#[derive(Debug, Clone, PartialEq, FromFile)] pub struct Coordinates { x: i32, y: i32, } +impl Coordinates { + fn new(x: i32, y: i32) -> Self { + Self { x, y } + } +} + +impl CoordinatesFile { + fn new(x: i32, y: i32) -> Self { + Self { + x: Some(x), + y: Some(y), + } + } +} + +#[derive(Debug, Clone, PartialEq, FromFile)] +struct Wrapper { + parent: Parent, +} + +// And one more level +#[derive(Debug, Clone, PartialEq, FromFile)] +struct DoubleWrapper { + wrapper: Wrapper, +} + #[derive(Debug, Clone, PartialEq, FromFile)] pub struct Parent { #[from_file(default = "Foo")] name: String, coordinates: Coordinates, } + +#[test] +fn parent_all_defaults() { + let p = Parent::from_file(None); + assert_eq!(p.name, "Foo".to_string()); + assert_eq!(p.coordinates, Coordinates::new(0, 0)); +} + +#[test] +fn parent_partial_shadow_merges_defaults() { + let shadow = ParentFile { + name: None, + coordinates: Some(CoordinatesFile::new(1, 2)), + }; + let p = Parent::from_file(Some(shadow)); + assert_eq!(p.name, "Foo".to_string()); + assert_eq!(p.coordinates, Coordinates::new(1, 2)); +} + +#[test] +fn parent_full_shadow_overrides_everything() { + let shadow = ParentFile { + name: Some("Bar".into()), + coordinates: Some(CoordinatesFile::new(42, 24)), + }; + let p = Parent::from_file(Some(shadow)); + assert_eq!(p.name, "Bar".to_string()); + assert_eq!(p.coordinates, Coordinates::new(42, 24)); +} + +#[test] +fn wrapper_all_defaults() { + // None → WrapperFile::default() → parent = Parent::from_file(None) + let w = Wrapper::from_file(None); + assert_eq!(w.parent.name, "Foo".to_string()); + assert_eq!(w.parent.coordinates, Coordinates::new(0, 0)); +} + +#[test] +fn wrapper_partial_parent() { + // We supply only coordinates + let shadow = WrapperFile { + parent: Some(ParentFile { + name: None, + coordinates: Some(CoordinatesFile::new(5, -2)), + }), + }; + let w = Wrapper::from_file(Some(shadow)); + assert_eq!(w.parent.name, "Foo".to_string()); + assert_eq!(w.parent.coordinates, Coordinates::new(5, -2)); +} + +#[test] +fn wrapper_full_parent_override() { + let shadow = WrapperFile { + parent: Some(ParentFile { + name: Some("Baz".into()), + coordinates: Some(CoordinatesFile::new(1, 1)), + }), + }; + let w = Wrapper::from_file(Some(shadow)); + assert_eq!(w.parent.name, "Baz".to_string()); + assert_eq!(w.parent.coordinates, Coordinates::new(1, 1)); +} + +#[test] +fn double_wrapper_all_defaults() { + let dw = DoubleWrapper::from_file(None); + assert_eq!(dw.wrapper.parent.name, "Foo".to_string()); + assert_eq!(dw.wrapper.parent.coordinates, Coordinates::new(0, 0)); +} + +#[test] +fn double_wrapper_partial_deep() { + let shadow = DoubleWrapperFile { + wrapper: Some(WrapperFile { + parent: Some(ParentFile { + name: None, + coordinates: Some(CoordinatesFile::new(10, 20)), + }), + }), + }; + let dw = DoubleWrapper::from_file(Some(shadow)); + assert_eq!(dw.wrapper.parent.name, "Foo".to_string()); + assert_eq!(dw.wrapper.parent.coordinates, Coordinates::new(10, 20)); +} + +#[test] +fn double_wrapper_full_override_deep() { + let shadow = DoubleWrapperFile { + wrapper: Some(WrapperFile { + parent: Some(ParentFile { + name: Some("Deep".into()), + coordinates: Some(CoordinatesFile::new(3, 4)), + }), + }), + }; + let dw = DoubleWrapper::from_file(Some(shadow)); + assert_eq!(dw.wrapper.parent.name, "Deep".to_string()); + assert_eq!(dw.wrapper.parent.coordinates, Coordinates::new(3, 4)); +}