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 a parser for flags formatted as bar-separated-values #297

Merged
merged 16 commits into from Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 5 additions & 12 deletions .github/workflows/rust.yml
Expand Up @@ -40,17 +40,13 @@ jobs:
uses: actions/checkout@v2

- name: Install Rust Toolchain
uses: actions-rs/toolchain@v1
with:
override: true
profile: minimal
toolchain: ${{ matrix.channel }}-${{ matrix.rust_target }}
run: rustup default ${{ matrix.channel }}-${{ matrix.rust_target }}

- name: Install cargo-hack
run: cargo install cargo-hack

- name: Powerset
run: cargo hack test --feature-powerset --lib --optional-deps "serde" --depth 3 --skip rustc-dep-of-std
run: cargo hack test --feature-powerset --lib --optional-deps "std serde" --depth 3 --skip rustc-dep-of-std

- name: Docs
run: cargo doc --features example_generated
Expand All @@ -66,12 +62,9 @@ jobs:
uses: actions/checkout@v2

- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
target: thumbv6m-none-eabi
override: true
run: |
rustup default nightly
rustup target add thumbv6m-none-eabi

- name: Default features
uses: actions-rs/cargo@v1
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Expand Up @@ -28,8 +28,10 @@ trybuild = "1.0"
rustversion = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_test = "1.0"

[features]
std = []
example_generated = []
rustc-dep-of-std = ["core", "compiler_builtins"]

Expand Down
49 changes: 49 additions & 0 deletions examples/fmt.rs
@@ -0,0 +1,49 @@
//! An example of implementing Rust's standard formatting and parsing traits for flags types.

use core::{fmt, str};

fn main() -> Result<(), bitflags::ParseError> {
bitflags::bitflags! {
// You can `#[derive]` the `Debug` trait, but implementing it manually
// can produce output like `A | B` instead of `Flags(A | B)`.
// #[derive(Debug)]
#[derive(PartialEq, Eq)]
pub struct Flags: u32 {
const A = 1;
const B = 2;
const C = 4;
const D = 8;
}
}

impl fmt::Debug for Flags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}

impl fmt::Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

impl str::FromStr for Flags {
type Err = bitflags::ParseError;

fn from_str(flags: &str) -> Result<Self, Self::Err> {
Ok(Self(flags.parse()?))
}
}

let flags = Flags::A | Flags::B;

println!("{}", flags);

let formatted = flags.to_string();
let parsed: Flags = formatted.parse()?;

assert_eq!(flags, parsed);

Ok(())
}
36 changes: 36 additions & 0 deletions examples/serde.rs
@@ -0,0 +1,36 @@
//! An example of implementing `serde::Serialize` and `serde::Deserialize`.
//! The `#[serde(transparent)]` attribute is recommended to serialize directly
//! to the underlying bits type without wrapping it in a `serde` newtype.

#[cfg(feature = "serde")]
fn main() {
use serde_derive::*;

bitflags::bitflags! {
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(transparent)]
pub struct Flags: u32 {
const A = 1;
const B = 2;
const C = 4;
const D = 8;
}
}

let flags = Flags::A | Flags::B;

let serialized = serde_json::to_string(&flags).unwrap();

println!("{:?} -> {}", flags, serialized);

assert_eq!(serialized, r#""A | B""#);

let deserialized: Flags = serde_json::from_str(&serialized).unwrap();

println!("{} -> {:?}", serialized, flags);

assert_eq!(deserialized, flags);
}

#[cfg(not(feature = "serde"))]
fn main() {}
125 changes: 125 additions & 0 deletions examples/serde_compat.rs
@@ -0,0 +1,125 @@
//! An example of implementing `serde::Serialize` and `serde::Deserialize` equivalently to how
//! `#[derive(Serialize, Deserialize)]` would on `bitflags` `1.x` types.

#[cfg(feature = "serde")]
fn main() {
bitflags::bitflags! {
// Removed: `serde` traits from the `#[derive]` attribute
// #[derive(Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq)]
pub struct Flags: u32 {
const A = 1;
const B = 2;
const C = 4;
const D = 8;
}
}

// Added: Manual `Serialize` and `Deserialize` implementations based on a generic impl
impl serde::Serialize for Flags {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
legacy_format::serialize(self, serializer)
}
}

impl<'de> serde::Deserialize<'de> for Flags {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
legacy_format::deserialize(deserializer)
}
}

