From b3182fd2c962a537cc0584105cd0b4745a7e261d Mon Sep 17 00:00:00 2001 From: aobatact Date: Sun, 23 Jan 2022 01:05:23 +0900 Subject: [PATCH] version 0.2.0 : yaml support (#15) * Add yaml support! * version 0.2.0 * tiny doc fix --- .gitignore | 1 + Cargo.toml | 7 +- README.md | 30 +++- src/lib.rs | 7 +- src/tests.rs | 74 +++++++--- src/yaml.rs | 376 +++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 474 insertions(+), 21 deletions(-) create mode 100644 src/yaml.rs diff --git a/.gitignore b/.gitignore index 96ef6c0..97e6d83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +.vscode/ diff --git a/Cargo.toml b/Cargo.toml index b8793ee..3b8d851 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clap-serde" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" description = "Provides a wrapper to deserialize clap app using serde." @@ -9,16 +9,17 @@ repository = "https://github.com/aobatact/clap-serde" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = [ "kebab-case-setting" ] +default = [ "kebab-case-setting", "yaml" ] env = [ "clap/env" ] kebab-case-setting = [] snake-case-setting = [] +yaml = [ "yaml-rust"] [dependencies] clap = { version = "3.0", default-features = false, features = ["std"]} serde = { version = "1", features = ["derive"]} +yaml-rust = { version = "0.4.5", default-features = false, optional = true } [dev-dependencies] -serde_yaml = { version = "0.8.23" } serde_json = { version = "1.0.75" } toml = { version = "0.5.8" } diff --git a/README.md b/README.md index 273a45f..2bb0c84 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,39 @@ assert_eq!(app.get_about(), Some("test-clap-serde")); ``` ## yaml -Not working now because [serde_yaml](https://crates.io/crates/serde_yaml) only accepts `DeserializeOwned`. +``` +const YAML_STR: &'static str = r#" +name: app_clap_serde +version : "1.0" +about : yaml_support! +author : yaml_supporter + +args: + - apple : + - short: a + - banana: + - short: b + - long: banana + - aliases : + - musa_spp + +subcommands: + - sub1: + - about : subcommand_1 + - sub2: + - about : subcommand_2 + +"#; +let yaml = yaml_rust::Yaml::Array(yaml_rust::YamlLoader::load_from_str(YAML_STR).expect("not a yaml")); +let app = clap_serde::yaml_to_app(&yaml).expect("parse failed from yaml"); +assert_eq!(app.get_name(), "app_clap_serde"); +``` # features - env Enables env feature in clap. +- yaml +Enables to use yaml. ## (settings name letter) Settings names format for [`AppSettings`](`clap::AppSettings`) and [`ArgSettings`](`clap::ArgSettings`). diff --git a/src/lib.rs b/src/lib.rs index 5cf1859..e4b1aa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,23 @@ #![doc = include_str!("../README.md")] -use std::ops::Deref; use clap::{App, Arg, ArgGroup}; use serde::Deserializer; +use std::ops::Deref; #[cfg(all(feature = "kebab-case-setting", feature = "snake-case-setting"))] compile_error!("Feature \"kebab-case-setting\" and \"snake-case-setting\" collides. At most one should be set."); #[macro_use] mod de; +#[cfg(feature = "yaml")] +mod yaml; #[cfg(test)] mod tests; +#[cfg(feature = "yaml")] +pub use yaml::{YamlWrap, yaml_to_app}; + /** Deserialize [`App`] from [`Deserializer`]. ``` diff --git a/src/tests.rs b/src/tests.rs index 0d1b6e0..3abd466 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,22 +1,64 @@ use crate::AppWrap; use clap::App; -//currently fails... beacuse serde_yaml only supports `DeserializeOwned` and no zero copy deserialization -// #[test] -// fn name_yaml() { -// const NAME_YAML: &'static str = "name : app_clap_serde"; -// let app: App = serde_yaml::from_str::(NAME_YAML).expect("parse failed").into(); -// assert_eq!(app.get_name(), "app_clap_serde"); -// } - -// #[test] -// fn name_yaml_2(){ -// use serde::de::Deserialize; -// const NAME_YAML: &'static str = "name: app_clap_serde"; -// let de = serde_yaml::Deserializer::from_str(NAME_YAML); -// let app: App = AppWrap::deserialize(de).expect("parse failed").into(); -// assert_eq!(app.get_name(), "app_clap_serde"); -// } +#[cfg(feature = "yaml")] +#[test] +fn name_yaml() { + use yaml_rust::Yaml; + + const NAME_YAML: &'static str = "name: app_clap_serde\n"; + let yaml = + Yaml::Array(yaml_rust::YamlLoader::load_from_str(NAME_YAML).expect("fail to make yaml")); + let app = crate::load(crate::yaml::YamlWrap::new(&yaml)).expect("parse failed"); + assert_eq!(app.get_name(), "app_clap_serde"); +} + +#[cfg(feature = "yaml")] +#[test] +fn test_yaml() { + use yaml_rust::Yaml; + + const NAME_YAML: &'static str = r#" +name: app_clap_serde +version : "1.0" +about : yaml_support! +author : yaml_supporter + +args: + - apple : + - short: a + - banana: + - short: b + - long: banana + - aliases : + - musa_spp + +subcommands: + - sub1: + - about : subcommand_1 + - sub2: + - about : subcommand_2 + +"#; + let yaml = + Yaml::Array(yaml_rust::YamlLoader::load_from_str(NAME_YAML).expect("fail to make yaml")); + let app = crate::load(crate::yaml::YamlWrap::new(&yaml)).expect("parse failed"); + assert_eq!(app.get_name(), "app_clap_serde"); + let subs = app.get_subcommands().collect::>(); + assert!(subs + .iter() + .any(|x| x.get_name() == "sub1" && x.get_about() == Some("subcommand_1"))); + assert!(subs + .iter() + .any(|x| x.get_name() == "sub2" && x.get_about() == Some("subcommand_2"))); + let args = app.get_arguments().collect::>(); + assert!(args + .iter() + .any(|x| x.get_name() == "apple" && x.get_short() == Some('a'))); + assert!(args.iter().any(|x| x.get_name() == "banana" + && x.get_short() == Some('b') + && x.get_long() == Some("banana"))); +} #[test] fn name_json() { diff --git a/src/yaml.rs b/src/yaml.rs new file mode 100644 index 0000000..bda4e8a --- /dev/null +++ b/src/yaml.rs @@ -0,0 +1,376 @@ +use serde::{ + de::{ + value::{MapDeserializer, SeqDeserializer}, + Error as _, IntoDeserializer, Unexpected, + }, + Deserializer, +}; +use yaml_rust::{Yaml}; + +/** +Deserializing from [`Yaml`] +``` +const YAML_STR: &'static str = r#" +name: app_clap_serde +version : "1.0" +about : yaml_support! +author : yaml_supporter + +args: + - apple : + - short: a + - banana: + - short: b + - long: banana + - aliases : + - musa_spp + +subcommands: + - sub1: + - about : subcommand_1 + - sub2: + - about : subcommand_2 + +"#; +let yaml = yaml_rust::Yaml::Array(yaml_rust::YamlLoader::load_from_str(YAML_STR).expect("not a yaml")); +let app = clap_serde::yaml_to_app(&yaml).expect("parse failed from yaml"); +assert_eq!(app.get_name(), "app_clap_serde"); +``` +*/ +pub fn yaml_to_app<'a>(yaml: &'a Yaml) -> Result, Error> { + let wrap = YamlWrap { yaml }; + use serde::Deserialize; + crate::AppWrap::deserialize(wrap).map(|x| x.into()) +} + +/// Wrapper to use [`Yaml`] as [`Deserializer`]. +/// +/// Currently this implement functions in [`Deserializer`] that is only needed in deserializing into `App`. +/// Recommend to use [`yaml_to_app`] instead. +pub struct YamlWrap<'a> { + yaml: &'a yaml_rust::Yaml, +} + +impl<'a> YamlWrap<'a> { + pub fn new(yaml: &'a yaml_rust::Yaml) -> Self { + Self { yaml } + } +} + +#[derive(Debug, Clone)] +pub enum Error { + Custom(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} +impl std::error::Error for Error {} + +impl serde::de::Error for Error { + fn custom(msg: T) -> Self + where + T: std::fmt::Display, + { + Self::Custom(msg.to_string()) + } +} + +macro_rules! de_num { + ($sig : ident, $sig_v : ident) => { + fn $sig(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.$sig_v(match self.yaml.as_i64().map(|i| i.try_into()) { + Some(Ok(i)) => i, + _ => return Err(as_invalid(self.yaml, "Intger")), + }) + } + }; +} + +fn as_invalid(y: &Yaml, expected: &str) -> Error { + Error::invalid_type( + match y { + Yaml::Real(r) => r + .parse() + .map(|r| Unexpected::Float(r)) + .unwrap_or(Unexpected::Other(r)), + Yaml::Integer(i) => Unexpected::Signed(*i), + Yaml::String(s) => Unexpected::Str(s), + Yaml::Boolean(b) => Unexpected::Bool(*b), + Yaml::Array(_) => Unexpected::Seq, + Yaml::Hash(_) => Unexpected::Map, + Yaml::Alias(_) => todo!(), + Yaml::Null => Unexpected::Unit, + Yaml::BadValue => Unexpected::Other("BadValue"), + }, + &expected, + ) +} + +impl<'de> IntoDeserializer<'de, Error> for YamlWrap<'de> { + type Deserializer = Self; + + fn into_deserializer(self) -> Self::Deserializer { + self + } +} + +impl<'de> Deserializer<'de> for YamlWrap<'de> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match self.yaml { + yaml_rust::Yaml::Real(s) => { + visitor.visit_f64(s.parse::().map_err(|e| Error::Custom(e.to_string()))?) + } + yaml_rust::Yaml::Integer(i) => visitor.visit_i64(*i), + yaml_rust::Yaml::String(s) => visitor.visit_str(s), + yaml_rust::Yaml::Boolean(b) => visitor.visit_bool(*b), + yaml_rust::Yaml::Array(_) => self.deserialize_seq(visitor), //visitor.visit_seq(a), + yaml_rust::Yaml::Hash(_) => self.deserialize_map(visitor), + yaml_rust::Yaml::Alias(_) => todo!(), + yaml_rust::Yaml::Null => visitor.visit_none(), + yaml_rust::Yaml::BadValue => return Err(as_invalid(self.yaml, "any")), + } + } + + fn deserialize_bool(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_bool(self.yaml.as_bool().ok_or(as_invalid(self.yaml, "bool"))?) + } + + de_num!(deserialize_i8, visit_i8); + de_num!(deserialize_i16, visit_i16); + de_num!(deserialize_i32, visit_i32); + de_num!(deserialize_i64, visit_i64); + de_num!(deserialize_u8, visit_i8); + de_num!(deserialize_u16, visit_u16); + de_num!(deserialize_u32, visit_u32); + de_num!(deserialize_u64, visit_u64); + + fn deserialize_f32(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_f32(self.yaml.as_f64().ok_or(as_invalid(self.yaml, "f32"))? as f32) + } + + fn deserialize_f64(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_f64(self.yaml.as_f64().ok_or(as_invalid(self.yaml, "f64"))?) + } + + fn deserialize_char(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_char( + self.yaml + .as_str() + .ok_or(as_invalid(self.yaml, "char"))? + .chars() + .next() + .ok_or(as_invalid(self.yaml, "char"))?, + ) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + let s = self.yaml.as_str(); + visitor.visit_borrowed_str(s.ok_or_else(|| as_invalid(self.yaml, "str"))?) + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_string( + self.yaml + .as_str() + .ok_or(as_invalid(self.yaml, "string"))? + .to_string(), + ) + } + + ///not supported + fn deserialize_bytes(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + unimplemented!() + } + + ///not supported + fn deserialize_byte_buf(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + unimplemented!() + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + if matches!(self.yaml, yaml_rust::Yaml::Null) { + visitor.visit_none() + } else { + visitor.visit_some(self) + } + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + if matches!(self.yaml, yaml_rust::Yaml::Null) { + visitor.visit_unit() + } else { + Err(as_invalid(self.yaml, "unit")) + } + } + + ///unimplemented + fn deserialize_unit_struct( + self, + _name: &'static str, + _visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + ///unimplemented + fn deserialize_newtype_struct( + self, + _name: &'static str, + _visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + ///unimplemented + fn deserialize_seq(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + if let Some(n) = self.yaml.as_vec() { + let seq = SeqDeserializer::new(n.iter().map(|y| YamlWrap { yaml: y })); + visitor.visit_seq(seq) + } else { + Err(as_invalid(self.yaml, "seq")) + } + } + + ///unimplemented + fn deserialize_tuple(self, _len: usize, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + ///unimplemented + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + _visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match self.yaml { + Yaml::Hash(h) => { + let m = MapDeserializer::new( + h.iter() + .map(|(k, v)| (YamlWrap { yaml: k }, YamlWrap { yaml: v })), + ); + visitor.visit_map(m) + } + Yaml::Array(a) => { + let x = a + .iter() + .map(|y| y.as_hash().ok_or_else(|| as_invalid(self.yaml, "map"))) + .collect::, _>>()?; + let m = MapDeserializer::new( + x.into_iter() + .map(|x| x.iter()) + .flatten() + .map(|(k, v)| (YamlWrap { yaml: k }, YamlWrap { yaml: v })), + ); + visitor.visit_map(m) + } + _ => Err(as_invalid(self.yaml, "map")), + } + } + + ///unimplemented + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + _visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + if let Some(s) = self.yaml.as_str() { + visitor.visit_enum(s.into_deserializer()) + } else { + Err(as_invalid(self.yaml, "enum")) + } + } + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_str(visitor) + } + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_any(visitor) + } +}