Skip to content

added rename_all container attribute for enums and structs #1008

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

Merged
merged 4 commits into from
Jun 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions postgres-derive-test/src/composites.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,49 @@ fn name_overrides() {
);
}

#[test]
fn rename_all_overrides() {
#[derive(FromSql, ToSql, Debug, PartialEq)]
#[postgres(name = "inventory_item", rename_all = "SCREAMING_SNAKE_CASE")]
struct InventoryItem {
name: String,
supplier_id: i32,
#[postgres(name = "Price")]
price: Option<f64>,
}

let mut conn = Client::connect("user=postgres host=localhost port=5433", NoTls).unwrap();
conn.batch_execute(
"CREATE TYPE pg_temp.inventory_item AS (
\"NAME\" TEXT,
\"SUPPLIER_ID\" INT,
\"Price\" DOUBLE PRECISION
);",
)
.unwrap();

let item = InventoryItem {
name: "foobar".to_owned(),
supplier_id: 100,
price: Some(15.50),
};

let item_null = InventoryItem {
name: "foobar".to_owned(),
supplier_id: 100,
price: None,
};

test_type(
&mut conn,
"inventory_item",
&[
(item, "ROW('foobar', 100, 15.50)"),
(item_null, "ROW('foobar', 100, NULL)"),
],
);
}

#[test]
fn wrong_name() {
#[derive(FromSql, ToSql, Debug, PartialEq)]
Expand Down
29 changes: 29 additions & 0 deletions postgres-derive-test/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,35 @@ fn name_overrides() {
);
}

#[test]
fn rename_all_overrides() {
#[derive(Debug, ToSql, FromSql, PartialEq)]
#[postgres(name = "mood", rename_all = "snake_case")]
enum Mood {
VerySad,
#[postgres(name = "okay")]
Ok,
VeryHappy,
}

let mut conn = Client::connect("user=postgres host=localhost port=5433", NoTls).unwrap();
conn.execute(
"CREATE TYPE pg_temp.mood AS ENUM ('very_sad', 'okay', 'very_happy')",
&[],
)
.unwrap();

test_type(
&mut conn,
"mood",
&[
(Mood::VerySad, "'very_sad'"),
(Mood::Ok, "'okay'"),
(Mood::VeryHappy, "'very_happy'"),
],
);
}

