diff --git a/.circleci/config.yml b/.circleci/config.yml index f6fd133..1e658aa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -142,6 +142,9 @@ jobs: - run: name: Cargo test (preserve proto field names) command: cargo test --workspace --features preserve-proto-field-names + - run: + name: Cargo test (ignore unknown enum variants) + command: cargo test --workspace --features ignore-unknown-enum-variants - cache_save vendor: diff --git a/pbjson-build/src/generator/enumeration.rs b/pbjson-build/src/generator/enumeration.rs index a511d8c..fb74b43 100644 --- a/pbjson-build/src/generator/enumeration.rs +++ b/pbjson-build/src/generator/enumeration.rs @@ -20,6 +20,7 @@ pub fn generate_enum( descriptor: &EnumDescriptor, writer: &mut W, use_integers_for_enums: bool, + ignore_unknown_enum_variants: bool, ) -> Result<()> { let rust_type = resolver.rust_type(path); @@ -74,7 +75,13 @@ pub fn generate_enum( // Generate Deserialize write_deserialize_start(0, &rust_type, writer)?; write_fields_array(writer, 2, variants.iter().map(|(name, _, _)| name.as_str()))?; - write_visitor(writer, 2, &rust_type, &variants)?; + write_visitor( + writer, + 2, + &rust_type, + &variants, + ignore_unknown_enum_variants, + )?; // Use deserialize_any to allow users to provide integers or strings writeln!( @@ -92,7 +99,23 @@ fn write_visitor( indent: usize, rust_type: &str, variants: &[(String, i32, String)], + ignore_unknown_enum_variants: bool, ) -> Result<()> { + // These are what needs to be done for an unknown i32 or string value. + let (or_unknown_i32, unknown_string_return) = if ignore_unknown_enum_variants { + // If ignore_unknown_enum_variants is set, we will return the default for the enum. + ( + format!(".or_else(|| Some({rust_type}::default()))"), + format!("Ok({rust_type}::default())"), + ) + } else { + // If ignore_unknown_enum_variants is not set, we will return an Err. + ( + "".into(), + "Err(serde::de::Error::unknown_variant(value, FIELDS))".into(), + ) + }; + // Protobuf supports deserialization of enumerations both from string and integer values writeln!( writer, @@ -111,7 +134,7 @@ fn write_visitor( {indent} {{ {indent} i32::try_from(v) {indent} .ok() -{indent} .and_then(|x| x.try_into().ok()) +{indent} .and_then(|x| x.try_into().ok(){or_unknown_i32}) {indent} .ok_or_else(|| {{ {indent} serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) {indent} }}) @@ -123,7 +146,7 @@ fn write_visitor( {indent} {{ {indent} i32::try_from(v) {indent} .ok() -{indent} .and_then(|x| x.try_into().ok()) +{indent} .and_then(|x| x.try_into().ok(){or_unknown_i32}) {indent} .ok_or_else(|| {{ {indent} serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) {indent} }}) @@ -151,7 +174,7 @@ fn write_visitor( writeln!( writer, - "{indent}_ => Err(serde::de::Error::unknown_variant(value, FIELDS)),", + "{indent}_ => {unknown_string_return},", indent = Indent(indent + 3) )?; writeln!(writer, "{}}}", Indent(indent + 2))?; diff --git a/pbjson-build/src/lib.rs b/pbjson-build/src/lib.rs index ce91cbd..839506c 100644 --- a/pbjson-build/src/lib.rs +++ b/pbjson-build/src/lib.rs @@ -107,6 +107,7 @@ pub struct Builder { btree_map_paths: Vec, emit_fields: bool, use_integers_for_enums: bool, + ignore_unknown_enum_variants: bool, preserve_proto_field_names: bool, } @@ -193,6 +194,12 @@ impl Builder { self } + /// Ignore unknown enum variants, and instead return the enum default. + pub fn ignore_unknown_enum_variants(&mut self) -> &mut Self { + self.ignore_unknown_enum_variants = true; + self + } + /// Output fields with their original names as defined in their proto schemas, instead of /// lowerCamelCase pub fn preserve_proto_field_names(&mut self) -> &mut Self { @@ -276,6 +283,7 @@ impl Builder { descriptor, writer, self.use_integers_for_enums, + self.ignore_unknown_enum_variants, )?, Descriptor::Message(descriptor) => { if let Some(message) = resolve_message(&self.descriptors, descriptor) { diff --git a/pbjson-test/Cargo.toml b/pbjson-test/Cargo.toml index a806e8d..7990dd2 100644 --- a/pbjson-test/Cargo.toml +++ b/pbjson-test/Cargo.toml @@ -13,6 +13,7 @@ pbjson-types = { path = "../pbjson-types" } serde = { version = "1.0", features = ["derive"] } [features] +ignore-unknown-enum-variants = [] ignore-unknown-fields = [] btree = [] emit-fields = [] diff --git a/pbjson-test/build.rs b/pbjson-test/build.rs index 040dd1c..d9283bd 100644 --- a/pbjson-test/build.rs +++ b/pbjson-test/build.rs @@ -44,6 +44,10 @@ fn main() -> Result<()> { .register_descriptors(&descriptor_set)? .extern_path(".test.external", "crate"); + if cfg!(feature = "ignore-unknown-enum-variants") { + builder.ignore_unknown_enum_variants(); + } + if cfg!(feature = "ignore-unknown-fields") { builder.ignore_unknown_fields(); } diff --git a/pbjson-test/src/lib.rs b/pbjson-test/src/lib.rs index c4f1773..45dde55 100644 --- a/pbjson-test/src/lib.rs +++ b/pbjson-test/src/lib.rs @@ -188,6 +188,33 @@ mod tests { assert_eq!(empty, Empty {}); } + #[test] + #[cfg(feature = "ignore-unknown-enum-variants")] + fn test_ignore_unknown_enum_variant() { + // A known string still maps correctly. + let kitchen_sink = + serde_json::from_str::("{\n \"value\": \"VALUE_A\"\n}").unwrap(); + assert!(matches!(kitchen_sink, KitchenSink { value: 45, .. })); + + // A known integer still maps correctly. + let kitchen_sink = serde_json::from_str::("{\n \"value\": 63\n}").unwrap(); + assert!(matches!(kitchen_sink, KitchenSink { value: 63, .. })); + + // An unknown string maps to default. + let kitchen_sink = + serde_json::from_str::("{\n \"value\": \"VALUE_DOES_NOT_EXIST\"\n}") + .unwrap(); + assert!(matches!(kitchen_sink, KitchenSink { value: 0, .. })); + + // An unknown integer maps to default. + let kitchen_sink = serde_json::from_str::("{\n \"value\": 1337\n}").unwrap(); + assert!(matches!(kitchen_sink, KitchenSink { value: 0, .. })); + + // Numeric values that don't fit in an i32 should still error. + assert!(serde_json::from_str::("{\n \"value\": 5.6\n}").is_err()); + assert!(serde_json::from_str::("{\n \"value\": 3000000000\n}").is_err()); + } + #[test] #[cfg(feature = "btree")] fn test_btree() {