diff --git a/README.md b/README.md index 550e010..8c07e24 100644 --- a/README.md +++ b/README.md @@ -15,23 +15,21 @@ Procedural macro to derive configuration from files, with optional merging capab filecaster = "0.1" ``` -Example: - ```rust use filecaster::FromFile; -use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Default, Deserialize, Serialize, FromFile)] +#[derive(Debug, Clone, FromFile)] pub struct MyConfig { + #[from_file(default = "localhost")] pub host: String, - #[default = "8080"] + #[from_file(default = 8080)] pub port: u16, - #[default = "false"] + #[from_file(default = false)] pub enabled: bool, } fn main() { - // Simulate loading from a file (e.g., JSON, YAML) + // Simulate loading from a file (e.g., JSON, YAML, TOML) let file_content = r#" { "host": "localhost" diff --git a/src/from_file.rs b/src/from_file.rs index 23f3b46..456bd87 100644 --- a/src/from_file.rs +++ b/src/from_file.rs @@ -1,8 +1,8 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ - Attribute, Data, DeriveInput, Error, Expr, Fields, FieldsNamed, GenericParam, Generics, Meta, - Result, WhereClause, WherePredicate, parse_quote, parse2, + Attribute, Data, DeriveInput, Error, Expr, Fields, FieldsNamed, GenericParam, Generics, Lit, + Meta, MetaList, Result, WhereClause, WherePredicate, parse_quote, parse2, }; const WITH_MERGE: bool = cfg!(feature = "merge"); @@ -155,18 +155,7 @@ fn parse_from_file_default_attr(attrs: &[Attribute]) -> Result> { // Parse the content inside the parentheses of #[from_file(...)] return match &attr.meta { - Meta::List(meta_list) => { - let mut default_expr = None; - meta_list.parse_nested_meta(|meta| { - if meta.path.is_ident("default") { - let value = meta.value()?; - let expr = value.parse::()?; - default_expr = Some(expr); - } - Ok(()) - })?; - Ok(default_expr) - } + Meta::List(meta_list) => parse_default(meta_list), _ => Err(Error::new_spanned( attr, "Expected #[from_file(default = \"literal\")] or similar", @@ -176,6 +165,28 @@ fn parse_from_file_default_attr(attrs: &[Attribute]) -> Result> { Ok(None) } +fn parse_default(list: &MetaList) -> Result> { + let mut default_expr = None; + list.parse_nested_meta(|meta| { + if meta.path.is_ident("default") { + let value = meta.value()?; + let expr = value.parse::()?; + + if let Expr::Lit(expr_lit) = &expr { + if let Lit::Str(lit_str) = &expr_lit.lit { + default_expr = Some(parse_quote! { + #lit_str.to_string() + }); + return Ok(()); + } + } + default_expr = Some(expr); + } + Ok(()) + })?; + Ok(default_expr) +} + #[cfg(test)] mod tests { use claims::{assert_err, assert_none}; @@ -213,18 +224,6 @@ mod tests { assert_err!(extract_named_fields(&input)); } - #[test] - fn parse_default_attrs_picks_first_default() { - let attrs: Vec = vec![ - parse_quote!(#[foo]), - parse_quote!(#[from_file(default = "bar")]), - parse_quote!(#[from_file(default = "baz")]), - ]; - let expr = parse_from_file_default_attr(&attrs).unwrap().unwrap(); - // should pick the first default attribute - assert_eq!(expr, parse_quote!("bar")); - } - #[test] fn parse_default_attrs_none() { let attrs: Vec = vec![parse_quote!(#[foo])]; @@ -303,10 +302,15 @@ mod tests { fn build_derive_clause_defaults() { let derive_ts = build_derive_clause(); let s = derive_ts.to_string(); - dbg!(&s); - assert!(s.contains( - "derive (Debug , Clone , Default , serde :: Deserialize , serde :: Serialize)" - )); + if WITH_MERGE { + assert!(s.contains( + "derive (Debug , Clone , Default , serde :: Deserialize , serde :: Serialize , merge :: Merge)" + )); + } else { + assert!(s.contains( + "derive (Debug , Clone , Default , serde :: Deserialize , serde :: Serialize)" + )); + } } #[test] diff --git a/tests/from_file.rs b/tests/from_file.rs new file mode 100644 index 0000000..ff08416 --- /dev/null +++ b/tests/from_file.rs @@ -0,0 +1,111 @@ +use filecaster::FromFile; + +#[derive(Debug, Clone, PartialEq, FromFile)] +struct Simple { + x: i32, + #[from_file(default = "hello")] + y: String, +} + +#[derive(Debug, Clone, PartialEq, FromFile)] +struct NumericDefault { + a: i32, + #[from_file(default = 42)] + b: i32, +} + +#[test] +fn test_simple_defaults() { + // No file passed -> all fields fall back to defaults + let s = Simple::from_file(None); + assert_eq!( + s, + Simple { + x: 0, + y: "hello".to_string(), + } + ); +} + +#[test] +fn test_simple_override() { + // Manually construct the generated `SimpleFile` and override both fields + let file = SimpleFile { + x: Some(10), + y: Some("world".to_string()), + }; + let s = Simple::from_file(Some(file)); + assert_eq!( + s, + Simple { + x: 10, + y: "world".to_string(), + } + ); +} + +#[test] +fn test_simple_serde_empty() { + // Deserialize from JSON missing both fields -> both None + let json = "{}"; + let file: SimpleFile = serde_json::from_str(json).unwrap(); + let s = Simple::from_file(Some(file)); + assert_eq!(s.x, 0); + assert_eq!(s.y, "hello".to_string()); +} + +#[test] +fn test_simple_serde_partial() { + // Deserialize from JSON with only `x` + let json = r#"{"x":5}"#; + let file: SimpleFile = serde_json::from_str(json).unwrap(); + let s = Simple::from_file(Some(file)); + assert_eq!(s.x, 5); + assert_eq!(s.y, "hello".to_string()); +} + +#[test] +fn test_simple_serde_full() { + // Deserialize from JSON with both fields + let json = r#"{"x":7,"y":"rust"}"#; + let file: SimpleFile = serde_json::from_str(json).unwrap(); + let s = Simple::from_file(Some(file)); + assert_eq!(s.x, 7); + assert_eq!(s.y, "rust".to_string()); +} + +#[test] +fn test_numeric_default() { + // No file -> default `b = 42` + let n = NumericDefault::from_file(None); + assert_eq!(n, NumericDefault { a: 0, b: 42 }); + + // Override both + let file = NumericDefaultFile { + a: Some(7), + b: Some(99), + }; + let n2 = NumericDefault::from_file(Some(file)); + assert_eq!(n2, NumericDefault { a: 7, b: 99 }); +} + +#[cfg(feature = "merge")] +mod merge_tests { + use super::*; + use merge::Merge; + + #[test] + fn test_merge_simple_file() { + let mut f1 = SimpleFile { + x: Some(1), + y: None, + }; + let f2 = SimpleFile { + x: None, + y: Some("foo".to_string()), + }; + f1.merge(f2); + assert_eq!(f1.x, Some(1)); + assert_eq!(f1.y, Some("foo".to_string())); + } +}