#[test]
fn wrong_name() {
#[derive(Debug, ToSql, FromSql, PartialEq)]
Expand Down
1 change: 1 addition & 0 deletions postgres-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ test = false
syn = "2.0"
proc-macro2 = "1.0"
quote = "1.0"
heck = "0.4"
110 changes: 110 additions & 0 deletions postgres-derive/src/case.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#[allow(deprecated, unused_imports)]
use std::ascii::AsciiExt;

use heck::{
ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTrainCase,
ToUpperCamelCase,
};

use self::RenameRule::*;

/// The different possible ways to change case of fields in a struct, or variants in an enum.
#[allow(clippy::enum_variant_names)]
#[derive(Copy, Clone, PartialEq)]
pub enum RenameRule {
/// Rename direct children to "lowercase" style.
LowerCase,
/// Rename direct children to "UPPERCASE" style.
UpperCase,
/// Rename direct children to "PascalCase" style, as typically used for
/// enum variants.
PascalCase,
/// Rename direct children to "camelCase" style.
CamelCase,
/// Rename direct children to "snake_case" style, as commonly used for
/// fields.
SnakeCase,
/// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly
/// used for constants.
ScreamingSnakeCase,
/// Rename direct children to "kebab-case" style.
KebabCase,
/// Rename direct children to "SCREAMING-KEBAB-CASE" style.
ScreamingKebabCase,

/// Rename direct children to "Train-Case" style.
TrainCase,
}

pub const RENAME_RULES: &[&str] = &[
"lowercase",
"UPPERCASE",
"PascalCase",
"camelCase",
"snake_case",
"SCREAMING_SNAKE_CASE",
"kebab-case",
"SCREAMING-KEBAB-CASE",
"Train-Case",
];

impl RenameRule {
pub fn from_str(rule: &str) -> Option<RenameRule> {
match rule {
"lowercase" => Some(LowerCase),
"UPPERCASE" => Some(UpperCase),
"PascalCase" => Some(PascalCase),
"camelCase" => Some(CamelCase),
"snake_case" => Some(SnakeCase),
"SCREAMING_SNAKE_CASE" => Some(ScreamingSnakeCase),
"kebab-case" => Some(KebabCase),
"SCREAMING-KEBAB-CASE" => Some(ScreamingKebabCase),
"Train-Case" => Some(TrainCase),
_ => None,
}
}
/// Apply a renaming rule to an enum or struct field, returning the version expected in the source.
pub fn apply_to_field(&self, variant: &str) -> String {
match *self {
LowerCase => variant.to_lowercase(),
UpperCase => variant.to_uppercase(),
PascalCase => variant.to_upper_camel_case(),
CamelCase => variant.to_lower_camel_case(),
SnakeCase => variant.to_snake_case(),
ScreamingSnakeCase => variant.to_shouty_snake_case(),
KebabCase => variant.to_kebab_case(),
ScreamingKebabCase => variant.to_shouty_kebab_case(),
TrainCase => variant.to_train_case(),
}
}
}

#[test]
fn rename_field() {
for &(original, lower, upper, camel, snake, screaming, kebab, screaming_kebab) in &[
(
"Outcome", "outcome", "OUTCOME", "outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
),
(
"VeryTasty",
"verytasty",
"VERYTASTY",
"veryTasty",
"very_tasty",
"VERY_TASTY",
"very-tasty",
"VERY-TASTY",
),
("A", "a", "A", "a", "a", "A", "a", "A"),
("Z42", "z42", "Z42", "z42", "z42", "Z42", "z42", "Z42"),
] {
assert_eq!(LowerCase.apply_to_field(original), lower);
assert_eq!(UpperCase.apply_to_field(original), upper);
assert_eq!(PascalCase.apply_to_field(original), original);
assert_eq!(CamelCase.apply_to_field(original), camel);
assert_eq!(SnakeCase.apply_to_field(original), snake);
assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming);
assert_eq!(KebabCase.apply_to_field(original), kebab);
assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab);
}
}
28 changes: 18 additions & 10 deletions postgres-derive/src/composites.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use syn::{
TypeParamBound,
};

use crate::overrides::Overrides;
use crate::{case::RenameRule, overrides::Overrides};

