Skip to content

Commit

Permalink
Fix inconsistencies with serde and add SCREAMING-KEBAB-CASE (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
escritorio-gustavo committed Apr 9, 2024
1 parent 17b2c29 commit 1079b99
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 32 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
58 changes: 35 additions & 23 deletions macros/src/attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub enum Inflection {
Pascal,
ScreamingSnake,
Kebab,
ScreamingKebab,
}

pub(super) trait Attr: Default {
Expand Down Expand Up @@ -55,16 +56,25 @@ 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(),
Inflection::Camel => {
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());

Expand All @@ -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<String> for Inflection {
type Error = Error;

fn try_from(value: String) -> Result<Self> {
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<String> {
input.parse::<Token![=]>()?;
match Lit::parse(input)? {
Expand Down Expand Up @@ -143,7 +137,25 @@ fn parse_concrete(input: ParseStream) -> Result<HashMap<syn::Ident, syn::Type>>
}

fn parse_assign_inflection(input: ParseStream) -> Result<Inflection> {
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<T>(input: ParseStream) -> Result<T>
Expand Down
9 changes: 6 additions & 3 deletions macros/src/attr/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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)?),
Expand All @@ -146,7 +149,7 @@ impl_parse! {
impl_parse! {
Serde<StructAttr>(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
Expand Down
3 changes: 2 additions & 1 deletion ts-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/// <br/><br/>
///
/// - **`#[ts(concrete(..)]`**
Expand Down Expand Up @@ -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"
/// <br/><br/>
///
/// - **`#[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"
/// <br/><br/>
///
/// ### enum variant attributes
Expand All @@ -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"
/// <br/><br/>
pub trait TS {
/// If this type does not have generic parameters, then `WithoutGenerics` should just be `Self`.
Expand Down
23 changes: 23 additions & 0 deletions ts-rs/tests/struct_rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 1079b99

Please sign in to comment.