diff --git a/src/de/app/color.rs b/src/de/app/color.rs index 2f69ab3..d334b64 100644 --- a/src/de/app/color.rs +++ b/src/de/app/color.rs @@ -1,8 +1,8 @@ use clap::ColorChoice; -use serde::{de::DeserializeSeed, Deserialize}; +use serde::{de::DeserializeSeed, Deserialize, Serialize}; enum_de!(ColorChoice,ColorChoice1, - #[derive(Deserialize, Clone, Copy)] + #[derive(Deserialize, Serialize, Clone, Copy)] #[cfg_attr(feature = "kebab-case-key" ,serde(rename_all = "kebab-case"))] #[cfg_attr(feature = "snake-case-key" ,serde(rename_all = "snake_case"))] { diff --git a/src/de/app/mod.rs b/src/de/app/mod.rs index 0ce3ab2..2b53307 100644 --- a/src/de/app/mod.rs +++ b/src/de/app/mod.rs @@ -1,14 +1,60 @@ -use crate::CommandWrap; use appsettings::*; use clap::Command; use serde::{ de::{DeserializeSeed, Error, Visitor}, Deserialize, }; +use std::ops::Deref; mod appsettings; #[cfg(feature = "color")] -mod color; +pub(crate) mod color; + +/// Wrapper of [`Command`] to serialzie and deserialize. +/// ``` +/// const CLAP_TOML: &'static str = r#" +/// name = "app_clap_serde" +/// version = "1.0" +/// author = "tester" +/// about = "test-clap-serde" +/// "#; +/// let app: clap::Command = toml::from_str::(CLAP_TOML) +/// .expect("parse failed") +/// .into(); +/// assert_eq!(app.get_name(), "app_clap_serde"); +/// assert_eq!(app.get_about(), Some("test-clap-serde")); +/// ``` +#[derive(Debug, Clone)] +pub struct CommandWrap<'a> { + pub(crate) app: Command<'a>, +} + +impl<'a> CommandWrap<'a> { + /// Create a wrapper for [`Command`]. + pub fn new(app: Command<'a>) -> Self { + Self { app } + } +} + +impl<'a> From> for Command<'a> { + fn from(a: CommandWrap<'a>) -> Self { + a.app + } +} + +impl<'a> From> for CommandWrap<'a> { + fn from(app: Command<'a>) -> Self { + CommandWrap { app } + } +} + +impl<'a> Deref for CommandWrap<'a> { + type Target = Command<'a>; + + fn deref(&self) -> &Self::Target { + &self.app + } +} const TMP_APP_NAME: &str = "__tmp__deserialize__name__"; impl<'de> Deserialize<'de> for CommandWrap<'de> { @@ -110,6 +156,7 @@ impl<'a> Visitor<'a> for CommandVisitor<'a> { // subcommands : specialized (subcommand_help_heading, &str), (subcommand_negates_reqs, bool), + (subcommand_precedence_over_arg, bool), (subcommand_required, bool), (subcommand_value_name, &str), (propagate_version, bool), @@ -158,7 +205,7 @@ impl<'a> Visitor<'a> for CommandVisitor<'a> { ]); } - Ok(CommandWrap { app }) + Ok(app.into()) } } diff --git a/src/de/arg/arg_action.rs b/src/de/arg/arg_action.rs index 95d7ecc..517667e 100644 --- a/src/de/arg/arg_action.rs +++ b/src/de/arg/arg_action.rs @@ -1,8 +1,8 @@ use clap::ArgAction as AA; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; enum_de!(AA, ArgAction, - #[derive(Deserialize, Clone, Copy)] + #[derive(Serialize, Deserialize, Clone, Copy)] #[cfg_attr(feature = "kebab-case-key" ,serde(rename_all = "kebab-case"))] #[cfg_attr(feature = "snake-case-key" ,serde(rename_all = "snake_case"))] { diff --git a/src/de/arg/mod.rs b/src/de/arg/mod.rs index 4b1f162..f2aabf5 100644 --- a/src/de/arg/mod.rs +++ b/src/de/arg/mod.rs @@ -1,13 +1,44 @@ use self::{arg_action::ArgAction, value_hint::ValueHint, value_parser::ValueParser}; -use crate::ArgWrap; use clap::{Arg, Command}; use serde::de::{DeserializeSeed, Error, Visitor}; -use std::marker::PhantomData; +use std::{marker::PhantomData, ops::Deref}; -mod arg_action; -mod value_hint; +pub(crate) mod arg_action; +pub(crate) mod value_hint; mod value_parser; +/// Wrapper of [`Arg`] to deserialize with [`DeserializeSeed`](`serde::de::DeserializeSeed`). +#[derive(Debug, Clone)] +pub struct ArgWrap<'a> { + pub(crate) arg: Arg<'a>, +} + +impl<'a> ArgWrap<'a> { + pub fn new(arg: Arg<'a>) -> Self { + Self { arg } + } +} + +impl<'a> From> for Arg<'a> { + fn from(arg: ArgWrap<'a>) -> Self { + arg.arg + } +} + +impl<'a> From> for ArgWrap<'a> { + fn from(arg: Arg<'a>) -> Self { + ArgWrap { arg } + } +} + +impl<'a> Deref for ArgWrap<'a> { + type Target = Arg<'a>; + + fn deref(&self) -> &Self::Target { + &self.arg + } +} + #[cfg(feature = "override-arg")] struct ArgKVO<'a>(Option>); @@ -121,6 +152,7 @@ impl<'a> Visitor<'a> for ArgVisitor<'a> { (default_value, &str), // default_value_if : tuple3 ref (default_value_ifs, Vec<(&str, Option<&str>, Option<&str>)> ), + ref (default_values, Vec<&str> ), (display_order, usize), // env : specialized // env_os // not supported yet @@ -240,10 +272,11 @@ impl<'a> Visitor<'a> for ArgVisitor<'a> { "value_parser" => { arg.value_parser(map.next_value::()?) } + "positional" => {map.next_value::()?;/* noop */ arg} ] ); } - Ok(ArgWrap { arg }) + Ok(ArgWrap::new(arg)) } } diff --git a/src/de/arg/value_hint.rs b/src/de/arg/value_hint.rs index aa0f4bf..6310d95 100644 --- a/src/de/arg/value_hint.rs +++ b/src/de/arg/value_hint.rs @@ -1,8 +1,8 @@ use clap::ValueHint as VH; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; enum_de!(VH,ValueHint, - #[derive(Deserialize, Clone, Copy)] + #[derive(Serialize, Deserialize, Clone, Copy)] #[cfg_attr(feature = "kebab-case-key" ,serde(rename_all = "kebab-case"))] #[cfg_attr(feature = "snake-case-key" ,serde(rename_all = "snake_case"))] { diff --git a/src/de/group.rs b/src/de/group.rs index 6bb1f12..f960317 100644 --- a/src/de/group.rs +++ b/src/de/group.rs @@ -1,7 +1,22 @@ -use crate::ArgGroupWrap; use clap::{ArgGroup, Command}; use serde::de::{DeserializeSeed, Error, Visitor}; +pub(crate) struct ArgGroupWrap<'a> { + group: ArgGroup<'a>, +} + +impl<'a> From> for ArgGroup<'a> { + fn from(group: ArgGroupWrap<'a>) -> Self { + group.group + } +} + +impl<'a> From> for ArgGroupWrap<'a> { + fn from(group: ArgGroup<'a>) -> Self { + ArgGroupWrap { group } + } +} + struct GroupVisitor<'a>(&'a str); impl<'de> Visitor<'de> for GroupVisitor<'de> { diff --git a/src/de/macros.rs b/src/de/macros.rs index e430415..0593190 100644 --- a/src/de/macros.rs +++ b/src/de/macros.rs @@ -125,5 +125,18 @@ macro_rules! enum_de { } } } + + impl $newty { + #[allow(dead_code)] + pub fn from_clap_type(c : $basety) -> Self { + match c { + $( + $(#[$cfg_meta])* + $basety::$var => $newty::$var, + )* + _ => unimplemented!(), + } + } + } }; } diff --git a/src/de/mod.rs b/src/de/mod.rs index 051a3ee..481006e 100644 --- a/src/de/mod.rs +++ b/src/de/mod.rs @@ -1,6 +1,6 @@ #[macro_use] mod macros; -mod app; -mod arg; -mod group; +pub(crate) mod app; +pub(crate) mod arg; +pub(crate) mod group; diff --git a/src/lib.rs b/src/lib.rs index f496849..955099a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,6 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] -use clap::{Arg, ArgGroup, Command}; -use serde::Deserializer; -use std::ops::Deref; - #[cfg(not(any( feature = "kebab-case-key", feature = "snake-case-key", @@ -19,125 +15,29 @@ compile_error!("Case setting feature is missing. Either one should be set."); ))] compile_error!("Case setting feature is conflicting. Only one should be set."); -#[macro_use] -mod de; +pub(crate) mod de; +mod ser; + #[cfg(feature = "docsrs")] pub mod documents; + #[cfg(feature = "yaml")] #[deprecated(since = "0.4", note = "use serde-yaml instead")] mod yaml; - -#[cfg(all(test, feature = "snake-case-key"))] -mod tests; - #[cfg(feature = "yaml")] pub use yaml::{yaml_to_app, YamlWrap}; -/** -Deserialize [`Command`] from [`Deserializer`]. -``` -const CLAP_TOML: &'static str = r#" -name = "app_clap_serde" -version = "1.0" -author = "tester" -about = "test-clap-serde" -"#; -let app = clap_serde::load(&mut toml::Deserializer::new(CLAP_TOML)) - .expect("parse failed"); -assert_eq!(app.get_name(), "app_clap_serde"); -assert_eq!(app.get_about(), Some("test-clap-serde")); -``` -*/ -pub fn load<'de, D>(de: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - use serde::Deserialize; - CommandWrap::deserialize(de).map(|a| a.into()) -} - -/** -Wrapper of [`Command`] to deserialize. -``` -const CLAP_TOML: &'static str = r#" -name = "app_clap_serde" -version = "1.0" -author = "tester" -about = "test-clap-serde" -"#; -let app: clap::Command = toml::from_str::(CLAP_TOML) - .expect("parse failed") - .into(); -assert_eq!(app.get_name(), "app_clap_serde"); -assert_eq!(app.get_about(), Some("test-clap-serde")); -``` -*/ -#[derive(Debug, Clone)] -pub struct CommandWrap<'a> { - app: Command<'a>, -} +pub use de::app::CommandWrap; +pub use de::arg::ArgWrap; +pub use ser::app::CommandWrapRef; +pub use ser::arg::ArgWrapRef; +pub use ser::NoSkip; -#[deprecated] +#[deprecated(note = "use CommandWrap instead")] pub type AppWrap<'a> = CommandWrap<'a>; -impl<'a> From> for Command<'a> { - fn from(a: CommandWrap<'a>) -> Self { - a.app - } -} - -impl<'a> From> for CommandWrap<'a> { - fn from(app: Command<'a>) -> Self { - CommandWrap { app } - } -} - -impl<'a> Deref for CommandWrap<'a> { - type Target = Command<'a>; - - fn deref(&self) -> &Self::Target { - &self.app - } -} - -/// Wrapper of [`Arg`] to deserialize with [`DeserializeSeed`](`serde::de::DeserializeSeed`). -#[derive(Debug, Clone)] -pub struct ArgWrap<'a> { - arg: Arg<'a>, -} - -impl<'a> From> for Arg<'a> { - fn from(arg: ArgWrap<'a>) -> Self { - arg.arg - } -} - -impl<'a> From> for ArgWrap<'a> { - fn from(arg: Arg<'a>) -> Self { - ArgWrap { arg } - } -} - -impl<'a> Deref for ArgWrap<'a> { - type Target = Arg<'a>; - - fn deref(&self) -> &Self::Target { - &self.arg - } -} - -pub(crate) struct ArgGroupWrap<'a> { - group: ArgGroup<'a>, -} - -impl<'a> From> for ArgGroup<'a> { - fn from(group: ArgGroupWrap<'a>) -> Self { - group.group - } -} +#[cfg(all(test, feature = "snake-case-key"))] +mod tests; -impl<'a> From> for ArgGroupWrap<'a> { - fn from(group: ArgGroup<'a>) -> Self { - ArgGroupWrap { group } - } -} +mod util; +pub use util::load; diff --git a/src/ser/app.rs b/src/ser/app.rs new file mode 100644 index 0000000..fc8d853 --- /dev/null +++ b/src/ser/app.rs @@ -0,0 +1,175 @@ +use super::IterSer; +use super::SerializeConfig; +use crate::CommandWrap; +use clap::Command; +use serde::ser::SerializeMap; +use serde::Serialize; +use std::ops::Deref; + +/// Wrapper of `&`[`Command`] to serialize. +#[derive(Debug, Clone)] +pub struct CommandWrapRef<'command, 'wrap, C = ()> { + command: &'wrap Command<'command>, + config: C, +} + +impl<'a, 'b> CommandWrapRef<'a, 'b> { + /// Create a wrapper for [`Command`]. + pub fn new(app: &'b Command<'a>) -> Self { + Self { + command: app, + config: (), + } + } + + /// Add a setting for serializeing. + /// See [`NoSkip`](`crate::NoSkip`) for details. + pub fn with_setting(self, config: C) -> CommandWrapRef<'a, 'b, C> { + CommandWrapRef { + command: self.command, + config, + } + } +} + +impl<'a, 'b, C> Deref for CommandWrapRef<'a, 'b, C> { + type Target = Command<'a>; + + fn deref(&self) -> &Self::Target { + self.command + } +} + +impl<'a, 'b, C> From> for &'b Command<'a> { + fn from(a: CommandWrapRef<'a, 'b, C>) -> Self { + a.command + } +} + +impl<'a, 'b, C: Default> From<&'b Command<'a>> for CommandWrapRef<'a, 'b, C> { + fn from(command: &'b Command<'a>) -> Self { + CommandWrapRef { + command, + config: C::default(), + } + } +} + +impl<'a, 'b, C: Default> From<&'b CommandWrap<'a>> for CommandWrapRef<'a, 'b, C> { + fn from(app: &'b CommandWrap<'a>) -> Self { + CommandWrapRef { + command: &app.app, + config: C::default(), + } + } +} + +impl<'a, 'b> Serialize for &'b CommandWrap<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let wrap_ref = CommandWrapRef::<()>::from(*self); + wrap_ref.serialize(serializer) + } +} + +impl<'a, 'b, C: SerializeConfig> Serialize for CommandWrapRef<'a, 'b, C> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let command = self.command; + let config = &self.config; + let r = ser_value!(command, serializer, config, [ + (name, get_name), + #[cfg(feature = "color")] + [&] {crate::de::app::color::from_clap_type} (color, get_color), + is [ + (no_binary_name, is_no_binary_name_set), + (dont_delimit_trailing_values,is_dont_delimit_trailing_values_set), + (disable_version_flag,is_disable_version_flag_set), + (propagate_version,is_propagate_version_set), + (next_line_help,is_next_line_help_set), + (disable_help_flag,is_disable_help_flag_set), + (disable_help_subcommand,is_disable_help_subcommand_set), + (disable_colored_help,is_disable_colored_help_set), + (dont_collapse_args_in_usage,is_dont_collapse_args_in_usage_set), + (arg_required_else_help,is_arg_required_else_help_set), + (allow_negative_numbers,is_allow_negative_numbers_set), + (trailing_var_arg,is_trailing_var_arg_set), + (allow_missing_positional,is_allow_missing_positional_set), + (hide,is_hide_set), + (subcommand_required,is_subcommand_required_set), + (allow_external_subcommands,is_allow_external_subcommands_set), + (allow_invalid_utf8_for_external_subcommands,is_allow_invalid_utf8_for_external_subcommands_set), + (args_conflicts_with_subcommands,is_args_conflicts_with_subcommands_set), + (subcommand_precedence_over_arg,is_subcommand_precedence_over_arg_set), + (subcommand_negates_reqs,is_subcommand_negates_reqs_set), + (multicall,is_multicall_set) + ], + opt [ + (display_name, get_display_name), + (bin_name, get_bin_name), + (version, get_version), + (long_version, get_long_version), + (author, get_author), + (long_flag, get_long_flag), + [&] (short_flag, get_short_flag), + (about, get_about), + (long_about, get_long_about), + (next_help_heading, get_next_help_heading), + (subcommand_help_heading, get_subcommand_help_heading), + (subcommand_value_name, get_subcommand_value_name), + (before_help, get_before_help), + (before_long_help, get_before_long_help), + (after_help, get_after_help), + (after_long_help, get_after_long_help), + ] + iter [ + (visible_aliases, Command::get_visible_aliases), + (visible_short_flag_aliases, Command::get_visible_short_flag_aliases), + (visible_long_flag_aliases, Command::get_visible_long_flag_aliases) + //missing get_hidden_aliases in Command + ] + //groups + specialize [ + (args, |s| {super::arg::ArgsWrap::new(s, &self.config)}), + (subcommands, |s| {SubCommandsWrap(s, self.config.clone())}) + ] + ]); + r + } +} + +struct SubCommandsWrap<'a, 'b, C>(&'b Command<'a>, C); + +impl<'a, 'b, C: SerializeConfig> Serialize for SubCommandsWrap<'a, 'b, C> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_seq( + self.0 + .get_subcommands() + .map(|s| SubcommandWrap(s, self.1.clone())), + ) + } +} + +struct SubcommandWrap<'a, 'b, C>(&'b Command<'a>, C); + +impl<'a, 'b, C: SerializeConfig> Serialize for SubcommandWrap<'a, 'b, C> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_map(Some(( + self.0.get_name(), + CommandWrapRef { + command: self.0, + config: self.1.clone(), + }, + ))) + } +} diff --git a/src/ser/arg.rs b/src/ser/arg.rs new file mode 100644 index 0000000..1819fb5 --- /dev/null +++ b/src/ser/arg.rs @@ -0,0 +1,154 @@ +use super::SerializeConfig; +use crate::ArgWrap; +use clap::{Arg, Command}; +use serde::ser::SerializeMap; +use serde::{Serialize, Serializer}; + +/// Wrapper of `&`[`Arg`] to serialize. +#[derive(Debug, Clone)] +pub struct ArgWrapRef<'a, 'b, C = ()> { + arg: &'b Arg<'a>, + pub(crate) config: C, +} + +impl<'a, 'b> ArgWrapRef<'a, 'b> { + pub fn new(arg: &'b Arg<'a>) -> Self { + Self { arg, config: () } + } + pub fn with_config(self, config: C) -> ArgWrapRef<'a, 'b, C> { + ArgWrapRef { + arg: self.arg, + config, + } + } +} + +impl<'a, 'b> From<&'b Arg<'a>> for ArgWrapRef<'a, 'b> { + fn from(arg: &'b Arg<'a>) -> Self { + Self { arg, config: () } + } +} + +impl<'a, 'b> From<&'b ArgWrap<'a>> for ArgWrapRef<'a, 'b> { + fn from(arg: &'b ArgWrap<'a>) -> Self { + Self { + arg: &arg.arg, + config: (), + } + } +} + +impl<'se> Serialize for ArgWrap<'se> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + ArgWrapRef::from(self).serialize(serializer) + } +} + +impl<'se, 'b, C: SerializeConfig> Serialize for ArgWrapRef<'se, 'b, C> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_map(Some((self.arg.get_id(), ArgWrapMaps::new(self)))) + } +} + +pub struct ArgWrapMaps<'se, 'wrap, S> { + wrap: &'wrap ArgWrapRef<'se, 'wrap, S>, +} + +impl<'se, 'wrap, S> ArgWrapMaps<'se, 'wrap, S> { + pub fn new(wrap: &'wrap ArgWrapRef<'se, 'wrap, S>) -> Self { + Self { wrap } + } +} + +impl<'se, 'wrap, C: SerializeConfig> Serialize for ArgWrapMaps<'se, 'wrap, C> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let arg = &self.wrap.arg; + let config = &self.wrap.config; + let r = ser_value!(arg, serializer, config, [ + // (id, get_id), not seriazed here. + is [ + (positional, is_positional), + (required,is_required_set), + (multiple_values,is_multiple_values_set), + (multiple_occurrences,is_multiple_occurrences_set), + (takes_value,is_takes_value_set), + (allow_hyphen_values,is_allow_hyphen_values_set), + (global,is_global_set), + (next_line_help,is_next_line_help_set), + (hide,is_hide_set), + (hide_default_value,is_hide_default_value_set), + (hide_possible_values,is_hide_possible_values_set), + #[cfg(feature = "env")] + (hide_env,is_hide_env_set), + #[cfg(feature = "env")] + (hide_env_values,is_hide_env_values_set), + (hide_short_help,is_hide_short_help_set), + (hide_long_help,is_hide_long_help_set), + (use_value_delimiter,is_use_value_delimiter_set), + (require_value_delimiter,is_require_value_delimiter_set), + (require_equals,is_require_equals_set), + (exclusive,is_exclusive_set), + (last,is_last_set), + (ignore_case,is_ignore_case_set), + ], + opt [ + (help, get_help), + (long_help, get_long_help), + (help_heading, get_help_heading), + [&] (short,get_short), + [&] (visible_short_aliases,get_visible_short_aliases), + // no hidden aliases because clap dosen't expose + (long,get_long), + (value_names,get_value_names), + [&] (number_of_values, get_num_vals), + [&] (value_delimiter, get_value_delimiter), + [&] (index,get_index), + #[cfg(feature = "env")] + (env, get_env), + [&] (visible_aliases, get_visible_aliases), + [&] (visible_short_aliases, get_visible_short_aliases), + ] + specialize [ + (default_values, Arg::get_default_values, |x : &[&std::ffi::OsStr]| { x.len() > 0 } ), + (value_hint, |s| { crate::de::arg::value_hint::ValueHint::from_clap_type(Arg::get_value_hint(s)) }), + (arg_action, |s| { crate::de::arg::arg_action::ArgAction::from_clap_type(Arg::get_action(s).clone()) }) + ] + ]); + + r + } +} + +pub(crate) struct ArgsWrap<'a, 'b, C> { + command: &'b Command<'a>, + config: C, +} + +impl<'a, 'b, C> ArgsWrap<'a, 'b, C> { + pub fn new(command: &'b Command<'a>, config: C) -> Self { + Self { command, config } + } +} + +impl<'a, 'b, C: SerializeConfig> Serialize for ArgsWrap<'a, 'b, C> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let args = self.command.get_arguments(); + let setting = &self.config; + serializer.collect_seq(args.map(|arg| ArgWrapRef { + arg, + config: setting.clone(), + })) + } +} diff --git a/src/ser/macros.rs b/src/ser/macros.rs new file mode 100644 index 0000000..f0dd8fd --- /dev/null +++ b/src/ser/macros.rs @@ -0,0 +1,97 @@ +use serde::Serialize; +use std::cell::RefCell; + +macro_rules! ser_value { + ( $command: ident, $ser: ident, $config: ident, [ + $( $(#[$m:meta])? $([$r:tt])? $({$ex:expr})? ($field: ident, $getter: ident),)* + is [ $($(#[$m_ref:meta])? ($field_ref: ident, $getter_ref: ident)),+ $(,)? ], + opt [ $( $(#[$m_opt:meta])? $([$r_opt:tt]$({$ex_r:expr})?)?($field_opt: ident, $getter_opt: ident)),+ $(,)? ] + $(iter [ $(($field_iter: ident, $expr_iter:expr )),+ ])? + $( specialize [ $(($field_sp: ident, $expr_sp: expr $(, $expr_sp_check: expr)?)),* ])? + ]) => {{ + let mut map = $ser.serialize_map(None)?; + let serialize_all = $config.serialize_all(); + $( + $(#[$m])* + { + map.serialize_entry(stringify!($field), $($r)* $($ex)*($command.$getter()))?; + })* + $( + $(#[$m_ref])* + { + let flag = $command.$getter_ref(); + if serialize_all || flag { + map.serialize_entry(stringify!($field_ref), &flag)?; + } + })* + $( + $(#[$m_opt])* + { + if serialize_all + { + map.serialize_entry( stringify!($field_opt), &($command.$getter_opt())$(.map(|v|$($ex_r)*(v)))*.unwrap_or_default())?; + } + else { + if let Some(value) = $command.$getter_opt() + { + map.serialize_entry( stringify!($field_opt), $($r_opt)* $($($ex_r)*)*(value))?; + } + } + } + )* + $($( + { + let iter = ($expr_iter)(&$command); + if serialize_all { + map.serialize_entry(stringify!($field_iter), &IterSer::new(iter))?; + } else { + let mut peekable = iter.peekable(); + if peekable.peek().is_some() { + map.serialize_entry(stringify!($field_iter), &IterSer::new(peekable))?; + } + } + } + )*)* + $($( + let value = $expr_sp($command); + if serialize_all || { + #[allow(unused_mut, unused_assignments)] + let mut check = true; + $( check = $expr_sp_check(&value); )* + check + } { + map.serialize_entry(stringify!($field_sp), &value)?; + } + )*)* + + map.end() } + }; +} + +pub struct IterSer(RefCell>); + +impl IterSer { + pub fn new(iter: I) -> Self { + Self(RefCell::new(Some(iter))) + } +} + +impl Serialize for IterSer +where + I: IntoIterator, + ::Item: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::Error; + // should be unreachable unchecked? + let iter = self + .0 + .borrow_mut() + .take() + .ok_or_else(|| S::Error::custom("logic error in IterSer"))?; + serializer.collect_seq(iter) + } +} diff --git a/src/ser/mod.rs b/src/ser/mod.rs new file mode 100644 index 0000000..262b54a --- /dev/null +++ b/src/ser/mod.rs @@ -0,0 +1,45 @@ +#[macro_use] +mod macros; +pub(crate) use macros::IterSer; + +pub(crate) mod app; +pub(crate) mod arg; + +/// Config to serialzie [`Command`](`clap::Command`) and [`Arg`](`clap::Arg`). +pub trait SerializeConfig: Clone { + /// Serialize all the fields in [`Command`](`clap::Command`) and [`Arg`](`clap::Arg`). + /// If this returns false, the flags (getter begin with `is_`) with `false` + /// and values (getter begin with `get_`) with `None` will be skipped. + fn serialize_all(&self) -> bool; +} + +impl SerializeConfig for () { + #[inline] + fn serialize_all(&self) -> bool { + false + } +} + +impl SerializeConfig for &S { + fn serialize_all(&self) -> bool { + (*self).serialize_all() + } +} + +/// Serialize all the fields in [`Command`](`clap::Command`) and [`Arg`](`clap::Arg`). +/// If not set, the flags (getter begin with `is_`) with `false` +/// and values (getter begin with `get_`) with `None` will be skipped. +/// ``` +/// # use clap::Command; +/// # use clap_serde::*; +/// # let command = Command::default(); +/// let wrap = CommandWrapRef::new(&command).with_setting(NoSkip); +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct NoSkip; +impl SerializeConfig for NoSkip { + #[inline] + fn serialize_all(&self) -> bool { + true + } +} diff --git a/src/tests.rs b/src/tests.rs index 045b53c..dd28ecd 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,4 +1,4 @@ -use crate::CommandWrap; +use crate::{ser::app::CommandWrapRef, CommandWrap, NoSkip}; use clap::{builder::ValueParser, Command}; #[test] @@ -307,3 +307,21 @@ apple = { short = } let mut de = toml::Deserializer::new(CLAP_TOML); assert!(wrap.deserialize(&mut de).is_err()); } + +#[test] +fn serialize() { + let c = Command::new("ser_test").about("testing _ser"); + let command: CommandWrapRef = (&c).into(); + let json = serde_json::to_string(&command).unwrap(); + let command2: CommandWrap = serde_json::from_str(&json).unwrap(); + assert_eq!(command.get_name(), command2.get_name()); +} + +#[test] +fn serialize_all() { + let c = Command::new("ser_test").about("testing _ser"); + let command: CommandWrapRef = (&c).into(); + let json = serde_json::to_string(&command).unwrap(); + let command2: CommandWrap = serde_json::from_str(&json).unwrap(); + assert_eq!(command.get_name(), command2.get_name()); +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..03a906d --- /dev/null +++ b/src/util.rs @@ -0,0 +1,27 @@ +use clap::Command; +use serde::Deserializer; + +use crate::CommandWrap; + +/** +Deserialize [`Command`] from [`Deserializer`]. +``` +const CLAP_TOML: &'static str = r#" +name = "app_clap_serde" +version = "1.0" +author = "tester" +about = "test-clap-serde" +"#; +let app = clap_serde::load(&mut toml::Deserializer::new(CLAP_TOML)) + .expect("parse failed"); +assert_eq!(app.get_name(), "app_clap_serde"); +assert_eq!(app.get_about(), Some("test-clap-serde")); +``` +*/ +pub fn load<'de, D>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::Deserialize; + CommandWrap::deserialize(de).map(|a| a.into()) +}