diff --git a/CHANGELOG.md b/CHANGELOG.md index 40fe92a9..5f1ec799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,14 @@ ### Breaking +- Fix incompatibility with serde for `snake_case`, `kebab-case` and `SCREAMING_SNAKE_CASE` ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298)) +- `#[ts(rename_all = "...")]` no longer accepts variations in the string's casing, dashes and underscores to make behavior consistent with serde ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298)) + ### Features - Add support for `#[ts(type = "..")]` directly on structs and enums ([#286](https://github.com/Aleph-Alpha/ts-rs/pull/286)) - Add support for `#[ts(as = "..")]` directly on structs and enums ([#288](https://github.com/Aleph-Alpha/ts-rs/pull/288)) +- Add support for `#[ts(rename_all = "SCREAMING-KEBAB-CASE")]` ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298)) ### Fixes diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 921ddcc8..7373256d 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -19,5 +19,4 @@ proc-macro = true proc-macro2 = "1" quote = "1" syn = { version = "2.0.28", features = ["full", "extra-traits"] } -Inflector = { version = "0.11", default-features = false } termcolor = { version = "1", optional = true } diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs index 2e0931fe..d5a84b6a 100644 --- a/macros/src/attr/mod.rs +++ b/macros/src/attr/mod.rs @@ -24,6 +24,7 @@ pub enum Inflection { Pascal, ScreamingSnake, Kebab, + ScreamingKebab, } pub(super) trait Attr: Default { @@ -55,8 +56,6 @@ where impl Inflection { pub fn apply(self, string: &str) -> String { - use inflector::Inflector; - match self { Inflection::Lower => string.to_lowercase(), Inflection::Upper => string.to_uppercase(), @@ -64,7 +63,18 @@ impl Inflection { let pascal = Inflection::apply(Inflection::Pascal, string); pascal[..1].to_ascii_lowercase() + &pascal[1..] } - Inflection::Snake => string.to_snake_case(), + Inflection::Snake => { + let mut s = String::new(); + + for (i, ch) in string.char_indices() { + if ch.is_uppercase() && i != 0 { + s.push('_'); + } + s.push(ch.to_ascii_lowercase()); + } + + s + } Inflection::Pascal => { let mut s = String::with_capacity(string.len()); @@ -83,29 +93,13 @@ impl Inflection { s } - Inflection::ScreamingSnake => string.to_screaming_snake_case(), - Inflection::Kebab => string.to_kebab_case(), + Inflection::ScreamingSnake => Self::Snake.apply(string).to_ascii_uppercase(), + Inflection::Kebab => Self::Snake.apply(string).replace('_', "-"), + Inflection::ScreamingKebab => Self::Kebab.apply(string).to_ascii_uppercase(), } } } -impl TryFrom for Inflection { - type Error = Error; - - fn try_from(value: String) -> Result { - Ok(match &*value.to_lowercase().replace(['_', '-'], "") { - "lowercase" => Self::Lower, - "uppercase" => Self::Upper, - "camelcase" => Self::Camel, - "snakecase" => Self::Snake, - "pascalcase" => Self::Pascal, - "screamingsnakecase" => Self::ScreamingSnake, - "kebabcase" => Self::Kebab, - _ => syn_err!("invalid inflection: '{}'", value), - }) - } -} - fn parse_assign_str(input: ParseStream) -> Result { input.parse::()?; match Lit::parse(input)? { @@ -143,7 +137,25 @@ fn parse_concrete(input: ParseStream) -> Result> } fn parse_assign_inflection(input: ParseStream) -> Result { - parse_assign_str(input).and_then(Inflection::try_from) + let span = input.span(); + let str = parse_assign_str(input)?; + + Ok(match &*str { + "lowercase" => Inflection::Lower, + "UPPERCASE" => Inflection::Upper, + "camelCase" => Inflection::Camel, + "snake_case" => Inflection::Snake, + "PascalCase" => Inflection::Pascal, + "SCREAMING_SNAKE_CASE" => Inflection::ScreamingSnake, + "kebab-case" => Inflection::Kebab, + "SCREAMING-KEBAB-CASE" => Inflection::ScreamingKebab, + other => { + syn_err!( + span; + r#"Value "{other}" is not valid for "rename_all". Accepted values are: "lowercase", "UPPERCASE", "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE", "kebab-case" and "SCREAMING-KEBAB-CASE""# + ) + } + }) } fn parse_assign_from_str(input: ParseStream) -> Result diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs index 937c817c..69a4b067 100644 --- a/macros/src/attr/struct.rs +++ b/macros/src/attr/struct.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; use syn::{parse_quote, Attribute, Fields, Ident, Path, Result, Type, WherePredicate}; -use super::{parse_assign_from_str, parse_bound, parse_concrete, Attr, ContainerAttr, Serde}; +use super::{ + parse_assign_from_str, parse_assign_inflection, parse_bound, parse_concrete, Attr, + ContainerAttr, Serde, +}; use crate::{ attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr}, utils::{parse_attrs, parse_docs}, @@ -133,7 +136,7 @@ impl_parse! { "as" => out.type_as = Some(parse_assign_from_str(input)?), "type" => out.type_override = Some(parse_assign_str(input)?), "rename" => out.rename = Some(parse_assign_str(input)?), - "rename_all" => out.rename_all = Some(parse_assign_str(input).and_then(Inflection::try_from)?), + "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), "tag" => out.tag = Some(parse_assign_str(input)?), "export" => out.export = true, "export_to" => out.export_to = Some(parse_assign_str(input)?), @@ -146,7 +149,7 @@ impl_parse! { impl_parse! { Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), - "rename_all" => out.0.rename_all = Some(parse_assign_str(input).and_then(Inflection::try_from)?), + "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?), "tag" => out.0.tag = Some(parse_assign_str(input)?), "bound" => out.0.bound = Some(parse_bound(input)?), // parse #[serde(default)] to not emit a warning diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index 158d4efa..a4687ba8 100644 --- a/ts-rs/Cargo.toml +++ b/ts-rs/Cargo.toml @@ -37,12 +37,13 @@ import-esm = [] [dev-dependencies] serde = { version = "1.0", features = ["derive"] } +serde_json = "1" chrono = { version = "0.4", features = ["serde"] } [dependencies] heapless = { version = ">= 0.7, < 0.9", optional = true } ts-rs-macros = { version = "=8.1.0", path = "../macros" } -dprint-plugin-typescript = { version = "0.89", optional = true } +dprint-plugin-typescript = { version = "0.90", optional = true } chrono = { version = "0.4", optional = true } bigdecimal = { version = ">= 0.0.13, < 0.5", features = [ "serde", diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 3c31eaf8..0954e697 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -215,7 +215,7 @@ pub mod typelist; /// /// - **`#[ts(rename_all = "..")]`** /// Rename all fields/variants of the type. -/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" +/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE" ///

