Skip to content

Commit

Permalink
Merge pull request sfackler#1008 from JaydenElliott/feature/rename_al…
Browse files Browse the repository at this point in the history
…l_attr

added `rename_all` container attribute for enums and structs
  • Loading branch information
sfackler authored Jun 10, 2023
2 parents 6f19bb9 + f4b181a commit 790af54
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 29 deletions.
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

0 comments on commit 790af54

Please sign in to comment.