Skip to content

Commit 790af54

Browse files
authored
Merge pull request sfackler#1008 from JaydenElliott/feature/rename_all_attr
added `rename_all` container attribute for enums and structs
2 parents 6f19bb9 + f4b181a commit 790af54

File tree

11 files changed

+293
-29
lines changed

11 files changed

+293
-29
lines changed

postgres-derive-test/src/composites.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,49 @@ fn name_overrides() {
8989
);
9090
}
9191

92+
#[test]
93+
fn rename_all_overrides() {
94+
#[derive(FromSql, ToSql, Debug, PartialEq)]
95+
#[postgres(name = "inventory_item", rename_all = "SCREAMING_SNAKE_CASE")]
96+
struct InventoryItem {
97+
name: String,
98+
supplier_id: i32,
99+
#[postgres(name = "Price")]
100+
price: Option<f64>,
101+
}
102+
103+
let mut conn = Client::connect("user=postgres host=localhost port=5433", NoTls).unwrap();
104+
conn.batch_execute(
105+
"CREATE TYPE pg_temp.inventory_item AS (
106+
\"NAME\" TEXT,
107+
\"SUPPLIER_ID\" INT,
108+
\"Price\" DOUBLE PRECISION
109+
);",
110+
)
111+
.unwrap();
112+
113+
let item = InventoryItem {
114+
name: "foobar".to_owned(),
115+
supplier_id: 100,
116+
price: Some(15.50),
117+
};
118+
119+
let item_null = InventoryItem {
120+
name: "foobar".to_owned(),
121+
supplier_id: 100,
122+
price: None,
123+
};
124+
125+
test_type(
126+
&mut conn,
127+
"inventory_item",
128+
&[
129+
(item, "ROW('foobar', 100, 15.50)"),
130+
(item_null, "ROW('foobar', 100, NULL)"),
131+
],
132+
);
133+
}
134+
92135
#[test]
93136
fn wrong_name() {
94137
#[derive(FromSql, ToSql, Debug, PartialEq)]

postgres-derive-test/src/enums.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,35 @@ fn name_overrides() {
5353
);
5454
}
5555

56+
#[test]
57+
fn rename_all_overrides() {
58+
#[derive(Debug, ToSql, FromSql, PartialEq)]
59+
#[postgres(name = "mood", rename_all = "snake_case")]
60+
enum Mood {
61+
VerySad,
62+
#[postgres(name = "okay")]
63+
Ok,
64+
VeryHappy,
65+
}
66+
67+
let mut conn = Client::connect("user=postgres host=localhost port=5433", NoTls).unwrap();
68+
conn.execute(
69+
"CREATE TYPE pg_temp.mood AS ENUM ('very_sad', 'okay', 'very_happy')",
70+
&[],
71+
)
72+
.unwrap();
73+
74+
test_type(
75+
&mut conn,
76+
"mood",
77+
&[
78+
(Mood::VerySad, "'very_sad'"),
79+
(Mood::Ok, "'okay'"),
80+
(Mood::VeryHappy, "'very_happy'"),
81+
],
82+
);
83+
}
84+
5685
#[test]
5786
fn wrong_name() {
5887
#[derive(Debug, ToSql, FromSql, PartialEq)]

postgres-derive/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ test = false
1515
syn = "2.0"
1616
proc-macro2 = "1.0"
1717
quote = "1.0"
18+
heck = "0.4"

postgres-derive/src/case.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#[allow(deprecated, unused_imports)]
2+
use std::ascii::AsciiExt;
3+
4+
use heck::{
5+
ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTrainCase,
6+
ToUpperCamelCase,
7+
};
8+
9+
use self::RenameRule::*;
10+
11+
/// The different possible ways to change case of fields in a struct, or variants in an enum.
12+
#[allow(clippy::enum_variant_names)]
13+
#[derive(Copy, Clone, PartialEq)]
14+
pub enum RenameRule {
15+
/// Rename direct children to "lowercase" style.
16+
LowerCase,
17+
/// Rename direct children to "UPPERCASE" style.
18+
UpperCase,
19+
/// Rename direct children to "PascalCase" style, as typically used for
20+
/// enum variants.
21+
PascalCase,
22+
/// Rename direct children to "camelCase" style.
23+
CamelCase,
24+
/// Rename direct children to "snake_case" style, as commonly used for
25+
/// fields.
26+
SnakeCase,
27+
/// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly
28+
/// used for constants.
29+
ScreamingSnakeCase,
30+
/// Rename direct children to "kebab-case" style.
31+
KebabCase,
32+
/// Rename direct children to "SCREAMING-KEBAB-CASE" style.
33+
ScreamingKebabCase,
34+
35+
/// Rename direct children to "Train-Case" style.
36+
TrainCase,
37+
}
38+
39+
pub const RENAME_RULES: &[&str] = &[
40+
"lowercase",
41+
"UPPERCASE",
42+
"PascalCase",
43+
"camelCase",
44+
"snake_case",
45+
"SCREAMING_SNAKE_CASE",
46+
"kebab-case",
47+
"SCREAMING-KEBAB-CASE",
48+
"Train-Case",
49+
];
50+
51+
impl RenameRule {
52+
pub fn from_str(rule: &str) -> Option<RenameRule> {
53+
match rule {
54+
"lowercase" => Some(LowerCase),
55+
"UPPERCASE" => Some(UpperCase),
56+
"PascalCase" => Some(PascalCase),
57+
"camelCase" => Some(CamelCase),
58+
"snake_case" => Some(SnakeCase),
59+
"SCREAMING_SNAKE_CASE" => Some(ScreamingSnakeCase),
60+
"kebab-case" => Some(KebabCase),
61+
"SCREAMING-KEBAB-CASE" => Some(ScreamingKebabCase),
62+
"Train-Case" => Some(TrainCase),
63+
_ => None,
64+
}
65+
}
66+
/// Apply a renaming rule to an enum or struct field, returning the version expected in the source.
67+
pub fn apply_to_field(&self, variant: &str) -> String {
68+
match *self {
69+
LowerCase => variant.to_lowercase(),
70+
UpperCase => variant.to_uppercase(),
71+
PascalCase => variant.to_upper_camel_case(),
72+
CamelCase => variant.to_lower_camel_case(),
73+
SnakeCase => variant.to_snake_case(),
74+
ScreamingSnakeCase => variant.to_shouty_snake_case(),
75+
KebabCase => variant.to_kebab_case(),
76+
ScreamingKebabCase => variant.to_shouty_kebab_case(),
77+
TrainCase => variant.to_train_case(),
78+
}
79+
}
80+
}
81+
82+
#[test]
83+
fn rename_field() {
84+
for &(original, lower, upper, camel, snake, screaming, kebab, screaming_kebab) in &[
85+
(
86+
"Outcome", "outcome", "OUTCOME", "outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
87+
),
88+
(
89+
"VeryTasty",
90+
"verytasty",
91+
"VERYTASTY",
92+
"veryTasty",
93+
"very_tasty",
94+
"VERY_TASTY",
95+
"very-tasty",
96+
"VERY-TASTY",
97+
),
98+
("A", "a", "A", "a", "a", "A", "a", "A"),
99+
("Z42", "z42", "Z42", "z42", "z42", "Z42", "z42", "Z42"),
100+
] {
101+
assert_eq!(LowerCase.apply_to_field(original), lower);
102+
assert_eq!(UpperCase.apply_to_field(original), upper);
103+
assert_eq!(PascalCase.apply_to_field(original), original);
104+
assert_eq!(CamelCase.apply_to_field(original), camel);
105+
assert_eq!(SnakeCase.apply_to_field(original), snake);
106+
assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming);
107+
assert_eq!(KebabCase.apply_to_field(original), kebab);
108+
assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab);
109+
}
110+
}

