diff --git a/Cargo.lock b/Cargo.lock index 4863d20..641c30e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,18 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filecaster" version = "0.1.0" +dependencies = [ + "filecaster-derive", + "merge", + "serde", + "serde_json", + "tempfile", + "toml", +] + +[[package]] +name = "filecaster-derive" +version = "0.1.0" dependencies = [ "claims", "merge", @@ -58,10 +70,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "serde_json", "syn", - "tempfile", - "toml", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 47bd27c..91175ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,40 +1,17 @@ -[package] -name = "filecaster" -version = "0.1.0" -edition = "2024" -authors = ["Kristofers Solo "] -description = "Procedural macro to derive configuration from files, with optional merging capabilities." -repository = "https://github.com/kristoferssolo/filecaster" -documentation = "https://docs.rs/filecaster" -homepage = "https://github.com/kristoferssolo/filecaster" -license = "MIT OR Apache-2.0" -keywords = ["proc-macro", "derive", "configuration", "file-parsing"] -categories = ["rust-patterns", "parsing", "config"] -exclude = ["/.github", "/.gitignore", "/tests", "*.png", "*.md"] -readme = "README.md" +[workspace] +resolver = "2" +members = ["filecaster", "filecaster-derive"] -[features] -default = [] -merge = ["dep:merge"] - -[dependencies] -proc-macro2 = "1.0" -quote = "1.0" -proc-macro-error2 = "2.0" -syn = { version = "2.0", features = ["extra-traits", "parsing"] } +[workspace.dependencies] serde = { version = "1.0", features = ["derive"] } -merge = { version = "0.2", optional = true } - -[dev-dependencies] +merge = "0.2.0" +# dev-dependencies claims = "0.8" serde_json = "1.0" -tempfile = "3.20" +tempfile = "3.10" toml = "0.9" -[lib] -proc-macro = true - -[lints.clippy] +[workspace.lints.clippy] pedantic = "warn" nursery = "warn" unwrap_used = "warn" diff --git a/filecaster-derive/Cargo.toml b/filecaster-derive/Cargo.toml new file mode 100644 index 0000000..4d5f598 --- /dev/null +++ b/filecaster-derive/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "filecaster-derive" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[features] +default = [] +merge = ["dep:merge"] + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +proc-macro-error2 = "2.0" +syn = { version = "2.0", features = ["extra-traits", "parsing"] } +serde.workspace = true +merge = { workspace = true, optional = true } + +[dev-dependencies] +claims.workspace = true diff --git a/src/derive_from_file.rs b/filecaster-derive/src/from_file.rs similarity index 93% rename from src/derive_from_file.rs rename to filecaster-derive/src/from_file.rs index 1bccebb..2ba28d8 100644 --- a/src/derive_from_file.rs +++ b/filecaster-derive/src/from_file.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ @@ -78,6 +80,15 @@ fn is_from_file_struct(ty: &Type) -> bool { false } +/// Extract the last identifier from a [`TypePath`]. +fn last_path_ident(ty: &Type) -> Result { + if let Type::Path(TypePath { qself: None, path }) = ty { + return Ok(path.segments.last().unwrap().ident.to_string()); + } + + Err(Error::new_spanned(ty, "expected a plain struct name")) +} + /// Build the shadow field + assignment for one original field fn build_file_field(field: &Field) -> Result<(TokenStream, TokenStream, Option)> { let ident = field @@ -87,38 +98,23 @@ fn build_file_field(field: &Field) -> Result<(TokenStream, TokenStream, Option - }; - let assign = quote! { - #ident: <#ty>::from_file(file.#ident) - }; - return Ok((field_decl, assign, None)); - } - - // Primitive / leaf field - let default_expr = parse_from_file_default_attr(&field.attrs)?; + // Nested struct -> delegate to its own `FromFile` impl let field_decl = quote! { #field_attrs pub #ident: Option<#ty> }; - let assign = default_expr.map_or_else( - || quote! { #ident: file.#ident.unwrap_or_default() }, - |expr| quote! { #ident: file.#ident.unwrap_or_else(|| #expr) }, - ); - let default = quote! { #ty: Default }; + let assign = quote! { + #ident: <#ty as filecaster::FromFile>::from_file(file.#ident) + }; - Ok((field_decl, assign, Some(default))) + let default_bound = Some(quote! { #ty: Default }); + + Ok((field_decl, assign, default_bound)) } /// Process all fields diff --git a/src/lib.rs b/filecaster-derive/src/lib.rs similarity index 97% rename from src/lib.rs rename to filecaster-derive/src/lib.rs index 4eaab79..5a48fcb 100644 --- a/src/lib.rs +++ b/filecaster-derive/src/lib.rs @@ -76,10 +76,9 @@ //! //! MIT OR Apache-2.0 -mod derive_from_file; mod from_file; -pub(crate) use derive_from_file::impl_from_file; +pub(crate) use from_file::impl_from_file; use proc_macro::TokenStream; use proc_macro_error2::proc_macro_error; use syn::{DeriveInput, parse_macro_input}; diff --git a/filecaster/Cargo.toml b/filecaster/Cargo.toml new file mode 100644 index 0000000..41d9f34 --- /dev/null +++ b/filecaster/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "filecaster" +version = "0.1.0" +edition = "2024" +authors = ["Kristofers Solo "] +description = "Procedural macro to derive configuration from files, with optional merging capabilities." +repository = "https://github.com/kristoferssolo/filecaster" +documentation = "https://docs.rs/filecaster" +homepage = "https://github.com/kristoferssolo/filecaster" +license = "MIT OR Apache-2.0" +keywords = ["proc-macro", "derive", "configuration", "file-parsing"] +categories = ["rust-patterns", "parsing", "config"] +exclude = ["/.github", "/.gitignore", "/tests", "*.png", "*.md"] +readme = "README.md" + +[features] +default = ["derive"] +derive = ["dep:filecaster-derive"] +merge = ["dep:merge", "filecaster-derive/merge"] + +[dependencies] +filecaster-derive = { path = "../filecaster-derive", optional = true } +serde.workspace = true +merge = { workspace = true, optional = true } + +[dev-dependencies] +serde_json.workspace = true +tempfile.workspace = true +toml.workspace = true diff --git a/src/from_file.rs b/filecaster/src/lib.rs similarity index 93% rename from src/from_file.rs rename to filecaster/src/lib.rs index a1b479a..da91ab2 100644 --- a/src/from_file.rs +++ b/filecaster/src/lib.rs @@ -1,3 +1,4 @@ +pub use filecaster_derive::FromFile; use serde::{Deserialize, Serialize}; /// Marker for types that can be built from an `Option` produced by the macro. diff --git a/tests/from_file.rs b/filecaster/tests/from_file.rs similarity index 100% rename from tests/from_file.rs rename to filecaster/tests/from_file.rs diff --git a/tests/from_file_io.rs b/filecaster/tests/from_file_io.rs similarity index 100% rename from tests/from_file_io.rs rename to filecaster/tests/from_file_io.rs diff --git a/filecaster/tests/nested_structure.rs b/filecaster/tests/nested_structure.rs new file mode 100644 index 0000000..622fdd1 --- /dev/null +++ b/filecaster/tests/nested_structure.rs @@ -0,0 +1,14 @@ +use filecaster::FromFile; + +#[derive(Debug, Default, Clone, PartialEq, FromFile)] +pub struct Coordinates { + x: i32, + y: i32, +} + +#[derive(Debug, Clone, PartialEq, FromFile)] +pub struct Parent { + #[from_file(default = "Foo")] + name: String, + coordinates: Coordinates, +}