diff --git a/.gitignore b/.gitignore index 621062929..ae37927d9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,9 @@ # Generated by Cargo /target/ /Cargo.lock + +# VSCode Extensions +/.trunk + +# MacOS Shenanigans +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index da3095450..4afeb39c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow `ron::value::RawValue` to capture any whitespace to the left and right of a ron value ([#487](https://github.com/ron-rs/ron/pull/487)) - Breaking: Enforce that ron always writes valid UTF-8 ([#488](https://github.com/ron-rs/ron/pull/488)) - Add convenient `Value::from` impls ([#498](https://github.com/ron-rs/ron/pull/498)) +- Add new extension `explicit_struct_names` which requires that struct names are included during deserialization ([#522](https://github.com/ron-rs/ron/pull/522)) ### Format Changes diff --git a/docs/extensions.md b/docs/extensions.md index a84922022..af5401f6f 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -114,3 +114,47 @@ With the `unwrap_variant_newtypes` extension, the first structural layer inside ``` Note that when the `unwrap_variant_newtypes` extension is enabled, the first layer inside a newtype variant will **always** be unwrapped, i.e. it is no longer possible to write `A(Inner(a: 4, b: true))` or `A((a: 4, b: true))`. + +# explicit_struct_names +During serialization, this extension emits struct names. For instance, this would be emitted: +```ron +Foo( + bar: Bar(42), +) +``` + +During deserialization, this extension requires that all structs have names attached to them. For example, the following deserializes perfectly fine: +```ron +Foo( + bar: Bar(42), +) +``` + +However, with the `explicit_struct_names` extension enabled, the following will throw an `ExpectedStructName` error: +```ron +( + bar: Bar(42), +) +``` + +Similarly, the following will throw the same error: +```ron +Foo( + bar: (42), +) +``` + +Note that if what you are parsing is spread across many files, you would likely use `Options::with_default_extension` to enable `Extensions::EXPLICIT_STRUCT_NAMES` before the parsing stage. This is because prepending `#![enable(explicit_struct_names)]` to the contents of every file you parse would violate DRY (Don't Repeat Yourself). + +Here is an example of how to enable `explicit_struct_names` using this method: +```rust +use ron::extensions::Extensions; +use ron::options::Options; + +// Setup the options +let options = Options::default().with_default_extension(Extensions::EXPLICIT_STRUCT_NAMES); +// Retrieve the contents of the file +let file_contents: &str = /* ... */; +// Parse the file's contents +let foo: Foo = options.from_str(file_contents)?; +``` diff --git a/src/error.rs b/src/error.rs index 74b65b297..46be80dd6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -110,6 +110,7 @@ pub enum Error { SuggestRawIdentifier(String), ExpectedRawValue, ExceededRecursionLimit, + ExpectedStructName(String), } impl fmt::Display for SpannedError { @@ -281,6 +282,11 @@ impl fmt::Display for Error { "Exceeded recursion limit, try increasing `ron::Options::recursion_limit` \ and using `serde_stacker` to protect against a stack overflow", ), + Error::ExpectedStructName(ref name) => write!( + f, + "Expected the explicit struct name {}, but none was found", + Identifier(name) + ), } } } @@ -568,6 +574,7 @@ mod tests { "Unexpected leading underscore in a number", ); check_error_message(&Error::UnexpectedChar('🦀'), "Unexpected char \'🦀\'"); + #[allow(invalid_from_utf8)] check_error_message( &Error::Utf8Error(std::str::from_utf8(b"error: \xff\xff\xff\xff").unwrap_err()), "invalid utf-8 sequence of 1 bytes from index 7", @@ -666,6 +673,10 @@ mod tests { "Exceeded recursion limit, try increasing `ron::Options::recursion_limit` \ and using `serde_stacker` to protect against a stack overflow", ); + check_error_message( + &Error::ExpectedStructName(String::from("Struct")), + "Expected the explicit struct name `Struct`, but none was found", + ); } fn check_error_message(err: &T, msg: &str) { diff --git a/src/extensions.rs b/src/extensions.rs index 454db7931..c4862b791 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -7,6 +7,10 @@ bitflags::bitflags! { const UNWRAP_NEWTYPES = 0x1; const IMPLICIT_SOME = 0x2; const UNWRAP_VARIANT_NEWTYPES = 0x4; + /// During serialization, this extension emits struct names. See also [`PrettyConfig::struct_names`](crate::ser::PrettyConfig::struct_names) for the [`PrettyConfig`](crate::ser::PrettyConfig) equivalent. + /// + /// During deserialization, this extension requires that structs' names are stated explicitly. + const EXPLICIT_STRUCT_NAMES = 0x8; } } // GRCOV_EXCL_STOP @@ -15,20 +19,23 @@ impl Extensions { /// Creates an extension flag from an ident. #[must_use] pub fn from_ident(ident: &str) -> Option { - match ident { - "unwrap_newtypes" => Some(Extensions::UNWRAP_NEWTYPES), - "implicit_some" => Some(Extensions::IMPLICIT_SOME), - "unwrap_variant_newtypes" => Some(Extensions::UNWRAP_VARIANT_NEWTYPES), - _ => None, + for (name, extension) in Extensions::all().iter_names() { + if ident == name.to_lowercase() { + return Some(extension); + } } + + None } } +// GRCOV_EXCL_START impl Default for Extensions { fn default() -> Self { Extensions::empty() } } +// GRCOV_EXCL_STOP #[cfg(test)] mod tests { @@ -42,17 +49,10 @@ mod tests { #[test] fn test_extension_serde() { - roundtrip_extensions(Extensions::default()); - roundtrip_extensions(Extensions::UNWRAP_NEWTYPES); - roundtrip_extensions(Extensions::IMPLICIT_SOME); - roundtrip_extensions(Extensions::UNWRAP_VARIANT_NEWTYPES); - roundtrip_extensions(Extensions::UNWRAP_NEWTYPES | Extensions::IMPLICIT_SOME); - roundtrip_extensions(Extensions::UNWRAP_NEWTYPES | Extensions::UNWRAP_VARIANT_NEWTYPES); - roundtrip_extensions(Extensions::IMPLICIT_SOME | Extensions::UNWRAP_VARIANT_NEWTYPES); - roundtrip_extensions( - Extensions::UNWRAP_NEWTYPES - | Extensions::IMPLICIT_SOME - | Extensions::UNWRAP_VARIANT_NEWTYPES, - ); + // iterate over the powerset of all extensions (i.e. every possible combination of extensions) + for bits in Extensions::empty().bits()..=Extensions::all().bits() { + let extensions = Extensions::from_bits_retain(bits); + roundtrip_extensions(extensions); + } } } diff --git a/src/parse.rs b/src/parse.rs index fdc1b1073..0144d0ade 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -666,6 +666,10 @@ impl<'a> Parser<'a> { pub fn consume_struct_name(&mut self, ident: &'static str) -> Result { if self.check_ident("") { + if self.exts.contains(Extensions::EXPLICIT_STRUCT_NAMES) { + return Err(Error::ExpectedStructName(ident.to_string())); + } + return Ok(false); } diff --git a/src/ser/mod.rs b/src/ser/mod.rs index cfc30c430..fc040937f 100644 --- a/src/ser/mod.rs +++ b/src/ser/mod.rs @@ -163,6 +163,8 @@ impl PrettyConfig { /// Configures whether to emit struct names. /// + /// See also [`Extensions::EXPLICIT_STRUCT_NAMES`] for the extension equivalent. + /// /// Default: `false` #[must_use] pub fn struct_names(mut self, struct_names: bool) -> Self { @@ -413,20 +415,10 @@ impl Serializer { let non_default_extensions = !options.default_extensions; - if (non_default_extensions & conf.extensions).contains(Extensions::IMPLICIT_SOME) { - writer.write_str("#![enable(implicit_some)]")?; - writer.write_str(&conf.new_line)?; - }; - if (non_default_extensions & conf.extensions).contains(Extensions::UNWRAP_NEWTYPES) { - writer.write_str("#![enable(unwrap_newtypes)]")?; - writer.write_str(&conf.new_line)?; - }; - if (non_default_extensions & conf.extensions) - .contains(Extensions::UNWRAP_VARIANT_NEWTYPES) - { - writer.write_str("#![enable(unwrap_variant_newtypes)]")?; + for (extension_name, _) in (non_default_extensions & conf.extensions).iter_names() { + write!(writer, "#![enable({})]", extension_name.to_lowercase())?; writer.write_str(&conf.new_line)?; - }; + } }; Ok(Serializer { output: writer, @@ -636,10 +628,16 @@ impl Serializer { Ok(()) } + /// Checks if struct names should be emitted + /// + /// Note that when using the `explicit_struct_names` extension, this method will use an OR operation on the extension and the [`PrettyConfig::struct_names`] option. See also [`Extensions::EXPLICIT_STRUCT_NAMES`] for the extension equivalent. fn struct_names(&self) -> bool { - self.pretty - .as_ref() - .map_or(false, |(pc, _)| pc.struct_names) + self.extensions() + .contains(Extensions::EXPLICIT_STRUCT_NAMES) + || self + .pretty + .as_ref() + .map_or(false, |(pc, _)| pc.struct_names) } } diff --git a/tests/522_explicit_struct_names.rs b/tests/522_explicit_struct_names.rs new file mode 100644 index 000000000..7a9a7d463 --- /dev/null +++ b/tests/522_explicit_struct_names.rs @@ -0,0 +1,109 @@ +use ron::{ + extensions::Extensions, + from_str, + ser::{to_string_pretty, PrettyConfig}, + Error, Options, +}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct Id(u32); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct Position(f32, f32); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +enum Query { + None, + Creature(Id), + Location(Position), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct Foo { + #[allow(unused)] + pub id: Id, + #[allow(unused)] + pub position: Position, + #[allow(unused)] + pub query: Query, +} + +const EXPECT_ERROR_MESSAGE: &'static str = + "expected `Err(Error::ExpectedStructName)`, deserializer returned `Ok`"; + +#[test] +fn explicit_struct_names() { + let options = Options::default().with_default_extension(Extensions::EXPLICIT_STRUCT_NAMES); + let foo_ser = Foo { + id: Id(3), + position: Position(0.0, 8.72), + query: Query::Creature(Id(4)), + }; + + // phase 1 (regular structs) + let content_regular = r#"( + id: Id(3), + position: Position(0.0, 8.72), + query: None, + )"#; + let foo = options.from_str::(content_regular); + assert_eq!( + foo.expect_err(EXPECT_ERROR_MESSAGE).code, + Error::ExpectedStructName("Foo".to_string()) + ); + + // phase 2 (newtype structs) + let content_newtype = r#"Foo( + id: (3), + position: Position(0.0, 8.72), + query: None, + )"#; + let foo = options.from_str::(content_newtype); + assert_eq!( + foo.expect_err(EXPECT_ERROR_MESSAGE).code, + Error::ExpectedStructName("Id".to_string()) + ); + + // phase 3 (tuple structs) + let content_tuple = r#"Foo( + id: Id(3), + position: (0.0, 8.72), + query: None, + )"#; + let foo = options.from_str::(content_tuple); + assert_eq!( + foo.expect_err(EXPECT_ERROR_MESSAGE).code, + Error::ExpectedStructName("Position".to_string()) + ); + + // phase 4 (test without this extension) + let _foo1 = from_str::(content_regular).unwrap(); + let _foo2 = from_str::(content_newtype).unwrap(); + let _foo3 = from_str::(content_tuple).unwrap(); + + // phase 5 (test serialization) + let pretty_config = PrettyConfig::new() + .extensions(Extensions::EXPLICIT_STRUCT_NAMES | Extensions::UNWRAP_VARIANT_NEWTYPES); + let content = to_string_pretty(&foo_ser, pretty_config).unwrap(); + assert_eq!( + content, + r#"#![enable(unwrap_variant_newtypes)] +#![enable(explicit_struct_names)] +Foo( + id: Id(3), + position: Position(0.0, 8.72), + query: Creature(4), +)"# + ); + let foo_de = from_str::(&content); + match foo_de { + // GRCOV_EXCL_START + Err(err) => panic!( + "failed to deserialize with `explicit_struct_names` and `unwrap_variant_newtypes`: {}", + err + ), + // GRCOV_EXCL_STOP + Ok(foo_de) => assert_eq!(foo_de, foo_ser), + } +} diff --git a/tests/non_string_tag.rs b/tests/non_string_tag.rs index 4f896a220..6c6c206e5 100644 --- a/tests/non_string_tag.rs +++ b/tests/non_string_tag.rs @@ -26,7 +26,7 @@ macro_rules! test_non_tag { } } - #[derive(Debug)] + #[derive(Debug)] // GRCOV_EXCL_LINE struct InternallyTagged; impl<'de> Deserialize<'de> for InternallyTagged { @@ -112,7 +112,7 @@ macro_rules! test_tag { } } - #[derive(Debug)] + #[derive(Debug)] // GRCOV_EXCL_LINE struct InternallyTagged; impl<'de> Deserialize<'de> for InternallyTagged {