pub struct Field {
pub name: String,
Expand All @@ -13,18 +13,26 @@ pub struct Field {
}

impl Field {
pub fn parse(raw: &syn::Field) -> Result<Field, Error> {
let overrides = Overrides::extract(&raw.attrs)?;

pub fn parse(raw: &syn::Field, rename_all: Option<RenameRule>) -> Result<Field, Error> {
let overrides = Overrides::extract(&raw.attrs, false)?;
let ident = raw.ident.as_ref().unwrap().clone();
Ok(Field {
name: overrides.name.unwrap_or_else(|| {

// field level name override takes precendence over container level rename_all override
let name = match overrides.name {
Some(n) => n,
None => {
let name = ident.to_string();
match name.strip_prefix("r#") {
Some(name) => name.to_string(),
None => name,
let stripped = name.strip_prefix("r#").map(String::from).unwrap_or(name);

match rename_all {
Some(rule) => rule.apply_to_field(&stripped),
None => stripped,
}
}),
}
};

Ok(Field {
name,
ident,
type_: raw.ty.clone(),
})
Expand Down
13 changes: 9 additions & 4 deletions postgres-derive/src/enums.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use syn::{Error, Fields, Ident};

use crate::overrides::Overrides;
use crate::{case::RenameRule, overrides::Overrides};

pub struct Variant {
pub ident: Ident,
pub name: String,
}

impl Variant {
pub fn parse(raw: &syn::Variant) -> Result<Variant, Error> {
pub fn parse(raw: &syn::Variant, rename_all: Option<RenameRule>) -> Result<Variant, Error> {
match raw.fields {
Fields::Unit => {}
_ => {
Expand All @@ -18,11 +18,16 @@ impl Variant {
))
}
}
let overrides = Overrides::extract(&raw.attrs, false)?;

let overrides = Overrides::extract(&raw.attrs)?;
// variant level name override takes precendence over container level rename_all override
let name = overrides.name.unwrap_or_else(|| match rename_all {
Some(rule) => rule.apply_to_field(&raw.ident.to_string()),
None => raw.ident.to_string(),
});
Ok(Variant {
ident: raw.ident.clone(),
name: overrides.name.unwrap_or_else(|| raw.ident.to_string()),
name,
})
}
}
15 changes: 9 additions & 6 deletions postgres-derive/src/fromsql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ use crate::enums::Variant;
use crate::overrides::Overrides;

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

if overrides.name.is_some() && overrides.transparent {
if (overrides.name.is_some() || overrides.rename_all.is_some()) && overrides.transparent {
return Err(Error::new_spanned(
&input,
"#[postgres(transparent)] is not allowed with #[postgres(name = \"...\")]",
"#[postgres(transparent)] is not allowed with #[postgres(name = \"...\")] or #[postgres(rename_all = \"...\")]",
));
}

let name = overrides.name.unwrap_or_else(|| input.ident.to_string());
let name = overrides
.name
.clone()
.unwrap_or_else(|| input.ident.to_string());

let (accepts_body, to_sql_body) = if overrides.transparent {
match input.data {
Expand All @@ -51,7 +54,7 @@ pub fn expand_derive_fromsql(input: DeriveInput) -> Result<TokenStream, Error> {
let variants = data
.variants
.iter()
.map(Variant::parse)
.map(|variant| Variant::parse(variant, overrides.rename_all))
.collect::<Result<Vec<_>, _>>()?;
(
accepts::enum_body(&name, &variants),
Expand All @@ -75,7 +78,7 @@ pub fn expand_derive_fromsql(input: DeriveInput) -> Result<TokenStream, Error> {
let fields = fields
.named
.iter()
.map(Field::parse)
.map(|field| Field::parse(field, overrides.rename_all))
.collect::<Result<Vec<_>, _>>()?;
(
accepts::composite_body(&name, "FromSql", &fields),
Expand Down
1 change: 1 addition & 0 deletions postgres-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use proc_macro::TokenStream;
use syn::parse_macro_input;

mod accepts;
mod case;
mod composites;
mod enums;
mod fromsql;
Expand Down
36 changes: 33 additions & 3 deletions postgres-derive/src/overrides.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use syn::punctuated::Punctuated;
use syn::{Attribute, Error, Expr, ExprLit, Lit, Meta, Token};

use crate::case::{RenameRule, RENAME_RULES};

pub struct Overrides {
pub name: Option<String>,
pub rename_all: Option<RenameRule>,
pub transparent: bool,
}

impl Overrides {
pub fn extract(attrs: &[Attribute]) -> Result<Overrides, Error> {
pub fn extract(attrs: &[Attribute], container_attr: bool) -> Result<Overrides, Error> {
let mut overrides = Overrides {
name: None,
rename_all: None,
transparent: false,
};

Expand All @@ -28,7 +32,15 @@ impl Overrides {
for item in nested {
match item {
Meta::NameValue(meta) => {
if !meta.path.is_ident("name") {
let name_override = meta.path.is_ident("name");
let rename_all_override = meta.path.is_ident("rename_all");
if !container_attr && rename_all_override {
return Err(Error::new_spanned(
&meta.path,
"rename_all is a container attribute",
));
}
if !name_override && !rename_all_override {
return Err(Error::new_spanned(&meta.path, "unknown override"));
}

Expand All @@ -41,7 +53,25 @@ impl Overrides {
}
};

overrides.name = Some(value);
if name_override {
overrides.name = Some(value);
} else if rename_all_override {
let rename_rule = RenameRule::from_str(&value).ok_or_else(|| {
Error::new_spanned(
&meta.value,
format!(
"invalid rename_all rule, expected one of: {}",
RENAME_RULES
.iter()
.map(|rule| format!("\"{}\"", rule))
.collect::<Vec<_>>()
.join(", ")
),
)
})?;

overrides.rename_all = Some(rename_rule);
}
}
Meta::Path(path) => {
if !path.is_ident("transparent") {
Expand Down
Loading