diff --git a/Cargo.lock b/Cargo.lock index 79eb32d..2353696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -301,6 +301,15 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_macro" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -1951,6 +1960,7 @@ version = "0.1.0" dependencies = [ "color-eyre", "crossterm 0.29.0", + "derive_macro", "dirs", "ratatui", "serde", diff --git a/Cargo.toml b/Cargo.toml index b0acb41..57a7d8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ toml = "0.8" dirs = "6.0" transmission-rpc = "0.5" url = "2.5" +derive_macro = { path = "derive_macro" } diff --git a/derive_macro/Cargo.lock b/derive_macro/Cargo.lock new file mode 100644 index 0000000..3a90cca --- /dev/null +++ b/derive_macro/Cargo.lock @@ -0,0 +1,47 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "derive_macro" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/derive_macro/Cargo.toml b/derive_macro/Cargo.toml new file mode 100644 index 0000000..4cbb5a8 --- /dev/null +++ b/derive_macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "derive_macro" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["proc-macro"] + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } diff --git a/derive_macro/src/lib.rs b/derive_macro/src/lib.rs new file mode 100644 index 0000000..bfbd8e2 --- /dev/null +++ b/derive_macro/src/lib.rs @@ -0,0 +1,11 @@ +mod merge; + +use proc_macro::TokenStream; +use syn::{DeriveInput, parse_macro_input}; + +#[proc_macro_derive(Merge)] +pub fn merge_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + merge::impl_merge_derive(input) +} + diff --git a/derive_macro/src/merge.rs b/derive_macro/src/merge.rs new file mode 100644 index 0000000..bd682f3 --- /dev/null +++ b/derive_macro/src/merge.rs @@ -0,0 +1,58 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Data, DeriveInput, Fields, Type}; + +pub fn impl_merge_derive(input: DeriveInput) -> TokenStream { + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let fields = match input.data { + Data::Struct(data) => match data.fields { + Fields::Named(fields) => fields.named, + _ => unimplemented!("Only named fields are supported for Merge derive macro"), + }, + _ => unimplemented!("Only structs are supported for Merge derive macro"), + }; + + let merge_logic = fields.iter().map(|field| { + let field_name = &field.ident; + let field_type = &field.ty; + + // Check if the field is an Option + if let Type::Path(type_path) = field_type { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "Option" { + // This is an Option field + return quote! { + if let Some(o_val) = other.#field_name { + if let Some(s_val) = self.#field_name.as_mut() { + // If both are Some, attempt to merge recursively + s_val.merge(o_val); + } else { + // If self is None, take the other's Some value + self.#field_name = Some(o_val); + } + } + // If other is None, self remains unchanged + }; + } + } + } + + // For non-Option fields, attempt to merge recursively + quote! { + self.#field_name.merge(other.#field_name); + } + }); + + let expanded = quote! { + impl #impl_generics Merge for #name #ty_generics #where_clause { + fn merge(&mut self, other: Self) { + #(#merge_logic)* + } + } + }; + + expanded.into() +} + diff --git a/src/app/command.rs b/src/app/command.rs index b234b4e..4827cc7 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -20,8 +20,9 @@ impl Torrents { self.client .torrent_action(action, vec![id]) .await - .map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?; - + .map_err(|e| { + color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()) + })?; } } Ok(()) @@ -48,7 +49,9 @@ impl Torrents { self.client .torrent_action(action, vec![id]) .await - .map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?; + .map_err(|e| { + color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()) + })?; } Ok(()) } @@ -71,12 +74,18 @@ impl Torrents { self.client .torrent_set_location(vec![id], location.to_string_lossy().into(), move_from) .await - .map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?; + .map_err(|e| { + color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()) + })?; } Ok(()) } - pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> color_eyre::eyre::Result<()> { + pub async fn delete( + &mut self, + ids: Selected, + delete_local_data: bool, + ) -> color_eyre::eyre::Result<()> { self.client .torrent_remove(ids.into(), delete_local_data) .await @@ -89,7 +98,9 @@ impl Torrents { self.client .torrent_rename_path(vec![id], old_name, name.to_string_lossy().into()) .await - .map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?; + .map_err(|e| { + color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()) + })?; } Ok(()) } diff --git a/src/config/colors.rs b/src/config/colors.rs index c49ffab..cb41d33 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -1,8 +1,9 @@ -use crate::merge_fields; +use crate::merge::Merge; +use derive_macro::Merge; use ratatui::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Merge)] pub struct ColorsConfig { pub highlight_background: Option, pub highlight_foreground: Option, @@ -32,18 +33,6 @@ impl ColorsConfig { None => Color::Reset, } } - - pub fn merge(&mut self, other: Self) { - merge_fields!( - self, - other, - highlight_background, - highlight_foreground, - warning_foreground, - info_foreground, - error_foreground - ); - } } impl Default for ColorsConfig { diff --git a/src/config/keybinds.rs b/src/config/keybinds.rs index fa37a24..502d1ca 100644 --- a/src/config/keybinds.rs +++ b/src/config/keybinds.rs @@ -1,7 +1,8 @@ +use crate::merge::Merge; +use derive_macro::Merge; use serde::{Deserialize, Serialize}; -use crate::merge_fields; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Merge)] pub struct KeybindsConfig { pub quit: Option, pub next_tab: Option, @@ -19,29 +20,6 @@ pub struct KeybindsConfig { pub toggle_help: Option, } -impl KeybindsConfig { - pub fn merge(&mut self, other: Self) { - merge_fields!( - self, - other, - quit, - next_tab, - prev_tab, - next_torrent, - prev_torrent, - switch_tab_1, - switch_tab_2, - switch_tab_3, - toggle_torrent, - toggle_all, - delete, - delete_force, - select, - toggle_help - ); - } -} - impl Default for KeybindsConfig { fn default() -> Self { Self { diff --git a/src/config/mod.rs b/src/config/mod.rs index b0c893b..92949bf 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,24 +1,15 @@ mod colors; mod keybinds; +use crate::merge::Merge; use color_eyre::Result; use colors::ColorsConfig; +use derive_macro::Merge; use keybinds::KeybindsConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -#[macro_export] -macro_rules! merge_fields { - ($self:ident, $other:ident, $($field:ident),*) => { - $( - if let Some($field) = $other.$field { - $self.$field = Some($field); - } - )* - }; -} - -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Merge)] pub struct Config { pub keybinds: KeybindsConfig, pub colors: ColorsConfig, @@ -64,9 +55,4 @@ impl Config { }); Ok(config_dir.join("traxor").join("config.toml")) } - - pub fn merge(&mut self, other: Self) { - self.keybinds.merge(other.keybinds); - self.colors.merge(other.colors); - } } diff --git a/src/handler.rs b/src/handler.rs index b72e6ee..bfa33b0 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -72,8 +72,12 @@ pub fn get_action(key_event: KeyEvent, app: &App) -> Option { _ if matches_keybind(&key_event, &config_keybinds.quit) => Some(Action::Quit), _ if matches_keybind(&key_event, &config_keybinds.next_tab) => Some(Action::NextTab), _ if matches_keybind(&key_event, &config_keybinds.prev_tab) => Some(Action::PrevTab), - _ if matches_keybind(&key_event, &config_keybinds.next_torrent) => Some(Action::NextTorrent), - _ if matches_keybind(&key_event, &config_keybinds.prev_torrent) => Some(Action::PrevTorrent), + _ if matches_keybind(&key_event, &config_keybinds.next_torrent) => { + Some(Action::NextTorrent) + } + _ if matches_keybind(&key_event, &config_keybinds.prev_torrent) => { + Some(Action::PrevTorrent) + } _ if matches_keybind(&key_event, &config_keybinds.switch_tab_1) => { Some(Action::SwitchTab(0)) } diff --git a/src/lib.rs b/src/lib.rs index 9767bf5..3500dc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ -pub mod config; pub mod app; +pub mod config; pub mod event; pub mod handler; pub mod log; +pub mod merge; pub mod tui; -pub mod ui; \ No newline at end of file +pub mod ui; diff --git a/src/main.rs b/src/main.rs index 3d8df7d..c7caec6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,4 +53,3 @@ async fn main() -> Result<()> { tui.exit()?; Ok(()) } - diff --git a/src/merge.rs b/src/merge.rs new file mode 100644 index 0000000..cd9e0e4 --- /dev/null +++ b/src/merge.rs @@ -0,0 +1,21 @@ +pub trait Merge { + fn merge(&mut self, other: Self); +} + +impl Merge for String { + fn merge(&mut self, other: Self) { + *self = other; + } +} + +impl Merge for u32 { + fn merge(&mut self, other: Self) { + *self = other; + } +} + +impl Merge for bool { + fn merge(&mut self, other: Self) { + *self = other; + } +} diff --git a/src/ui/help.rs b/src/ui/help.rs index fd5e1a1..93e6d56 100644 --- a/src/ui/help.rs +++ b/src/ui/help.rs @@ -1,5 +1,5 @@ -use ratatui::{prelude::*, widgets::*}; use crate::app::App; +use ratatui::{prelude::*, widgets::*}; pub fn render_help(frame: &mut Frame, app: &App) { let block = Block::default() @@ -11,23 +11,62 @@ pub fn render_help(frame: &mut Frame, app: &App) { let keybinds = &app.config.keybinds; let rows = vec![ - Row::new(vec![Cell::from(keybinds.toggle_help.as_deref().unwrap_or("?")), Cell::from("Show help")]), - Row::new(vec![Cell::from(keybinds.quit.as_deref().unwrap_or("q")), Cell::from("Quit")]), - Row::new(vec![Cell::from(keybinds.prev_tab.as_deref().unwrap_or("h")), Cell::from("Left")]), - Row::new(vec![Cell::from(keybinds.next_tab.as_deref().unwrap_or("l")), Cell::from("Right")]), - Row::new(vec![Cell::from(keybinds.next_torrent.as_deref().unwrap_or("j")), Cell::from("Down")]), - Row::new(vec![Cell::from(keybinds.prev_torrent.as_deref().unwrap_or("k")), Cell::from("Up")]), - Row::new(vec![Cell::from(keybinds.switch_tab_1.as_deref().unwrap_or("1")), Cell::from("Switch to All tab")]), - Row::new(vec![Cell::from(keybinds.switch_tab_2.as_deref().unwrap_or("2")), Cell::from("Switch to Active tab")]), + Row::new(vec![ + Cell::from(keybinds.toggle_help.as_deref().unwrap_or("?")), + Cell::from("Show help"), + ]), + Row::new(vec![ + Cell::from(keybinds.quit.as_deref().unwrap_or("q")), + Cell::from("Quit"), + ]), + Row::new(vec![ + Cell::from(keybinds.prev_tab.as_deref().unwrap_or("h")), + Cell::from("Left"), + ]), + Row::new(vec![ + Cell::from(keybinds.next_tab.as_deref().unwrap_or("l")), + Cell::from("Right"), + ]), + Row::new(vec![ + Cell::from(keybinds.next_torrent.as_deref().unwrap_or("j")), + Cell::from("Down"), + ]), + Row::new(vec![ + Cell::from(keybinds.prev_torrent.as_deref().unwrap_or("k")), + Cell::from("Up"), + ]), + Row::new(vec![ + Cell::from(keybinds.switch_tab_1.as_deref().unwrap_or("1")), + Cell::from("Switch to All tab"), + ]), + Row::new(vec![ + Cell::from(keybinds.switch_tab_2.as_deref().unwrap_or("2")), + Cell::from("Switch to Active tab"), + ]), Row::new(vec![ Cell::from(keybinds.switch_tab_3.as_deref().unwrap_or("3")), Cell::from("Switch to Downloading tab"), ]), - Row::new(vec![Cell::from(keybinds.toggle_torrent.as_deref().unwrap_or("t")), Cell::from("Toggle torrent")]), - Row::new(vec![Cell::from(keybinds.toggle_all.as_deref().unwrap_or("a")), Cell::from("Toggle all torrents")]), - Row::new(vec![Cell::from(keybinds.delete.as_deref().unwrap_or("d")), Cell::from("Delete torrent")]), - Row::new(vec![Cell::from(keybinds.delete_force.as_deref().unwrap_or("D")), Cell::from("Delete torrent and data")]), - Row::new(vec![Cell::from(keybinds.select.as_deref().unwrap_or(" ")), Cell::from("Select torrent")]), + Row::new(vec![ + Cell::from(keybinds.toggle_torrent.as_deref().unwrap_or("t")), + Cell::from("Toggle torrent"), + ]), + Row::new(vec![ + Cell::from(keybinds.toggle_all.as_deref().unwrap_or("a")), + Cell::from("Toggle all torrents"), + ]), + Row::new(vec![ + Cell::from(keybinds.delete.as_deref().unwrap_or("d")), + Cell::from("Delete torrent"), + ]), + Row::new(vec![ + Cell::from(keybinds.delete_force.as_deref().unwrap_or("D")), + Cell::from("Delete torrent and data"), + ]), + Row::new(vec![ + Cell::from(keybinds.select.as_deref().unwrap_or(" ")), + Cell::from("Select torrent"), + ]), ]; let table = Table::new( @@ -35,7 +74,12 @@ pub fn render_help(frame: &mut Frame, app: &App) { &[Constraint::Percentage(20), Constraint::Percentage(80)], ) .block(block) - .style(Style::default().fg(app.config.colors.get_color(&app.config.colors.info_foreground))); + .style( + Style::default().fg(app + .config + .colors + .get_color(&app.config.colors.info_foreground)), + ); let area = frame.area(); let height = 15; // Desired height for the help menu @@ -51,4 +95,3 @@ pub fn render_help(frame: &mut Frame, app: &App) { frame.render_widget(Clear, popup_area); frame.render_widget(table, popup_area); } - diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2b50822..ab9c3ad 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -32,8 +32,18 @@ pub fn render(app: &mut App, frame: &mut Frame) { .border_type(BorderType::Rounded), ) .select(app.index()) - .style(Style::default().fg(app.config.colors.get_color(&app.config.colors.info_foreground))) - .highlight_style(Style::default().fg(app.config.colors.get_color(&app.config.colors.warning_foreground))) + .style( + Style::default().fg(app + .config + .colors + .get_color(&app.config.colors.info_foreground)), + ) + .highlight_style( + Style::default().fg(app + .config + .colors + .get_color(&app.config.colors.warning_foreground)), + ) .divider("|"); frame.render_widget(tabs, chunks[0]); // renders tab diff --git a/src/ui/table.rs b/src/ui/table.rs index c278c46..abdf366 100644 --- a/src/ui/table.rs +++ b/src/ui/table.rs @@ -10,8 +10,14 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> { let selected = &app.torrents.selected.clone(); let torrents = &app.torrents.set_fields(None).torrents; - let highlight_bg = app.config.colors.get_color(&app.config.colors.highlight_background); - let highlight_fg = app.config.colors.get_color(&app.config.colors.highlight_foreground); + let highlight_bg = app + .config + .colors + .get_color(&app.config.colors.highlight_background); + let highlight_fg = app + .config + .colors + .get_color(&app.config.colors.highlight_foreground); let highlight_style = Style::default().bg(highlight_bg).fg(highlight_fg); let rows: Vec> = torrents @@ -38,7 +44,10 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> { .map(|&field| Constraint::Length(field.width())) .collect::>(); - let header_fg = app.config.colors.get_color(&app.config.colors.warning_foreground); + let header_fg = app + .config + .colors + .get_color(&app.config.colors.warning_foreground); let header = Row::new( fields .iter() @@ -47,8 +56,14 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> { ) .style(Style::default().fg(header_fg)); - let row_highlight_bg = app.config.colors.get_color(&app.config.colors.info_foreground); - let row_highlight_fg = app.config.colors.get_color(&app.config.colors.highlight_foreground); + let row_highlight_bg = app + .config + .colors + .get_color(&app.config.colors.info_foreground); + let row_highlight_fg = app + .config + .colors + .get_color(&app.config.colors.highlight_foreground); let row_highlight_style = Style::default().bg(row_highlight_bg).fg(row_highlight_fg); Table::new(rows, widths) diff --git a/tests/handler.rs b/tests/handler.rs index b637e2d..aff7bcb 100644 --- a/tests/handler.rs +++ b/tests/handler.rs @@ -1,5 +1,5 @@ use crossterm::event::{KeyCode, KeyEvent}; -use traxor::{app::action::Action, handler::get_action, app::App, config::Config}; +use traxor::{app::action::Action, app::App, config::Config, handler::get_action}; #[test] fn test_get_action_quit() { diff --git a/tests/merge.rs b/tests/merge.rs new file mode 100644 index 0000000..abd9e17 --- /dev/null +++ b/tests/merge.rs @@ -0,0 +1,286 @@ +use derive_macro::Merge; +use traxor::merge::Merge; + +#[derive(Debug, PartialEq, Merge)] +struct NestedStruct { + value: u32, + name: Option, +} + +#[derive(Debug, PartialEq, Merge)] +struct TestStruct { + a: u32, + b: Option, + c: Option, + nested: NestedStruct, + optional_nested: Option, +} + +#[test] +fn test_merge_basic_fields() { + let mut s1 = TestStruct { + a: 1, + b: Some("hello".to_string()), + c: None, + nested: NestedStruct { + value: 10, + name: Some("original".to_string()), + }, + optional_nested: None, + }; + let s2 = TestStruct { + a: 2, + b: Some("world".to_string()), + c: Some(true), + nested: NestedStruct { + value: 20, + name: Some("new".to_string()), + }, + optional_nested: Some(NestedStruct { + value: 30, + name: Some("optional".to_string()), + }), + }; + + s1.merge(s2); + + assert_eq!(s1.a, 2); + assert_eq!(s1.b, Some("world".to_string())); + assert_eq!(s1.c, Some(true)); + assert_eq!(s1.nested.value, 20); + assert_eq!(s1.nested.name, Some("new".to_string())); + assert_eq!( + s1.optional_nested, + Some(NestedStruct { + value: 30, + name: Some("optional".to_string()) + }) + ); +} + +#[test] +fn test_merge_option_none_other() { + let mut s1 = TestStruct { + a: 1, + b: Some("hello".to_string()), + c: Some(false), + nested: NestedStruct { + value: 10, + name: Some("original".to_string()), + }, + optional_nested: Some(NestedStruct { + value: 100, + name: Some("existing".to_string()), + }), + }; + let s2 = TestStruct { + a: 2, + b: None, + c: None, + nested: NestedStruct { + value: 20, + name: None, + }, + optional_nested: None, + }; + + s1.merge(s2); + + assert_eq!(s1.a, 2); + assert_eq!(s1.b, Some("hello".to_string())); // Should remain Some("hello") + assert_eq!(s1.c, Some(false)); // Should remain Some(false) + assert_eq!(s1.nested.value, 20); + assert_eq!(s1.nested.name, Some("original".to_string())); // Should remain original + assert_eq!( + s1.optional_nested, + Some(NestedStruct { + value: 100, + name: Some("existing".to_string()) + }) + ); // Should remain existing +} + +#[test] +fn test_merge_option_some_other() { + let mut s1 = TestStruct { + a: 1, + b: None, + c: None, + nested: NestedStruct { + value: 10, + name: None, + }, + optional_nested: None, + }; + let s2 = TestStruct { + a: 2, + b: Some("world".to_string()), + c: Some(true), + nested: NestedStruct { + value: 20, + name: Some("new".to_string()), + }, + optional_nested: Some(NestedStruct { + value: 30, + name: Some("optional".to_string()), + }), + }; + + s1.merge(s2); + + assert_eq!(s1.a, 2); + assert_eq!(s1.b, Some("world".to_string())); + assert_eq!(s1.c, Some(true)); + assert_eq!(s1.nested.value, 20); + assert_eq!(s1.nested.name, Some("new".to_string())); + assert_eq!( + s1.optional_nested, + Some(NestedStruct { + value: 30, + name: Some("optional".to_string()) + }) + ); +} + +#[test] +fn test_merge_nested_struct_with_none_name() { + let mut s1 = TestStruct { + a: 1, + b: None, + c: None, + nested: NestedStruct { + value: 10, + name: Some("original".to_string()), + }, + optional_nested: None, + }; + let s2 = TestStruct { + a: 2, + b: None, + c: None, + nested: NestedStruct { + value: 20, + name: None, + }, + optional_nested: None, + }; + + s1.merge(s2); + + assert_eq!(s1.nested.value, 20); + assert_eq!(s1.nested.name, Some("original".to_string())); // Should remain original +} + +#[test] +fn test_merge_optional_nested_struct_some_to_some() { + let mut s1 = TestStruct { + a: 1, + b: None, + c: None, + nested: NestedStruct { + value: 10, + name: None, + }, + optional_nested: Some(NestedStruct { + value: 100, + name: Some("existing".to_string()), + }), + }; + let s2 = TestStruct { + a: 2, + b: None, + c: None, + nested: NestedStruct { + value: 20, + name: None, + }, + optional_nested: Some(NestedStruct { + value: 200, + name: Some("new_optional".to_string()), + }), + }; + + s1.merge(s2); + + assert_eq!( + s1.optional_nested, + Some(NestedStruct { + value: 200, + name: Some("new_optional".to_string()) + }) + ); +} + +#[test] +fn test_merge_optional_nested_struct_none_to_some() { + let mut s1 = TestStruct { + a: 1, + b: None, + c: None, + nested: NestedStruct { + value: 10, + name: None, + }, + optional_nested: None, + }; + let s2 = TestStruct { + a: 2, + b: None, + c: None, + nested: NestedStruct { + value: 20, + name: None, + }, + optional_nested: Some(NestedStruct { + value: 200, + name: Some("new_optional".to_string()), + }), + }; + + s1.merge(s2); + + assert_eq!( + s1.optional_nested, + Some(NestedStruct { + value: 200, + name: Some("new_optional".to_string()) + }) + ); +} + +#[test] +fn test_merge_optional_nested_struct_some_to_none() { + let mut s1 = TestStruct { + a: 1, + b: None, + c: None, + nested: NestedStruct { + value: 10, + name: None, + }, + optional_nested: Some(NestedStruct { + value: 100, + name: Some("existing".to_string()), + }), + }; + let s2 = TestStruct { + a: 2, + b: None, + c: None, + nested: NestedStruct { + value: 20, + name: None, + }, + optional_nested: None, + }; + + s1.merge(s2); + + assert_eq!( + s1.optional_nested, + Some(NestedStruct { + value: 100, + name: Some("existing".to_string()) + }) + ); // Should remain existing +}