postgres-derive/src/composites.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use syn::{
44
TypeParamBound,
55
};
66

7-
use crate::overrides::Overrides;
7+
use crate::{case::RenameRule, overrides::Overrides};
88

99
pub struct Field {
1010
pub name: String,
@@ -13,18 +13,26 @@ pub struct Field {
1313
}
1414

1515
impl Field {
16-
pub fn parse(raw: &syn::Field) -> Result<Field, Error> {
17-
let overrides = Overrides::extract(&raw.attrs)?;
18-
16+
pub fn parse(raw: &syn::Field, rename_all: Option<RenameRule>) -> Result<Field, Error> {
17+
let overrides = Overrides::extract(&raw.attrs, false)?;
1918
let ident = raw.ident.as_ref().unwrap().clone();
20-
Ok(Field {
21-
name: overrides.name.unwrap_or_else(|| {
19+
20+
// field level name override takes precendence over container level rename_all override
21+
let name = match overrides.name {
22+
Some(n) => n,
23+
None => {
2224
let name = ident.to_string();
23-
match name.strip_prefix("r#") {
24-
Some(name) => name.to_string(),
25-
None => name,
25+
let stripped = name.strip_prefix("r#").map(String::from).unwrap_or(name);
26+
27+
match rename_all {
28+
Some(rule) => rule.apply_to_field(&stripped),
29+
None => stripped,
2630
}
27-
}),
31+
}
32+
};
33+
34+
Ok(Field {
35+
name,
2836
ident,
2937
type_: raw.ty.clone(),
3038
})

postgres-derive/src/enums.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use syn::{Error, Fields, Ident};
22

3-
use crate::overrides::Overrides;
3+
use crate::{case::RenameRule, overrides::Overrides};
44

55
pub struct Variant {
66
pub ident: Ident,
77
pub name: String,
88
}
99

1010
impl Variant {
11-
pub fn parse(raw: &syn::Variant) -> Result<Variant, Error> {
11+
pub fn parse(raw: &syn::Variant, rename_all: Option<RenameRule>) -> Result<Variant, Error> {
1212
match raw.fields {
1313
Fields::Unit => {}
1414
_ => {
@@ -18,11 +18,16 @@ impl Variant {
1818
))
1919
}
2020
}
21+
let overrides = Overrides::extract(&raw.attrs, false)?;
2122

22-
let overrides = Overrides::extract(&raw.attrs)?;
23+
// variant level name override takes precendence over container level rename_all override
24+
let name = overrides.name.unwrap_or_else(|| match rename_all {
25+
Some(rule) => rule.apply_to_field(&raw.ident.to_string()),
26+
None => raw.ident.to_string(),
27+
});
2328
Ok(Variant {
2429
ident: raw.ident.clone(),
25-
name: overrides.name.unwrap_or_else(|| raw.ident.to_string()),
30+
name,
2631
})
2732
}
2833
}

postgres-derive/src/fromsql.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@ use crate::enums::Variant;
1515
use crate::overrides::Overrides;
1616

1717
pub fn expand_derive_fromsql(input: DeriveInput) -> Result<TokenStream, Error> {
18-
let overrides = Overrides::extract(&input.attrs)?;
18+
let overrides = Overrides::extract(&input.attrs, true)?;
1919

20-
if overrides.name.is_some() && overrides.transparent {
20+
if (overrides.name.is_some() || overrides.rename_all.is_some()) && overrides.transparent {
2121
return Err(Error::new_spanned(
2222
&input,
23-
"#[postgres(transparent)] is not allowed with #[postgres(name = \"...\")]",
23+
"#[postgres(transparent)] is not allowed with #[postgres(name = \"...\")] or #[postgres(rename_all = \"...\")]",
2424
));
2525
}
2626

27-
let name = overrides.name.unwrap_or_else(|| input.ident.to_string());
27+
let name = overrides
28+
.name
29+
.clone()
30+
.unwrap_or_else(|| input.ident.to_string());
2831

2932
let (accepts_body, to_sql_body) = if overrides.transparent {
3033
match input.data {
@@ -51,7 +54,7 @@ pub fn expand_derive_fromsql(input: DeriveInput) -> Result<TokenStream, Error> {
5154
let variants = data
5255
.variants
5356
.iter()
54-
.map(Variant::parse)
57+
.map(|variant| Variant::parse(variant, overrides.rename_all))
5558
.collect::<Result<Vec<_>, _>>()?;
5659
(
5760
accepts::enum_body(&name, &variants),
@@ -75,7 +78,7 @@ pub fn expand_derive_fromsql(input: DeriveInput) -> Result<TokenStream, Error> {
7578
let fields = fields
7679
.named
7780
.iter()
78-
.map(Field::parse)
81+
.map(|field| Field::parse(field, overrides.rename_all))
7982
.collect::<Result<Vec<_>, _>>()?;
8083
(
8184
accepts::composite_body(&name, "FromSql", &fields),

postgres-derive/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use proc_macro::TokenStream;
77
use syn::parse_macro_input;
88

99
mod accepts;
10+
mod case;
1011
mod composites;
1112
mod enums;
1213
mod fromsql;

postgres-derive/src/overrides.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use syn::punctuated::Punctuated;
22
use syn::{Attribute, Error, Expr, ExprLit, Lit, Meta, Token};
33

4+
use crate::case::{RenameRule, RENAME_RULES};
5+
46
pub struct Overrides {
57
pub name: Option<String>,
8+
pub rename_all: Option<RenameRule>,
69
pub transparent: bool,
710
}
811

912
impl Overrides {
10-
pub fn extract(attrs: &[Attribute]) -> Result<Overrides, Error> {
13+
pub fn extract(attrs: &[Attribute], container_attr: bool) -> Result<Overrides, Error> {
1114
let mut overrides = Overrides {
1215
name: None,
16+
rename_all: None,
1317
transparent: false,
1418
};
1519

@@ -28,7 +32,15 @@ impl Overrides {
2832
for item in nested {
2933
match item {
3034
Meta::NameValue(meta) => {
31-
if !meta.path.is_ident("name") {
35+
let name_override = meta.path.is_ident("name");
36+
let rename_all_override = meta.path.is_ident("rename_all");
37+
if !container_attr && rename_all_override {
38+
return Err(Error::new_spanned(
39+
&meta.path,
40+
"rename_all is a container attribute",
41+
));
42+
}
43+
if !name_override && !rename_all_override {
3244
return Err(Error::new_spanned(&meta.path, "unknown override"));
3345
}
3446

@@ -41,7 +53,25 @@ impl Overrides {
4153
}
4254
};
4355

44-
overrides.name = Some(value);
56+
if name_override {
57+
overrides.name = Some(value);
58+
} else if rename_all_override {
59+
let rename_rule = RenameRule::from_str(&value).ok_or_else(|| {
60+
Error::new_spanned(
61+
&meta.value,
62+
format!(
63+
"invalid rename_all rule, expected one of: {}",
64+
RENAME_RULES
65+
.iter()
66+
.map(|rule| format!("\"{}\"", rule))
67+
.collect::<Vec<_>>()
68+
.join(", ")
69+
),
70+
)
71+
})?;
72+
73+
overrides.rename_all = Some(rename_rule);
74+
}
4575
}
4676
Meta::Path(path) => {
4777
if !path.is_ident("transparent") {

0 commit comments

Comments
 (0)