pub mod legacy_format {
KodrAus marked this conversation as resolved.
Show resolved Hide resolved
//! This module is a generic implementation of `Serialize` and `Deserialize` that can be used by
//! any flags type generated by `bitflags!`.
//!
//! Don't be intimidated by the amount of `serde` code in here! It boils down to serializing and deserializing
//! a struct with a single `bits` field. It may be converted into a library at some point, but is also suitable
//! to copy into your own project if you need it.

use core::{any::type_name, fmt};
use serde::{
de::{Error, MapAccess, Visitor},
ser::SerializeStruct,
Deserialize, Deserializer, Serialize, Serializer,
};

use bitflags::BitFlags;

/// Serialize a flags type equivalently to how `#[derive(Serialize)]` on a flags type
/// from `bitflags` `1.x` would.
pub fn serialize<T: BitFlags, S: Serializer>(
flags: &T,
serializer: S,
) -> Result<S::Ok, S::Error>
where
<T as BitFlags>::Bits: Serialize,
{
let mut serialize_struct = serializer.serialize_struct(type_name::<T>(), 1)?;
serialize_struct.serialize_field("bits", &flags.bits())?;
serialize_struct.end()
}

/// Deserialize a flags type equivalently to how `#[derive(Deserialize)]` on a flags type
/// from `bitflags` `1.x` would.
pub fn deserialize<'de, T: BitFlags, D: Deserializer<'de>>(
deserializer: D,
) -> Result<T, D::Error>
where
<T as BitFlags>::Bits: Deserialize<'de>,
{
struct BitsVisitor<T>(core::marker::PhantomData<T>);

impl<'de, T: Deserialize<'de>> Visitor<'de> for BitsVisitor<T> {
type Value = T;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a primitive bitflags value wrapped in a struct")
}

fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut bits = None;

while let Some(key) = map.next_key()? {
match key {
"bits" => {
if bits.is_some() {
return Err(Error::duplicate_field("bits"));
}

bits = Some(map.next_value()?);
}
v => return Err(Error::unknown_field(v, &["bits"])),
}
}

bits.ok_or_else(|| Error::missing_field("bits"))
}
}

let bits = deserializer.deserialize_struct(
type_name::<T>(),
&["bits"],
BitsVisitor(Default::default()),
)?;

Ok(T::from_bits_retain(bits))
}
}

let flags = Flags::A | Flags::B;

let serialized = serde_json::to_string(&flags).unwrap();

println!("{:?} -> {}", flags, serialized);

assert_eq!(serialized, r#"{"bits":3}"#);

let deserialized: Flags = serde_json::from_str(&serialized).unwrap();

println!("{} -> {:?}", serialized, flags);

assert_eq!(deserialized, flags);
}

#[cfg(not(feature = "serde"))]
fn main() {}
92 changes: 92 additions & 0 deletions src/error.rs
@@ -0,0 +1,92 @@
use core::fmt;

/// An error encountered while parsing flags from text.
#[derive(Debug)]
pub struct ParseError(ParseErrorKind);
KodrAus marked this conversation as resolved.
Show resolved Hide resolved

#[derive(Debug)]
enum ParseErrorKind {
EmptyFlag,
InvalidNamedFlag {
#[cfg(not(feature = "std"))]
got: (),
#[cfg(feature = "std")]
got: String,
},
InvalidHexFlag {
#[cfg(not(feature = "std"))]
got: (),
#[cfg(feature = "std")]
got: String,
},
}

impl ParseError {
/// An invalid hex flag was encountered.
pub fn invalid_hex_flag(flag: impl fmt::Display) -> Self {
let _flag = flag;

let got = {
#[cfg(feature = "std")]
{
_flag.to_string()
}
};

ParseError(ParseErrorKind::InvalidHexFlag { got })
}

/// A named flag that doesn't correspond to any on the flags type was encountered.
pub fn invalid_named_flag(flag: impl fmt::Display) -> Self {
let _flag = flag;

let got = {
#[cfg(feature = "std")]
{
_flag.to_string()
}
};

ParseError(ParseErrorKind::InvalidNamedFlag { got })
}

/// A hex or named flag wasn't found between separators.
pub fn empty_flag() -> Self {
ParseError(ParseErrorKind::EmptyFlag)
}
}

impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
ParseErrorKind::InvalidNamedFlag { got } => {
let _got = got;

write!(f, "unrecognized named flag")?;

#[cfg(feature = "std")]
{
write!(f, " `{}`", _got)?;
}
}
ParseErrorKind::InvalidHexFlag { got } => {
let _got = got;

write!(f, "invalid hex flag")?;

#[cfg(feature = "std")]
{
write!(f, " `{}`", _got)?;
}
}
ParseErrorKind::EmptyFlag => {
write!(f, "encountered empty flag")?;
}
}

Ok(())
}
}

#[cfg(feature = "std")]
impl std::error::Error for ParseError {}
14 changes: 4 additions & 10 deletions src/external.rs
Expand Up @@ -48,9 +48,8 @@ macro_rules! __impl_external_bitflags_serde {
&self,
serializer: S,
) -> $crate::__private::core::result::Result<S::Ok, S::Error> {
$crate::__private::serde_support::serialize_bits_default(
$crate::__private::core::stringify!($InternalBitFlags),
&self.bits,
$crate::__private::serde_support::serialize_bits_default::<$InternalBitFlags, $T, S>(
&self,
serializer,
)
}
Expand All @@ -60,14 +59,9 @@ macro_rules! __impl_external_bitflags_serde {
fn deserialize<D: $crate::__private::serde::Deserializer<'de>>(
deserializer: D,
) -> $crate::__private::core::result::Result<Self, D::Error> {
let bits = $crate::__private::serde_support::deserialize_bits_default(
$crate::__private::core::stringify!($InternalBitFlags),
$crate::__private::serde_support::deserialize_bits_default::<$InternalBitFlags, $T, D>(
deserializer,
)?;

$crate::__private::core::result::Result::Ok($InternalBitFlags::from_bits_retain(
bits,
))
)
}
}
};
Expand Down