mirror of
https://github.com/kristoferssolo/filecaster.git
synced 2025-10-21 19:00:34 +00:00
fix: nested structure
This commit is contained in:
parent
db1dab2aa1
commit
60488d364e
@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
|
|||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
use syn::{
|
use syn::{
|
||||||
Attribute, Data, DeriveInput, Error, Expr, Field, Fields, FieldsNamed, GenericParam, Generics,
|
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");
|
const WITH_MERGE: bool = cfg!(feature = "merge");
|
||||||
@ -17,9 +17,8 @@ pub fn impl_from_file(input: &DeriveInput) -> Result<TokenStream> {
|
|||||||
let file_ident = format_ident!("{name}File");
|
let file_ident = format_ident!("{name}File");
|
||||||
|
|
||||||
let fields = extract_named_fields(input)?;
|
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();
|
let derive_clause = build_derive_clause();
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
@ -71,13 +70,15 @@ fn extract_named_fields(input: &DeriveInput) -> Result<&FieldsNamed> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the shadow field + assignment for one original field
|
/// Build the shadow field + assignment for one original field
|
||||||
fn build_file_field(field: &Field) -> Result<(TokenStream, TokenStream, Option<TokenStream>)> {
|
fn build_file_field(field: &Field) -> Result<(TokenStream, TokenStream)> {
|
||||||
let ident = field
|
let ident = field
|
||||||
.ident
|
.ident
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| Error::new_spanned(field, "Expected named fields"))?;
|
.ok_or_else(|| Error::new_spanned(field, "Expected named fields"))?;
|
||||||
let ty = &field.ty;
|
let ty = &field.ty;
|
||||||
|
|
||||||
|
let default_override = parse_from_file_default_attr(&field.attrs)?;
|
||||||
|
|
||||||
let field_attrs = if WITH_MERGE {
|
let field_attrs = if WITH_MERGE {
|
||||||
quote! { #[merge(strategy = merge::option::overwrite_none)] }
|
quote! { #[merge(strategy = merge::option::overwrite_none)] }
|
||||||
} else {
|
} else {
|
||||||
@ -85,59 +86,41 @@ fn build_file_field(field: &Field) -> Result<(TokenStream, TokenStream, Option<T
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Nested struct -> delegate to its own `FromFile` impl
|
// Nested struct -> delegate to its own `FromFile` impl
|
||||||
|
let shadow_ty = quote! { <#ty as filecaster::FromFile>::Shadow };
|
||||||
let field_decl = quote! {
|
let field_decl = quote! {
|
||||||
#field_attrs
|
#field_attrs
|
||||||
pub #ident: Option<#ty>
|
pub #ident: Option<#shadow_ty>
|
||||||
};
|
|
||||||
let assign = quote! {
|
|
||||||
#ident: <#ty as filecaster::FromFile>::from_file(file.#ident)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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<Expr>) -> 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
|
/// Process all fields
|
||||||
fn process_fields(
|
fn process_fields(fields: &FieldsNamed) -> Result<(Vec<TokenStream>, Vec<TokenStream>)> {
|
||||||
fields: &FieldsNamed,
|
|
||||||
) -> Result<(Vec<TokenStream>, Vec<TokenStream>, Vec<TokenStream>)> {
|
|
||||||
fields.named.iter().try_fold(
|
fields.named.iter().try_fold(
|
||||||
(Vec::new(), Vec::new(), Vec::new()),
|
(Vec::new(), Vec::new()),
|
||||||
|(mut assignments, mut file_fields, mut defaults), field| {
|
|(mut assignments, mut file_fields), field| {
|
||||||
let (file_field, assignment, default_value) = build_file_field(field)?;
|
let (file_field, assignment) = build_file_field(field)?;
|
||||||
file_fields.push(file_field);
|
file_fields.push(file_field);
|
||||||
assignments.push(assignment);
|
assignments.push(assignment);
|
||||||
if let Some(value) = default_value {
|
Ok((assignments, file_fields))
|
||||||
defaults.push(value);
|
|
||||||
}
|
|
||||||
Ok((assignments, file_fields, defaults))
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where-clause helpers
|
|
||||||
fn build_where_clause(
|
|
||||||
where_clause: Option<WhereClause>,
|
|
||||||
default_bounds: Vec<TokenStream>,
|
|
||||||
) -> Result<Option<WhereClause>> {
|
|
||||||
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::<WherePredicate>(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
|
/// Derive clause for the shadow struct
|
||||||
fn build_derive_clause() -> TokenStream {
|
fn build_derive_clause() -> TokenStream {
|
||||||
quote! {
|
quote! {
|
||||||
@ -150,8 +133,8 @@ fn build_derive_clause() -> TokenStream {
|
|||||||
/// Add Default bound to every generic parameter
|
/// Add Default bound to every generic parameter
|
||||||
fn add_trait_bounds(mut generics: Generics) -> Generics {
|
fn add_trait_bounds(mut generics: Generics) -> Generics {
|
||||||
for param in &mut generics.params {
|
for param in &mut generics.params {
|
||||||
if let GenericParam::Type(type_param) = param {
|
if let GenericParam::Type(ty) = param {
|
||||||
type_param.bounds.push(parse_quote!(Default));
|
ty.bounds.push(parse_quote!(Default));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
generics
|
generics
|
||||||
@ -250,63 +233,10 @@ mod tests {
|
|||||||
b: String,
|
b: String,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (assign, file_fields, bounds) = process_fields(&fields).unwrap();
|
let (assign, file_fields) = process_fields(&fields).unwrap();
|
||||||
// two fields
|
// two fields
|
||||||
assert_eq!(assign.len(), 2);
|
assert_eq!(assign.len(), 2);
|
||||||
assert_eq!(file_fields.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]
|
#[test]
|
||||||
|
|||||||
@ -83,7 +83,7 @@ use proc_macro::TokenStream;
|
|||||||
use proc_macro_error2::proc_macro_error;
|
use proc_macro_error2::proc_macro_error;
|
||||||
use syn::{DeriveInput, parse_macro_input};
|
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
|
/// This macro processes the `#[from_file]` attribute on structs to generate
|
||||||
/// code for loading data from files.
|
/// code for loading data from files.
|
||||||
|
|||||||
@ -2,26 +2,10 @@ pub use filecaster_derive::FromFile;
|
|||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Marker for types that can be built from an `Option<Shadow>` produced by the macro.
|
/// Marker for types that can be built from an [`Option<Shadow>`] produced by the macro.
|
||||||
pub trait FromFile: Sized {
|
pub trait FromFile: Sized {
|
||||||
fn from_file(file: Option<Self::Shadow>) -> 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;
|
type Shadow: Default;
|
||||||
}
|
fn from_file(file: Option<Self::Shadow>) -> Self;
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
impl<T> FromFile for T
|
|
||||||
where
|
|
||||||
T: Default + Serialize + for<'de> Deserialize<'de>,
|
|
||||||
{
|
|
||||||
type Shadow = T;
|
|
||||||
fn from_file(file: Option<Self::Shadow>) -> Self {
|
|
||||||
file.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "serde"))]
|
#[cfg(not(feature = "serde"))]
|
||||||
@ -30,7 +14,18 @@ where
|
|||||||
T: Default,
|
T: Default,
|
||||||
{
|
{
|
||||||
type Shadow = T;
|
type Shadow = T;
|
||||||
fn from_file(file: Option<Self::Shadow>) -> Self {
|
fn from_file(file: Option<Self>) -> Self {
|
||||||
|
file.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<T> FromFile for T
|
||||||
|
where
|
||||||
|
T: Default + Serialize + for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
type Shadow = T;
|
||||||
|
fn from_file(file: Option<Self>) -> Self {
|
||||||
file.unwrap_or_default()
|
file.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,141 @@
|
|||||||
use filecaster::FromFile;
|
use filecaster::FromFile;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, FromFile)]
|
#[derive(Debug, Clone, PartialEq, FromFile)]
|
||||||
pub struct Coordinates {
|
pub struct Coordinates {
|
||||||
x: i32,
|
x: i32,
|
||||||
y: 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)]
|
#[derive(Debug, Clone, PartialEq, FromFile)]
|
||||||
pub struct Parent {
|
pub struct Parent {
|
||||||
#[from_file(default = "Foo")]
|
#[from_file(default = "Foo")]
|
||||||
name: String,
|
name: String,
|
||||||
coordinates: Coordinates,
|
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));
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user