/// /// - **`#[ts(concrete(..)]`** @@ -331,13 +331,13 @@ pub mod typelist; /// /// - **`#[ts(rename_all = "..")]`** /// Rename all variants of this enum. -/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" +/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE" ///

/// /// - **`#[ts(rename_all_fieds = "..")]`** /// Renames the fields of all the struct variants of this enum. This is equivalent to using /// `#[ts(rename_all = "..")]` on all of the enum's variants. -/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" +/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE" ///

/// /// ### enum variant attributes @@ -357,7 +357,7 @@ pub mod typelist; /// /// - **`#[ts(rename_all = "..")]`** /// Renames all the fields of a struct variant. -/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" +/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE" ///

pub trait TS { /// If this type does not have generic parameters, then `WithoutGenerics` should just be `Self`. diff --git a/ts-rs/tests/struct_rename.rs b/ts-rs/tests/struct_rename.rs index ef4ed774..9536fcaa 100644 --- a/ts-rs/tests/struct_rename.rs +++ b/ts-rs/tests/struct_rename.rs @@ -46,6 +46,29 @@ fn rename_all_pascal_case() { ); } +#[derive(TS, Default, serde::Serialize)] +#[ts(export, export_to = "struct_rename/")] +#[cfg_attr(feature = "serde-compat", serde(rename_all = "SCREAMING-KEBAB-CASE"))] +#[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "SCREAMING-KEBAB-CASE"))] +struct RenameAllScreamingKebab { + crc32c_hash: i32, + some_field: i32, + some_other_field: i32, +} + +#[test] +fn rename_all_screaming_kebab_case() { + let rename_all = RenameAllScreamingKebab::default(); + assert_eq!( + serde_json::to_string(&rename_all).unwrap(), + r#"{"CRC32C-HASH":0,"SOME-FIELD":0,"SOME-OTHER-FIELD":0}"# + ); + assert_eq!( + RenameAllScreamingKebab::inline(), + r#"{ "CRC32C-HASH": number, "SOME-FIELD": number, "SOME-OTHER-FIELD": number, }"# + ); +} + #[derive(serde::Serialize, TS)] #[ts(export, export_to = "struct_rename/", rename_all = "camelCase")] struct RenameSerdeSpecialChar {