Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add explicit_struct_names Extension #522

Merged
merged 27 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
521d91c
prepare adding `explicit_struct_names` extension
sylv256 Jan 3, 2024
35b973e
add error handling, prepare to implement it, prepare to add test
sylv256 Jan 3, 2024
f372298
implement extension, add test
sylv256 Jan 4, 2024
cf7710e
update CHANGELOG.md
sylv256 Jan 4, 2024
8605b97
add documentation, improve comments
sylv256 Jan 4, 2024
5f29b8c
add example using the Options method
sylv256 Jan 4, 2024
c2fdc0a
fix wording in ExpectedStructName error message
sylv256 Jan 4, 2024
c7c4a8a
add tuple struct test, use all codepaths
sylv256 Jan 4, 2024
ef7f6cc
Merge branch 'master' of https://github.com/SylvKT/ron
sylv256 Jan 4, 2024
6dabe55
in test: rename each foo variable to correspond with their phase (for…
sylv256 Jan 4, 2024
d59f672
fix extensions test
sylv256 Jan 4, 2024
b620b3f
add check for Error::ExpectedStructName message, suppress unnecessary…
sylv256 Jan 4, 2024
ec5f527
resolve redundant else block
sylv256 Jan 4, 2024
3f584af
add "see also" blurbs, OR the `explicit_struct_names` value in `Prett…
sylv256 Jan 4, 2024
1057138
fix formatting
sylv256 Jan 4, 2024
00a65cd
fix coverage
sylv256 Jan 4, 2024
966675a
move OR operation to Serializer::struct_names
sylv256 Jan 5, 2024
08ebb5d
fix pretty print in Error::ExpectedStructName message
sylv256 Jan 5, 2024
8add0cc
fix Serializer::struct_names
sylv256 Jan 5, 2024
17cb912
add trunk config files to .gitignore (for those who use it)
sylv256 Jan 6, 2024
2d99d9e
add .DS_Store to the .gitignore (you'll thank me later)
sylv256 Jan 6, 2024
90de04f
add test for enum variants, clean up `Extensions::from_ident` code
sylv256 Jan 6, 2024
7283d7b
Fix grammar error in extensions.rs
sylv256 Jan 6, 2024
313b30f
add `unwrap_variant_newtypes` to test
sylv256 Jan 6, 2024
45204f4
Merge branch 'master' of https://github.com/SylvKT/ron
sylv256 Jan 6, 2024
94fabf3
add to phase 5 a deserialize test
sylv256 Jan 6, 2024
4bf6ef7
add GRCOV_EXCL
sylv256 Jan 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@
# Generated by Cargo
/target/
/Cargo.lock

# VSCode Extensions
/.trunk

# MacOS Shenanigans
.DS_Store
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
44 changes: 44 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
```
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub enum Error {
SuggestRawIdentifier(String),
ExpectedRawValue,
ExceededRecursionLimit,
ExpectedStructName(String),
}

impl fmt::Display for SpannedError {
Expand Down Expand Up @@ -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)
),
}
}
}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<T: std::fmt::Display>(err: &T, msg: &str) {
Expand Down
34 changes: 17 additions & 17 deletions src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,20 +19,23 @@ impl Extensions {
/// Creates an extension flag from an ident.
#[must_use]
pub fn from_ident(ident: &str) -> Option<Extensions> {
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 {
Expand All @@ -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() {
sylv256 marked this conversation as resolved.
Show resolved Hide resolved
let extensions = Extensions::from_bits_retain(bits);
roundtrip_extensions(extensions);
}
}
}
4 changes: 4 additions & 0 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,10 @@ impl<'a> Parser<'a> {

pub fn consume_struct_name(&mut self, ident: &'static str) -> Result<bool> {
if self.check_ident("") {
if self.exts.contains(Extensions::EXPLICIT_STRUCT_NAMES) {
return Err(Error::ExpectedStructName(ident.to_string()));
}

return Ok(false);
}

Expand Down
30 changes: 14 additions & 16 deletions src/ser/mod.rs
juntyr marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -413,20 +415,10 @@ impl<W: fmt::Write> Serializer<W> {

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,
Expand Down Expand Up @@ -636,10 +628,16 @@ impl<W: fmt::Write> Serializer<W> {
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)
}
}

Expand Down
109 changes: 109 additions & 0 deletions tests/522_explicit_struct_names.rs
Original file line number Diff line number Diff line change
@@ -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::<Foo>(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::<Foo>(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::<Foo>(content_tuple);
assert_eq!(
foo.expect_err(EXPECT_ERROR_MESSAGE).code,
Error::ExpectedStructName("Position".to_string())
);
sylv256 marked this conversation as resolved.
Show resolved Hide resolved

// phase 4 (test without this extension)
let _foo1 = from_str::<Foo>(content_regular).unwrap();
let _foo2 = from_str::<Foo>(content_newtype).unwrap();
let _foo3 = from_str::<Foo>(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::<Foo>(&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),
}
}
4 changes: 2 additions & 2 deletions tests/non_string_tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ macro_rules! test_non_tag {
}
}

#[derive(Debug)]
#[derive(Debug)] // GRCOV_EXCL_LINE
struct InternallyTagged;

impl<'de> Deserialize<'de> for InternallyTagged {
Expand Down Expand Up @@ -112,7 +112,7 @@ macro_rules! test_tag {
}
}

#[derive(Debug)]
#[derive(Debug)] // GRCOV_EXCL_LINE
struct InternallyTagged;

impl<'de> Deserialize<'de> for InternallyTagged {
Expand Down
Loading