Compare commits
14
Commits
afa8e15ace
...
rename
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3137d86c2 | ||
|
|
5bd28d6774 | ||
|
|
16ae2d403e | ||
|
|
803e582012 | ||
|
|
754af04b47 | ||
|
|
605630a824 | ||
|
|
615db6be6d | ||
|
|
dca10eb38d | ||
|
|
406b42f0ca | ||
|
|
0483c756f0 | ||
|
|
c8ca0b7e60 | ||
|
|
14effd44c5 | ||
|
|
9487a7f533 | ||
|
|
a799521e02 |
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "enumscribe"
|
name = "enumscribe"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = ["Tom Panton <pantonshire@gmail.com>"]
|
authors = ["Tom Panton <pantonshire@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -11,7 +11,7 @@ categories = ["encoding"]
|
|||||||
keywords = ["enum", "derive", "serde"]
|
keywords = ["enum", "derive", "serde"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
enumscribe_derive = { version = "0.3.0", path = "../enumscribe_derive", default-features = false, optional = true }
|
enumscribe_derive = { version = "0.3.1", path = "../enumscribe_derive", default-features = false, optional = true }
|
||||||
serde = { version = "1.0", default-features = false, optional = true }
|
serde = { version = "1.0", default-features = false, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -11,6 +11,49 @@ pub enum CowCappedString<'a, const N: usize> {
|
|||||||
Owned(CappedString<N>),
|
Owned(CappedString<N>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, const N: usize> CowCappedString<'a, N> {
|
||||||
|
/// Returns the string data contained by this `CowCappedString`.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
CowCappedString::Borrowed(s) => s,
|
||||||
|
CowCappedString::Owned(s) => s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new `CappedString` with capacity `M` containing the string converted to
|
||||||
|
/// uppercase. Returns `None` if the uppercase-converted string is longer than `M` bytes.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_uppercase<const M: usize>(&self) -> Option<CappedString<M>> {
|
||||||
|
CappedString::<M>::uppercase_from_str(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const N: usize> Deref for CowCappedString<'a, N> {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const N: usize> AsRef<str> for CowCappedString<'a, N> {
|
||||||
|
#[inline]
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const N: usize> Borrow<str> for CowCappedString<'a, N> {
|
||||||
|
#[inline]
|
||||||
|
fn borrow(&self) -> &str {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
impl<'de, const N: usize> serde::Deserialize<'de> for CowCappedString<'de, N> {
|
impl<'de, const N: usize> serde::Deserialize<'de> for CowCappedString<'de, N> {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
@@ -74,16 +117,45 @@ pub struct CappedString<const N: usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> CappedString<N> {
|
impl<const N: usize> CappedString<N> {
|
||||||
/// Returns a new `CappedString` containing a copy of the given string data. Returns an error
|
/// Returns a new `CappedString` containing a copy of the given string data. Returns `None` if
|
||||||
/// if the string data is larger than `N` bytes.
|
/// the string data is larger than `N` bytes.
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn from_str(s: &str) -> Option<Self> {
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
unsafe { Self::from_utf8_unchecked(s.as_bytes()) }
|
unsafe { Self::from_utf8_unchecked(s.as_bytes()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a new `CappedString` containing an uppercase conversion of the given string data.
|
||||||
|
/// Returns `None` if the converted string is larger than `N` bytes.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn uppercase_from_str(s: &str) -> Option<Self> {
|
||||||
|
let mut buf = [0u8; N];
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
|
||||||
|
for c_orig in s.chars() {
|
||||||
|
for c_upper in c_orig.to_uppercase() {
|
||||||
|
let encode_buf = cursor
|
||||||
|
.checked_add(c_upper.len_utf8())
|
||||||
|
.and_then(|encode_buf_end| buf.get_mut(cursor..encode_buf_end))?;
|
||||||
|
|
||||||
|
// FIXME: avoid the panic asm that gets generated for this encode (can never panic,
|
||||||
|
// as we always have at least `c_upper.len_utf8()` buffer space).
|
||||||
|
let encoded = c_upper.encode_utf8(encode_buf);
|
||||||
|
cursor = cursor.checked_add(encoded.len())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let filled_buf = buf.get(..cursor)?;
|
||||||
|
|
||||||
|
// SAFETY:
|
||||||
|
// `filled_buf` has been filled with a sequence of bytes obtained from `char::encode_utf8`,
|
||||||
|
// so it is valid UTF-8.
|
||||||
|
unsafe { Self::from_utf8_unchecked(filled_buf) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a new `CappedString` containing a copy of the given UTF-8 encoded string data.
|
/// Returns a new `CappedString` containing a copy of the given UTF-8 encoded string data.
|
||||||
/// Returns an error if more than `N` bytes of data are given.
|
/// Returns `None` if more than `N` bytes of data are given.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// - `bs` must be valid UTF-8.
|
/// - `bs` must be valid UTF-8.
|
||||||
@@ -134,32 +206,11 @@ impl<const N: usize> CappedString<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new `CappedString` with capacity `M` containing the string converted to
|
/// Returns a new `CappedString` with capacity `M` containing the string converted to
|
||||||
/// uppercase. Returns an error if the uppercase-converted string is longer than `M` bytes.
|
/// uppercase. Returns `None` if the uppercase-converted string is longer than `M` bytes.
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn to_uppercase<const M: usize>(&self) -> Option<CappedString<M>> {
|
pub fn to_uppercase<const M: usize>(&self) -> Option<CappedString<M>> {
|
||||||
let mut buf = [0u8; M];
|
CappedString::<M>::uppercase_from_str(self)
|
||||||
let mut cursor = 0usize;
|
|
||||||
|
|
||||||
for c_orig in self.as_str().chars() {
|
|
||||||
for c_upper in c_orig.to_uppercase() {
|
|
||||||
let encode_buf = cursor
|
|
||||||
.checked_add(c_upper.len_utf8())
|
|
||||||
.and_then(|encode_buf_end| buf.get_mut(cursor..encode_buf_end))?;
|
|
||||||
|
|
||||||
// FIXME: avoid the panic asm that gets generated for this encode (can never panic,
|
|
||||||
// as we always have at least `c_upper.len_utf8()` buffer space).
|
|
||||||
let encoded = c_upper.encode_utf8(encode_buf);
|
|
||||||
cursor = cursor.checked_add(encoded.len())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let filled_buf = buf.get(..cursor)?;
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// `filled_buf` has been filled with a sequence of bytes obtained from `char::encode_utf8`,
|
|
||||||
// so it is valid UTF-8.
|
|
||||||
unsafe { CappedString::from_utf8_unchecked(filled_buf) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +237,20 @@ impl<const N: usize> Borrow<str> for CappedString<N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> PartialEq for CappedString<N> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.as_str() == other.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Eq for CappedString<N> {}
|
||||||
|
|
||||||
|
impl<const N: usize> PartialEq<str> for CappedString<N> {
|
||||||
|
fn eq(&self, other: &str) -> bool {
|
||||||
|
self.as_str() == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
impl<'de, const N: usize> serde::Deserialize<'de> for CappedString<N> {
|
impl<'de, const N: usize> serde::Deserialize<'de> for CappedString<N> {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
@@ -227,7 +292,115 @@ impl<'de, const N: usize> serde::de::Visitor<'de> for CappedStringVisitor<N> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::CappedString;
|
use super::{CappedString, CowCappedString};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_cow_capped_string_deserialize() {
|
||||||
|
struct DeBorrowedOnly<const N: usize>(String);
|
||||||
|
|
||||||
|
impl<'de, const N: usize> serde::Deserialize<'de> for DeBorrowedOnly<N> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>
|
||||||
|
{
|
||||||
|
match CowCappedString::<'de, N>::deserialize(deserializer)? {
|
||||||
|
CowCappedString::Borrowed(s) => Ok(Self(s.to_owned())),
|
||||||
|
CowCappedString::Owned(_) => {
|
||||||
|
Err(serde::de::Error::custom("expected borrowed CowCappedString"))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeOwnedOnly<const N: usize>(String);
|
||||||
|
|
||||||
|
impl<'de, const N: usize> serde::Deserialize<'de> for DeOwnedOnly<N> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>
|
||||||
|
{
|
||||||
|
match CowCappedString::<'de, N>::deserialize(deserializer)? {
|
||||||
|
CowCappedString::Borrowed(_) => {
|
||||||
|
Err(serde::de::Error::custom("expected owned CowCappedString"))
|
||||||
|
},
|
||||||
|
CowCappedString::Owned(s) => Ok(Self(s.to_owned())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let DeBorrowedOnly(s) = serde_json::from_str::<DeBorrowedOnly<5>>(
|
||||||
|
r#""hello""#
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(s, "hello");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let DeBorrowedOnly(s) = serde_json::from_str::<DeBorrowedOnly<0>>(
|
||||||
|
r#""hello""#
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(s, "hello");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let s = serde_json::from_str::<DeOwnedOnly<5>>(
|
||||||
|
r#""hello""#
|
||||||
|
);
|
||||||
|
assert!(s.is_err());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let DeOwnedOnly(s) = serde_json::from_str::<DeOwnedOnly<3>>(
|
||||||
|
r#""\u87f9""#
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(s, "蟹");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let s = serde_json::from_str::<DeBorrowedOnly<3>>(
|
||||||
|
r#""\u87f9""#
|
||||||
|
);
|
||||||
|
assert!(s.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_capped_string_deserialize() {
|
||||||
|
{
|
||||||
|
let s = serde_json::from_str::<CappedString<5>>(
|
||||||
|
r#""hello""#
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(s.as_str(), "hello");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let s = serde_json::from_str::<CappedString<4>>(
|
||||||
|
r#""hello""#
|
||||||
|
);
|
||||||
|
assert!(s.is_err());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let s = serde_json::from_str::<CappedString<10>>(
|
||||||
|
r#""hello""#
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(s.as_str(), "hello");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let s = serde_json::from_str::<CappedString<12>>(
|
||||||
|
r#""hello\tworld\n""#
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(s.as_str(), "hello\tworld\n");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let s = serde_json::from_str::<CappedString<3>>(
|
||||||
|
r#""\u87f9""#
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(s.as_str(), "蟹");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let s = serde_json::from_str::<CappedString<2>>(
|
||||||
|
r#""\u87f9""#
|
||||||
|
);
|
||||||
|
assert!(s.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_capped_string_uppercase() {
|
fn test_capped_string_uppercase() {
|
||||||
@@ -247,8 +420,8 @@ mod tests {
|
|||||||
assert_eq!(s2.as_str(), "HELLO");
|
assert_eq!(s2.as_str(), "HELLO");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let s1 = CappedString::<5>::from_str("hello").unwrap();
|
let s = CappedString::<5>::from_str("hello").unwrap();
|
||||||
assert!(s1.to_uppercase::<4>().is_none());
|
assert!(s.to_uppercase::<4>().is_none());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let s1 = CappedString::<5>::from_str("groß").unwrap();
|
let s1 = CappedString::<5>::from_str("groß").unwrap();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "enumscribe_derive"
|
name = "enumscribe_derive"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = ["Tom Panton <pantonshire@gmail.com>"]
|
authors = ["Tom Panton <pantonshire@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+151
-42
@@ -7,12 +7,51 @@ use syn::{DataEnum, Fields, Attribute};
|
|||||||
|
|
||||||
use crate::attribute::{Dict, Value};
|
use crate::attribute::{Dict, Value};
|
||||||
use crate::error::{MacroError, MacroResult};
|
use crate::error::{MacroError, MacroResult};
|
||||||
|
use crate::rename::RenameVariant;
|
||||||
use crate::{TokenStream2, CASE_SENSITIVE};
|
use crate::{TokenStream2, CASE_SENSITIVE};
|
||||||
use crate::{CASE_INSENSITIVE, CRATE_ATTR, IGNORE, NAME, OTHER};
|
use crate::{CASE_INSENSITIVE, RENAME, RENAME_ALL, CRATE_ATTR, IGNORE, NAME, OTHER};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Enum<'a> {
|
pub(crate) struct Enum<'a> {
|
||||||
pub(crate) variants: Vec<Variant<'a>>,
|
variants: Box<[Variant<'a>]>,
|
||||||
|
name_capacity: usize,
|
||||||
|
name_upper_capacity: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Enum<'a> {
|
||||||
|
pub(crate) fn new(variants: Box<[Variant<'a>]>) -> Self {
|
||||||
|
let name_capacity = variants
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.v_type.as_named())
|
||||||
|
.map(|named| named.name().len())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let name_upper_capacity = variants
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.v_type.as_named())
|
||||||
|
.map(|named| named.name_upper().len())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
variants,
|
||||||
|
name_capacity,
|
||||||
|
name_upper_capacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn variants(&self) -> &[Variant<'a>] {
|
||||||
|
&self.variants
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn name_capacity(&self) -> usize {
|
||||||
|
self.name_capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn name_upper_capacity(&self) -> usize {
|
||||||
|
self.name_upper_capacity
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -25,14 +64,69 @@ pub(crate) struct Variant<'a> {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) enum VariantType<'a> {
|
pub(crate) enum VariantType<'a> {
|
||||||
Ignore,
|
Ignore,
|
||||||
Named {
|
Named(NamedVariant),
|
||||||
name: String,
|
Other(OtherVariant<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> VariantType<'a> {
|
||||||
|
pub(crate) fn as_named(&self) -> Option<&NamedVariant> {
|
||||||
|
match self {
|
||||||
|
Self::Named(named) => Some(named),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct NamedVariant {
|
||||||
|
name: Box<str>,
|
||||||
|
name_upper: Box<str>,
|
||||||
|
constructor: VariantConstructor,
|
||||||
|
case_insensitive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NamedVariant {
|
||||||
|
pub(crate) fn new(
|
||||||
|
name: Box<str>,
|
||||||
constructor: VariantConstructor,
|
constructor: VariantConstructor,
|
||||||
case_insensitive: bool,
|
case_insensitive: bool
|
||||||
},
|
) -> Self
|
||||||
Other {
|
{
|
||||||
field_name: Option<&'a Ident>,
|
let name_upper = char_wise_uppercase(&name);
|
||||||
},
|
Self {
|
||||||
|
name,
|
||||||
|
name_upper,
|
||||||
|
constructor,
|
||||||
|
case_insensitive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn name_upper(&self) -> &str {
|
||||||
|
&self.name_upper
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn constructor(&self) -> VariantConstructor {
|
||||||
|
self.constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn case_insensitive(&self) -> bool {
|
||||||
|
self.case_insensitive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct OtherVariant<'a> {
|
||||||
|
field_name: Option<&'a Ident>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> OtherVariant<'a> {
|
||||||
|
pub(crate) fn field_name(&self) -> Option<&'a Ident> {
|
||||||
|
self.field_name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -58,20 +152,18 @@ impl<'a> Variant<'a> {
|
|||||||
match &self.v_type {
|
match &self.v_type {
|
||||||
VariantType::Ignore => Ok(None),
|
VariantType::Ignore => Ok(None),
|
||||||
|
|
||||||
VariantType::Named {
|
VariantType::Named(named) => {
|
||||||
name, constructor, ..
|
let constructor_tokens = named.constructor().empty_toks();
|
||||||
} => {
|
|
||||||
let constructor_tokens = constructor.empty();
|
|
||||||
let pattern = quote! { #enum_ident::#variant_ident #constructor_tokens };
|
let pattern = quote! { #enum_ident::#variant_ident #constructor_tokens };
|
||||||
Ok(Some((pattern, named_fn(self, enum_ident, name)?)))
|
Ok(Some((pattern, named_fn(self, enum_ident, named.name())?)))
|
||||||
}
|
}
|
||||||
|
|
||||||
VariantType::Other { field_name } => {
|
VariantType::Other(other) => {
|
||||||
let field_name_tokens = match field_name {
|
let field_name_tokens = match other.field_name() {
|
||||||
Some(field_name) => field_name.to_token_stream(),
|
Some(field_name) => field_name.to_token_stream(),
|
||||||
None => quote! { __enumscribe_other_inner },
|
None => quote! { __enumscribe_other_inner },
|
||||||
};
|
};
|
||||||
let pattern = match field_name {
|
let pattern = match other.field_name() {
|
||||||
Some(_) => quote! { #enum_ident::#variant_ident{#field_name_tokens} },
|
Some(_) => quote! { #enum_ident::#variant_ident{#field_name_tokens} },
|
||||||
None => quote! { #enum_ident::#variant_ident(#field_name_tokens) },
|
None => quote! { #enum_ident::#variant_ident(#field_name_tokens) },
|
||||||
};
|
};
|
||||||
@@ -85,7 +177,7 @@ impl<'a> Variant<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VariantConstructor {
|
impl VariantConstructor {
|
||||||
pub(crate) fn empty(&self) -> TokenStream2 {
|
pub(crate) fn empty_toks(&self) -> TokenStream2 {
|
||||||
match self {
|
match self {
|
||||||
VariantConstructor::None => quote! {},
|
VariantConstructor::None => quote! {},
|
||||||
VariantConstructor::Paren => quote! { () },
|
VariantConstructor::Paren => quote! { () },
|
||||||
@@ -101,19 +193,20 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
|
|||||||
let mut taken_sensitive_names = HashSet::new();
|
let mut taken_sensitive_names = HashSet::new();
|
||||||
let mut other_variant = false;
|
let mut other_variant = false;
|
||||||
|
|
||||||
let global_case_insensitive = {
|
let mut global_dict = Dict::from_attrs(CRATE_ATTR, attrs)?;
|
||||||
let mut dict = Dict::from_attrs(CRATE_ATTR, attrs)?;
|
|
||||||
|
let (global_case_insensitive, _) = global_dict.remove_typed_or_default(
|
||||||
|
CASE_INSENSITIVE,
|
||||||
|
(false, data.enum_token.span()),
|
||||||
|
Value::value_bool,
|
||||||
|
)?;
|
||||||
|
|
||||||
let (global_case_insensitive, _) = dict.remove_typed_or_default(
|
let global_rename = global_dict.remove_typed(RENAME_ALL, Value::value_string)?
|
||||||
CASE_INSENSITIVE,
|
.map(|(global_rename, span)| RenameVariant::from_str(&global_rename, span))
|
||||||
(false, data.enum_token.span()),
|
.transpose()?;
|
||||||
Value::value_bool,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
dict.assert_empty()?;
|
global_dict.assert_empty()?;
|
||||||
|
drop(global_dict);
|
||||||
global_case_insensitive
|
|
||||||
};
|
|
||||||
|
|
||||||
for variant in data.variants.iter() {
|
for variant in data.variants.iter() {
|
||||||
let variant_span = variant.span();
|
let variant_span = variant.span();
|
||||||
@@ -122,10 +215,7 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
|
|||||||
let mut dict = Dict::from_attrs(CRATE_ATTR, &variant.attrs)?;
|
let mut dict = Dict::from_attrs(CRATE_ATTR, &variant.attrs)?;
|
||||||
|
|
||||||
// Convert the values in the Dict to the appropriate types
|
// Convert the values in the Dict to the appropriate types
|
||||||
let name_opt = dict.remove_typed(
|
let name_opt = dict.remove_typed(NAME, Value::value_string)?;
|
||||||
NAME,
|
|
||||||
Value::value_string
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let (other, other_span) = dict.remove_typed_or_default(
|
let (other, other_span) = dict.remove_typed_or_default(
|
||||||
OTHER,
|
OTHER,
|
||||||
@@ -166,6 +256,11 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let rename = dict.remove_typed(RENAME, Value::value_string)?
|
||||||
|
.map(|(rename, span)| RenameVariant::from_str(&rename, span))
|
||||||
|
.transpose()?
|
||||||
|
.or(global_rename);
|
||||||
|
|
||||||
// Return an error if there are any unrecognised keys in the Dict
|
// Return an error if there are any unrecognised keys in the Dict
|
||||||
dict.assert_empty()?;
|
dict.assert_empty()?;
|
||||||
|
|
||||||
@@ -220,14 +315,21 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
|
|||||||
|
|
||||||
Variant {
|
Variant {
|
||||||
data: variant,
|
data: variant,
|
||||||
v_type: VariantType::Other { field_name },
|
v_type: VariantType::Other(OtherVariant { field_name }),
|
||||||
span: variant_span,
|
span: variant_span,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use the str name if one is provided, otherwise use the variant's name
|
// Use the str name if one is provided, otherwise use the variant's name
|
||||||
let (name, name_span) = match name_opt {
|
let (name, name_span) = match name_opt {
|
||||||
Some((name, name_span)) => (name, name_span),
|
Some((name, name_span)) => (name, name_span),
|
||||||
None => (variant.ident.to_string(), variant.ident.span()),
|
None => {
|
||||||
|
let name_span = variant.ident.span();
|
||||||
|
let mut name = variant.ident.to_string();
|
||||||
|
if let Some(rename) = rename {
|
||||||
|
name = rename.apply(&name);
|
||||||
|
}
|
||||||
|
(name, name_span)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Do not allow duplicate names
|
// Do not allow duplicate names
|
||||||
@@ -279,13 +381,12 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
|
|||||||
Fields::Unit => VariantConstructor::None,
|
Fields::Unit => VariantConstructor::None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let named = NamedVariant::new(name.into_boxed_str(), constructor, case_insensitive);
|
||||||
|
let v_type = VariantType::Named(named);
|
||||||
|
|
||||||
Variant {
|
Variant {
|
||||||
data: variant,
|
data: variant,
|
||||||
v_type: VariantType::Named {
|
v_type,
|
||||||
name,
|
|
||||||
constructor,
|
|
||||||
case_insensitive,
|
|
||||||
},
|
|
||||||
span: variant_span,
|
span: variant_span,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -293,5 +394,13 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
|
|||||||
variants.push(scribe_variant);
|
variants.push(scribe_variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Enum { variants })
|
Ok(Enum::new(variants.into_boxed_slice()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn char_wise_uppercase(s: &str) -> Box<str> {
|
||||||
|
// Use the same uppercase algorithm as `enumscribe::internal::capped_string`.
|
||||||
|
s.chars()
|
||||||
|
.flat_map(char::to_uppercase)
|
||||||
|
.collect::<String>()
|
||||||
|
.into_boxed_str()
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ impl MacroError {
|
|||||||
pub(crate) fn to_token_stream2(&self) -> TokenStream2 {
|
pub(crate) fn to_token_stream2(&self) -> TokenStream2 {
|
||||||
let message = &self.message;
|
let message = &self.message;
|
||||||
quote_spanned! {
|
quote_spanned! {
|
||||||
self.span => ::std::compile_error!(#message);
|
self.span => ::core::compile_error!(#message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::enums::{Enum, Variant, VariantType};
|
|||||||
mod attribute;
|
mod attribute;
|
||||||
mod enums;
|
mod enums;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod rename;
|
||||||
|
|
||||||
const CRATE_ATTR: &'static str = "enumscribe";
|
const CRATE_ATTR: &'static str = "enumscribe";
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@ const OTHER: &'static str = "other";
|
|||||||
const IGNORE: &'static str = "ignore";
|
const IGNORE: &'static str = "ignore";
|
||||||
const CASE_INSENSITIVE: &'static str = "case_insensitive";
|
const CASE_INSENSITIVE: &'static str = "case_insensitive";
|
||||||
const CASE_SENSITIVE: &'static str = "case_sensitive";
|
const CASE_SENSITIVE: &'static str = "case_sensitive";
|
||||||
|
const RENAME: &'static str = "rename";
|
||||||
|
const RENAME_ALL: &'static str = "rename_all";
|
||||||
|
|
||||||
type TokenStream2 = proc_macro2::TokenStream;
|
type TokenStream2 = proc_macro2::TokenStream;
|
||||||
|
|
||||||
@@ -59,9 +62,9 @@ where
|
|||||||
|
|
||||||
let enum_ident = &input.ident;
|
let enum_ident = &input.ident;
|
||||||
|
|
||||||
let mut match_arms = Vec::with_capacity(parsed_enum.variants.len());
|
let mut match_arms = Vec::with_capacity(parsed_enum.variants().len());
|
||||||
|
|
||||||
for variant in parsed_enum.variants.iter() {
|
for variant in parsed_enum.variants().iter() {
|
||||||
match variant.match_variant(enum_ident, &named_fn, &other_fn) {
|
match variant.match_variant(enum_ident, &named_fn, &other_fn) {
|
||||||
Ok(Some((pattern, result))) => match_arms.push(quote! { #pattern => #result }),
|
Ok(Some((pattern, result))) => match_arms.push(quote! { #pattern => #result }),
|
||||||
Ok(None) => return ignore_err_fn(variant, enum_ident).into(),
|
Ok(None) => return ignore_err_fn(variant, enum_ident).into(),
|
||||||
@@ -102,9 +105,9 @@ where
|
|||||||
let enum_ident = &input.ident;
|
let enum_ident = &input.ident;
|
||||||
|
|
||||||
let mut ignore_variant = false;
|
let mut ignore_variant = false;
|
||||||
let mut match_arms = Vec::with_capacity(parsed_enum.variants.len());
|
let mut match_arms = Vec::with_capacity(parsed_enum.variants().len());
|
||||||
|
|
||||||
for variant in parsed_enum.variants.iter() {
|
for variant in parsed_enum.variants().iter() {
|
||||||
match variant.match_variant(enum_ident, &named_fn, &other_fn) {
|
match variant.match_variant(enum_ident, &named_fn, &other_fn) {
|
||||||
Ok(Some((pattern, result))) => match_arms.push(quote! { #pattern => #result }),
|
Ok(Some((pattern, result))) => match_arms.push(quote! { #pattern => #result }),
|
||||||
Ok(None) => ignore_variant = true,
|
Ok(None) => ignore_variant = true,
|
||||||
@@ -192,30 +195,28 @@ where
|
|||||||
let mut case_sensitive_arms = Vec::new();
|
let mut case_sensitive_arms = Vec::new();
|
||||||
let mut case_insensitive_arms = Vec::new();
|
let mut case_insensitive_arms = Vec::new();
|
||||||
|
|
||||||
for variant in parsed_enum.variants.iter() {
|
for variant in parsed_enum.variants().iter() {
|
||||||
let variant_ident = &variant.data.ident;
|
let variant_ident = &variant.data.ident;
|
||||||
|
|
||||||
match &variant.v_type {
|
match &variant.v_type {
|
||||||
VariantType::Ignore => (),
|
VariantType::Ignore => (),
|
||||||
|
|
||||||
VariantType::Named {
|
VariantType::Named(named) => {
|
||||||
name,
|
let match_pattern = if named.case_insensitive() {
|
||||||
constructor,
|
let uppercase_name = named.name_upper();
|
||||||
case_insensitive,
|
quote! { #uppercase_name }
|
||||||
} => {
|
|
||||||
let match_pattern = if *case_insensitive {
|
|
||||||
let lowercase_name = name.to_lowercase();
|
|
||||||
quote! { #lowercase_name }
|
|
||||||
} else {
|
} else {
|
||||||
|
let name = named.name();
|
||||||
quote! { #name }
|
quote! { #name }
|
||||||
};
|
};
|
||||||
|
|
||||||
let constructor_tokens = constructor.empty();
|
let constructor_tokens = named.constructor().empty_toks();
|
||||||
let constructed_variant =
|
let constructed_variant = quote! {
|
||||||
quote! { #enum_ident::#variant_ident #constructor_tokens };
|
#enum_ident::#variant_ident #constructor_tokens
|
||||||
|
};
|
||||||
let match_result = named_fn(constructed_variant);
|
let match_result = named_fn(constructed_variant);
|
||||||
|
|
||||||
if *case_insensitive {
|
if named.case_insensitive() {
|
||||||
&mut case_insensitive_arms
|
&mut case_insensitive_arms
|
||||||
} else {
|
} else {
|
||||||
&mut case_sensitive_arms
|
&mut case_sensitive_arms
|
||||||
@@ -223,11 +224,11 @@ where
|
|||||||
.push(quote! { #match_pattern => #match_result });
|
.push(quote! { #match_pattern => #match_result });
|
||||||
}
|
}
|
||||||
|
|
||||||
VariantType::Other { field_name } => {
|
VariantType::Other(other) => {
|
||||||
let unscribe_value =
|
let unscribe_value =
|
||||||
quote! { <_ as ::std::convert::Into<_>>::into(#match_against) };
|
quote! { <_ as ::std::convert::Into<_>>::into(#match_against) };
|
||||||
|
|
||||||
let constructed_variant = match field_name {
|
let constructed_variant = match other.field_name() {
|
||||||
None => quote! {
|
None => quote! {
|
||||||
#enum_ident::#variant_ident(#unscribe_value)
|
#enum_ident::#variant_ident(#unscribe_value)
|
||||||
},
|
},
|
||||||
@@ -251,10 +252,23 @@ where
|
|||||||
let case_insensitive_match = if case_insensitive_arms.is_empty() {
|
let case_insensitive_match = if case_insensitive_arms.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
let match_against_upper_ident = quote! { __enumscribe_unscribe_uppercase };
|
||||||
|
let name_upper_cap = parsed_enum.name_upper_capacity();
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
let __enumscribe_unscribe_lowercase = #match_against.to_lowercase();
|
match ::enumscribe
|
||||||
match __enumscribe_unscribe_lowercase.as_str() {
|
::internal
|
||||||
#(#case_insensitive_arms,)*
|
::capped_string
|
||||||
|
::CappedString
|
||||||
|
::<#name_upper_cap>
|
||||||
|
::uppercase_from_str(#match_against)
|
||||||
|
{
|
||||||
|
Some(#match_against_upper_ident) => {
|
||||||
|
match &*#match_against_upper_ident {
|
||||||
|
#(#case_insensitive_arms,)*
|
||||||
|
#other_arm,
|
||||||
|
}
|
||||||
|
},
|
||||||
#other_arm,
|
#other_arm,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -659,23 +673,22 @@ pub fn derive_enum_serialize(input: TokenStream) -> TokenStream {
|
|||||||
let mut match_arms = Vec::new();
|
let mut match_arms = Vec::new();
|
||||||
let mut ignore_variant = false;
|
let mut ignore_variant = false;
|
||||||
|
|
||||||
for variant in parsed_enum.variants.iter() {
|
for variant in parsed_enum.variants().iter() {
|
||||||
let variant_ident = &variant.data.ident;
|
let variant_ident = &variant.data.ident;
|
||||||
|
|
||||||
match &variant.v_type {
|
match &variant.v_type {
|
||||||
VariantType::Ignore => ignore_variant = true,
|
VariantType::Ignore => ignore_variant = true,
|
||||||
|
|
||||||
VariantType::Named {
|
VariantType::Named(named) => {
|
||||||
name, constructor, ..
|
let constructor_tokens = named.constructor().empty_toks();
|
||||||
} => {
|
let name = named.name();
|
||||||
let constructor_tokens = constructor.empty();
|
|
||||||
match_arms.push(quote! {
|
match_arms.push(quote! {
|
||||||
#enum_ident::#variant_ident #constructor_tokens =>
|
#enum_ident::#variant_ident #constructor_tokens =>
|
||||||
#serializer_ident.serialize_str(#name)
|
#serializer_ident.serialize_str(#name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
VariantType::Other { field_name } => match field_name {
|
VariantType::Other(other) => match other.field_name() {
|
||||||
Some(field_name) => match_arms.push(quote! {
|
Some(field_name) => match_arms.push(quote! {
|
||||||
#enum_ident::#variant_ident { #field_name } =>
|
#enum_ident::#variant_ident { #field_name } =>
|
||||||
#serializer_ident.serialize_str(&#field_name)
|
#serializer_ident.serialize_str(&#field_name)
|
||||||
@@ -748,13 +761,14 @@ pub fn derive_enum_deserialize(input: TokenStream) -> TokenStream {
|
|||||||
let enum_ident = &input.ident;
|
let enum_ident = &input.ident;
|
||||||
|
|
||||||
let deserializer_ident = quote! { __enumscribe_deserializer };
|
let deserializer_ident = quote! { __enumscribe_deserializer };
|
||||||
|
let deserialized_cow_capped_str_ident = quote! { __enumscribe_deserialized_cow_capped_str };
|
||||||
let deserialized_str_ident = quote! { __enumscribe_deserialized_str };
|
let deserialized_str_ident = quote! { __enumscribe_deserialized_str };
|
||||||
|
|
||||||
let variant_strings = parsed_enum
|
let variant_strings = parsed_enum
|
||||||
.variants
|
.variants()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|variant| match &variant.v_type {
|
.map(|variant| match &variant.v_type {
|
||||||
VariantType::Named { name, .. } => Some(name.as_str()),
|
VariantType::Named(named) => Some(named.name()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.filter_map(|name| name)
|
.filter_map(|name| name)
|
||||||
@@ -771,22 +785,32 @@ pub fn derive_enum_deserialize(input: TokenStream) -> TokenStream {
|
|||||||
::core::result::Result::Ok(#constructed_other_variant)
|
::core::result::Result::Ok(#constructed_other_variant)
|
||||||
},
|
},
|
||||||
|_| Ok(quote! {
|
|_| Ok(quote! {
|
||||||
__enumscribe_deserialize_base_case => ::core::result::Result::Err(
|
_ => ::core::result::Result::Err(
|
||||||
::serde::de::Error::unknown_variant(
|
::serde::de::Error::unknown_variant(
|
||||||
__enumscribe_deserialize_base_case,
|
#deserialized_str_ident,
|
||||||
&[#(#variant_strings),*]
|
&[#(#variant_strings),*]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let name_cap = parsed_enum.name_capacity();
|
||||||
|
|
||||||
(quote! {
|
(quote! {
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
impl<'de> ::serde::Deserialize<'de> for #enum_ident {
|
impl<'de> ::serde::Deserialize<'de> for #enum_ident {
|
||||||
fn deserialize<D>(#deserializer_ident: D) -> ::core::result::Result<Self, D::Error>
|
fn deserialize<D>(#deserializer_ident: D) -> ::core::result::Result<Self, D::Error>
|
||||||
where D: ::serde::Deserializer<'de>
|
where D: ::serde::Deserializer<'de>
|
||||||
{
|
{
|
||||||
let #deserialized_str_ident = <&str as ::serde::Deserialize<'_>>::deserialize(#deserializer_ident)?;
|
let #deserialized_cow_capped_str_ident = <
|
||||||
|
::enumscribe
|
||||||
|
::internal
|
||||||
|
::capped_string
|
||||||
|
::CowCappedString<'de, #name_cap>
|
||||||
|
as ::serde::Deserialize<'_>
|
||||||
|
>::deserialize(#deserializer_ident)?;
|
||||||
|
|
||||||
|
let #deserialized_str_ident = &*#deserialized_cow_capped_str_ident;
|
||||||
#main_match
|
#main_match
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
use proc_macro2::Span;
|
||||||
|
|
||||||
|
use crate::error::{MacroResult, MacroError};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) enum RenameVariant {
|
||||||
|
Lower,
|
||||||
|
Upper,
|
||||||
|
Pascal,
|
||||||
|
Camel,
|
||||||
|
Snake,
|
||||||
|
ScreamingSnake,
|
||||||
|
Kebab,
|
||||||
|
ScreamingKebab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenameVariant {
|
||||||
|
pub(crate) fn from_str(s: &str, span: Span) -> MacroResult<Self> {
|
||||||
|
// Shame we can't use enumscribe for this...
|
||||||
|
match s {
|
||||||
|
"lowercase" => Ok(Self::Lower),
|
||||||
|
"UPPERCASE" => Ok(Self::Upper),
|
||||||
|
"PascalCase" => Ok(Self::Pascal),
|
||||||
|
"camelCase" => Ok(Self::Camel),
|
||||||
|
"snake_case" => Ok(Self::Snake),
|
||||||
|
"SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnake),
|
||||||
|
"kebab-case" => Ok(Self::Kebab),
|
||||||
|
"SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebab),
|
||||||
|
_ => Err(MacroError::new(
|
||||||
|
format!(
|
||||||
|
"invalid case {:?} (allowed values are: \
|
||||||
|
lowercase, \
|
||||||
|
UPPERCASE, \
|
||||||
|
PascalCase, \
|
||||||
|
camelCase, \
|
||||||
|
snake_case, \
|
||||||
|
SCREAMING_SNAKE_CASE, \
|
||||||
|
kebab-case, \
|
||||||
|
SCREAMING-KEBAB-CASE)",
|
||||||
|
s
|
||||||
|
),
|
||||||
|
span
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply(self, s: &str) -> String {
|
||||||
|
match self {
|
||||||
|
RenameVariant::Lower => s.to_lowercase(),
|
||||||
|
RenameVariant::Upper => s.to_uppercase(),
|
||||||
|
RenameVariant::Pascal => PascalCase.convert_enum_variant(s),
|
||||||
|
RenameVariant::Camel => CamelCase.convert_enum_variant(s),
|
||||||
|
RenameVariant::Snake => SnakeCase(CharCase::Lower).convert_enum_variant(s),
|
||||||
|
RenameVariant::ScreamingSnake => SnakeCase(CharCase::Upper).convert_enum_variant(s),
|
||||||
|
RenameVariant::Kebab => KebabCase(CharCase::Lower).convert_enum_variant(s),
|
||||||
|
RenameVariant::ScreamingKebab => KebabCase(CharCase::Upper).convert_enum_variant(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait WordAwareCase {
|
||||||
|
fn convert_enum_variant(&self, s: &str) -> String {
|
||||||
|
let mut converted = String::new();
|
||||||
|
let mut component = String::new();
|
||||||
|
let mut prev_case = Option::None;
|
||||||
|
|
||||||
|
for c in s.chars() {
|
||||||
|
let case = CharCase::of(c);
|
||||||
|
|
||||||
|
let (push_component, push_char) = {
|
||||||
|
if matches!((prev_case, case), (Some(CharCase::Lower), Some(CharCase::Upper))) {
|
||||||
|
(true, true)
|
||||||
|
} else if c == '_' {
|
||||||
|
(true, false)
|
||||||
|
} else {
|
||||||
|
(false, true)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if push_component && !component.is_empty() {
|
||||||
|
self.push_word(&mut converted, &component);
|
||||||
|
component.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if push_char {
|
||||||
|
component.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_case = case;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !component.is_empty() {
|
||||||
|
self.push_word(&mut converted, &component);
|
||||||
|
}
|
||||||
|
|
||||||
|
converted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_word(&self, buf: &mut String, word: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PascalCase;
|
||||||
|
|
||||||
|
impl WordAwareCase for PascalCase {
|
||||||
|
fn push_word(&self, buf: &mut String, word: &str) {
|
||||||
|
if let Some((head, tail)) = str_head_tail(word) {
|
||||||
|
buf.extend(head.to_uppercase());
|
||||||
|
buf.push_str(&tail.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CamelCase;
|
||||||
|
|
||||||
|
impl WordAwareCase for CamelCase {
|
||||||
|
fn push_word(&self, buf: &mut String, word: &str) {
|
||||||
|
if buf.is_empty() {
|
||||||
|
buf.push_str(&word.to_lowercase());
|
||||||
|
} else if let Some((head, tail)) = str_head_tail(word) {
|
||||||
|
buf.extend(head.to_uppercase());
|
||||||
|
buf.push_str(&tail.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnakeCase(CharCase);
|
||||||
|
|
||||||
|
impl WordAwareCase for SnakeCase {
|
||||||
|
fn push_word(&self, buf: &mut String, word: &str) {
|
||||||
|
if !buf.is_empty() {
|
||||||
|
buf.push('_');
|
||||||
|
}
|
||||||
|
buf.push_str(&self.0.convert(word));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KebabCase(CharCase);
|
||||||
|
|
||||||
|
impl WordAwareCase for KebabCase {
|
||||||
|
fn push_word(&self, buf: &mut String, word: &str) {
|
||||||
|
if !buf.is_empty() {
|
||||||
|
buf.push('-');
|
||||||
|
}
|
||||||
|
buf.push_str(&self.0.convert(word));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
enum CharCase {
|
||||||
|
Upper,
|
||||||
|
Lower,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CharCase {
|
||||||
|
fn of(c: char) -> Option<Self> {
|
||||||
|
if c.is_uppercase() {
|
||||||
|
Some(Self::Upper)
|
||||||
|
} else if c.is_lowercase() {
|
||||||
|
Some(Self::Lower)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert(self, s: &str) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Upper => s.to_uppercase(),
|
||||||
|
Self::Lower => s.to_lowercase(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_head_tail(s: &str) -> Option<(char, &str)> {
|
||||||
|
let head = s.chars().next()?;
|
||||||
|
let tail = &s[head.len_utf8()..];
|
||||||
|
Some((head, tail))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::{PascalCase, CamelCase, SnakeCase, KebabCase, CharCase, WordAwareCase};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pascal_case() {
|
||||||
|
assert_eq!(PascalCase.convert_enum_variant(""), "");
|
||||||
|
assert_eq!(PascalCase.convert_enum_variant("foo"), "Foo");
|
||||||
|
assert_eq!(PascalCase.convert_enum_variant("fooBaa"), "FooBaa");
|
||||||
|
assert_eq!(PascalCase.convert_enum_variant("FooBaa"), "FooBaa");
|
||||||
|
assert_eq!(PascalCase.convert_enum_variant("foo_baa"), "FooBaa");
|
||||||
|
assert_eq!(PascalCase.convert_enum_variant("FOO_BAA"), "FooBaa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_camel_case() {
|
||||||
|
assert_eq!(CamelCase.convert_enum_variant(""), "");
|
||||||
|
assert_eq!(CamelCase.convert_enum_variant("foo"), "foo");
|
||||||
|
assert_eq!(CamelCase.convert_enum_variant("fooBaa"), "fooBaa");
|
||||||
|
assert_eq!(CamelCase.convert_enum_variant("FooBaa"), "fooBaa");
|
||||||
|
assert_eq!(CamelCase.convert_enum_variant("foo_baa"), "fooBaa");
|
||||||
|
assert_eq!(CamelCase.convert_enum_variant("FOO_BAA"), "fooBaa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_snake_case() {
|
||||||
|
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant(""), "");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("foo"), "foo");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("fooBaa"), "foo_baa");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("FooBaa"), "foo_baa");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("foo_baa"), "foo_baa");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("FOO_BAA"), "foo_baa");
|
||||||
|
|
||||||
|
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant(""), "");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("foo"), "FOO");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("fooBaa"), "FOO_BAA");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("FooBaa"), "FOO_BAA");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("foo_baa"), "FOO_BAA");
|
||||||
|
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("FOO_BAA"), "FOO_BAA");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_kebab_case() {
|
||||||
|
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant(""), "");
|
||||||
|
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("foo"), "foo");
|
||||||
|
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("fooBaa"), "foo-baa");
|
||||||
|
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("FooBaa"), "foo-baa");
|
||||||
|
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("foo_baa"), "foo-baa");
|
||||||
|
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("FOO_BAA"), "foo-baa");
|
||||||
|
|
||||||
|
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant(""), "");
|
||||||
|
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("foo"), "FOO");
|
||||||
|
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("fooBaa"), "FOO-BAA");
|
||||||
|
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("FooBaa"), "FOO-BAA");
|
||||||
|
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("foo_baa"), "FOO-BAA");
|
||||||
|
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("FOO_BAA"), "FOO-BAA");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
use enumscribe::*;
|
||||||
|
|
||||||
|
#[derive(ScribeStaticStr, TryUnscribe, PartialEq, Eq, Debug)]
|
||||||
|
#[enumscribe(rename_all = "snake_case")]
|
||||||
|
enum Bird {
|
||||||
|
BlackRedstart,
|
||||||
|
#[enumscribe(case_insensitive)]
|
||||||
|
GardenWarbler,
|
||||||
|
#[enumscribe(rename = "SCREAMING-KEBAB-CASE")]
|
||||||
|
BarnacleGoose,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
assert_eq!(Bird::BlackRedstart.scribe(), "black_redstart");
|
||||||
|
assert_eq!(Bird::GardenWarbler.scribe(), "garden_warbler");
|
||||||
|
assert_eq!(Bird::BarnacleGoose.scribe(), "BARNACLE-GOOSE");
|
||||||
|
|
||||||
|
assert_eq!(Bird::try_unscribe("black_redstart").unwrap(), Bird::BlackRedstart);
|
||||||
|
assert_eq!(Bird::try_unscribe("gArDeN_wArBlEr").unwrap(), Bird::GardenWarbler);
|
||||||
|
assert_eq!(Bird::try_unscribe("BARNACLE-GOOSE").unwrap(), Bird::BarnacleGoose);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use enumscribe::EnumDeserialize;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize() {
|
||||||
|
#[derive(EnumDeserialize, Eq, PartialEq, Debug)]
|
||||||
|
enum E0 {
|
||||||
|
V0,
|
||||||
|
#[enumscribe(str = "baa", case_insensitive)]
|
||||||
|
V1,
|
||||||
|
#[enumscribe(str = "bAz\n", case_insensitive)]
|
||||||
|
V2,
|
||||||
|
#[enumscribe(str = "蟹")]
|
||||||
|
V3,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(serde_json::from_str::<E0>(r#""V0""#).unwrap(), E0::V0);
|
||||||
|
assert!(serde_json::from_str::<E0>(r#""v0""#).is_err());
|
||||||
|
assert_eq!(serde_json::from_str::<E0>(r#""baa""#).unwrap(), E0::V1);
|
||||||
|
assert_eq!(serde_json::from_str::<E0>(r#""BAA""#).unwrap(), E0::V1);
|
||||||
|
assert_eq!(serde_json::from_str::<E0>(r#""BaA""#).unwrap(), E0::V1);
|
||||||
|
assert_eq!(serde_json::from_str::<E0>(r#""baz\n""#).unwrap(), E0::V2);
|
||||||
|
assert_eq!(serde_json::from_str::<E0>(r#""BAZ\n""#).unwrap(), E0::V2);
|
||||||
|
assert_eq!(serde_json::from_str::<E0>(r#""BaZ\n""#).unwrap(), E0::V2);
|
||||||
|
assert_eq!(serde_json::from_str::<E0>(r#""\u87f9""#).unwrap(), E0::V3);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user