test: add integration tests

This commit is contained in:
Kristofers Solo 2025-07-14 21:11:18 +03:00
parent 360f75cb1a
commit cd8fd27e69
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
3 changed files with 150 additions and 37 deletions

View File

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

View File

@ -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<Option<Expr>> {
// 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::<Expr>()?;
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<Option<Expr>> {
Ok(None)
}
fn parse_default(list: &MetaList) -> Result<Option<Expr>> {
let mut default_expr = None;
list.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
let value = meta.value()?;
let expr = value.parse::<Expr>()?;
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<Attribute> = 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<Attribute> = 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]

111
tests/from_file.rs Normal file
View File

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