diff --git a/src/endpoint/log.rs b/src/endpoint/log.rs index a4e7b71..be309bb 100644 --- a/src/endpoint/log.rs +++ b/src/endpoint/log.rs @@ -20,8 +20,8 @@ impl Qbit { &self, last_known_id: impl Into> + Send + Sync, ) -> Result> { - #[derive(Serialize)] #[skip_serializing_none] + #[derive(Serialize)] struct Arg { last_known_id: Option, } diff --git a/src/endpoint/search.rs b/src/endpoint/search.rs index 983d0fc..2ffe2cf 100644 --- a/src/endpoint/search.rs +++ b/src/endpoint/search.rs @@ -91,8 +91,8 @@ impl Qbit { &self, id: impl Into> + Send + Sync, ) -> Result> { - #[derive(Serialize)] #[skip_serializing_none] + #[derive(Serialize)] struct Arg { id: Option, } @@ -129,8 +129,8 @@ impl Qbit { limit: impl Into> + Send + Sync, offset: impl Into> + Send + Sync, ) -> Result { - #[derive(Serialize)] #[skip_serializing_none] + #[derive(Serialize)] struct Arg { id: i64, limit: Option, diff --git a/src/endpoint/sync.rs b/src/endpoint/sync.rs index a2b5a93..6de6943 100644 --- a/src/endpoint/sync.rs +++ b/src/endpoint/sync.rs @@ -6,8 +6,8 @@ use crate::{Qbit, Result, ext::*, model::*}; impl Qbit { /// Return main-data changes since the supplied response ID. pub async fn sync(&self, rid: impl Into> + Send + Sync) -> Result { - #[derive(Serialize)] #[skip_serializing_none] + #[derive(Serialize)] struct Arg { rid: Option, } diff --git a/src/endpoint/torrent.rs b/src/endpoint/torrent.rs index 1000be5..eb225a9 100644 --- a/src/endpoint/torrent.rs +++ b/src/endpoint/torrent.rs @@ -197,8 +197,8 @@ impl Qbit { hashes: impl Into + Send + Sync, delete_files: impl Into> + Send + Sync, ) -> Result<()> { - #[derive(Serialize)] #[skip_serializing_none] + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct Arg { hashes: Hashes, @@ -840,8 +840,8 @@ impl Qbit { hashes: impl Into + Send + Sync, tags: Option> + Send>, ) -> Result<()> { - #[derive(Serialize)] #[skip_serializing_none] + #[derive(Serialize)] struct Arg { hashes: String, tags: Option, diff --git a/src/model/app.rs b/src/model/app.rs index 6cd2403..a76b672 100644 --- a/src/model/app.rs +++ b/src/model/app.rs @@ -28,6 +28,7 @@ pub struct ProcessInfo { pub launch_time: i64, } +#[skip_serializing_none] #[cfg_attr(feature = "builder", derive(typed_builder::TypedBuilder))] #[cfg_attr( feature = "builder", @@ -37,7 +38,6 @@ pub struct ProcessInfo { /// /// Optional fields allow callers to update only selected settings. #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq)] -#[skip_serializing_none] pub struct Preferences { /// Currently selected language (e.g. en_GB for English) pub locale: Option, @@ -465,3 +465,21 @@ pub struct SetCookieArg { /// Cookie expiration date (seconds since epoch) pub expiration_date: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preferences_skips_none_fields() { + let prefs = Preferences { + save_path: Some("/data/torrents".to_string()), + auto_tmm_enabled: Some(true), + ..Default::default() + }; + let json = serde_json::to_string(&prefs).unwrap(); + assert!(json.contains("save_path")); + assert!(json.contains("auto_tmm_enabled")); + assert!(!json.contains("null"), "None fields should be skipped, got: {json}"); + } +} diff --git a/src/model/log.rs b/src/model/log.rs index 39cff66..17dbc3a 100644 --- a/src/model/log.rs +++ b/src/model/log.rs @@ -62,8 +62,8 @@ pub enum LogLevel { feature = "builder", builder(field_defaults(default, setter(strip_option))) )] -#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)] #[skip_serializing_none] +#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)] pub struct GetLogsArg { /// Include normal messages (default: `true`) pub normal: Option, @@ -76,3 +76,24 @@ pub struct GetLogsArg { /// Exclude messages with "message id" <= `last_known_id` (default: `-1`) pub last_known_id: Option, } + +#[cfg(test)] +mod tests { + use super::GetLogsArg; + + #[test] + fn get_logs_arg_skips_none_fields() { + let arg = GetLogsArg { + normal: None, + info: None, + warning: None, + critical: None, + last_known_id: None, + }; + let json = serde_json::to_string(&arg).unwrap(); + assert!( + !json.contains("null"), + "None fields should be skipped, got: {json}" + ); + } +} diff --git a/src/model/torrent.rs b/src/model/torrent.rs index 93470bf..aa14e1d 100644 --- a/src/model/torrent.rs +++ b/src/model/torrent.rs @@ -405,8 +405,8 @@ impl Display for Hashes { feature = "builder", builder(field_defaults(default, setter(strip_option))) )] -#[derive(Debug, Clone, PartialEq, Default, serde::Serialize)] #[skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Default, serde::Serialize)] pub struct GetTorrentListArg { /// Filter torrent list by state. Allowed state filters: `all`, /// `downloading`, `seeding`, `completed`, `paused`, `active`, `inactive`, @@ -471,8 +471,8 @@ fn is_torrent_files(source: &TorrentSource) -> bool { feature = "builder", builder(field_defaults(default, setter(strip_option))) )] -#[derive(Debug, Clone, PartialEq, serde::Serialize, Default)] #[skip_serializing_none] +#[derive(Debug, Clone, PartialEq, serde::Serialize, Default)] pub struct AddTorrentArg { /// URLs or torrent files to add. #[serde(flatten)] @@ -638,6 +638,36 @@ mod torrent_source_tests { } } +#[cfg(test)] +mod skip_serializing_none_tests { + use super::{AddTorrentArg, GetTorrentListArg, TorrentSource}; + + #[test] + fn get_torrent_list_arg_skips_none_fields() { + let arg = GetTorrentListArg::default(); + let json = serde_json::to_string(&arg).unwrap(); + assert!( + !json.contains("null"), + "None fields should be skipped, got: {json}" + ); + } + + #[test] + fn add_torrent_arg_skips_none_fields() { + let arg = AddTorrentArg { + source: TorrentSource::Urls { + urls: super::Sep::from(vec![]), + }, + ..Default::default() + }; + let json = serde_json::to_string(&arg).unwrap(); + assert!( + !json.contains("null"), + "None fields should be skipped, got: {json}" + ); + } +} + /// Per-torrent share limits. #[cfg_attr(feature = "builder", derive(typed_builder::TypedBuilder))] #[derive(Debug, Clone, PartialEq, serde::Serialize)]