added a rename_all container attribute for enums and structs
This commit is contained in:
parent
70616712a4
commit
071dfa3f3b
@ -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)]
|
||||
|
@ -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 {
|
||||
Sad,
|
||||
#[postgres(name = "okay")]
|
||||
Ok,
|
||||
Happy,
|
||||
}
|
||||
|
||||
let mut conn = Client::connect("user=postgres host=localhost port=5433", NoTls).unwrap();
|
||||
conn.execute(
|
||||
"CREATE TYPE pg_temp.mood AS ENUM ('sad', 'okay', 'happy')",
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
test_type(
|
||||
&mut conn,
|
||||
"mood",
|
||||
&[
|
||||
(Mood::Sad, "'sad'"),
|
||||
(Mood::Ok, "'okay'"),
|
||||
(Mood::Happy, "'happy'"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_name() {
|
||||
#[derive(Debug, ToSql, FromSql, PartialEq)]
|
||||
|
158
postgres-derive/src/case.rs
Normal file
158
postgres-derive/src/case.rs
Normal file
@ -0,0 +1,158 @@
|
||||
#[allow(deprecated, unused_imports)]
|
||||
use std::ascii::AsciiExt;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
pub static RENAME_RULES: &[(&str, RenameRule)] = &[
|
||||
("lowercase", LowerCase),
|
||||
("UPPERCASE", UpperCase),
|
||||
("PascalCase", PascalCase),
|
||||
("camelCase", CamelCase),
|
||||
("snake_case", SnakeCase),
|
||||
("SCREAMING_SNAKE_CASE", ScreamingSnakeCase),
|
||||
("kebab-case", KebabCase),
|
||||
("SCREAMING-KEBAB-CASE", ScreamingKebabCase),
|
||||
];
|
||||
|
||||
impl RenameRule {
|
||||
/// Apply a renaming rule to an enum variant, returning the version expected in the source.
|
||||
pub fn apply_to_variant(&self, variant: &str) -> String {
|
||||
match *self {
|
||||
PascalCase => variant.to_owned(),
|
||||
LowerCase => variant.to_ascii_lowercase(),
|
||||
UpperCase => variant.to_ascii_uppercase(),
|
||||
CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..],
|
||||
SnakeCase => {
|
||||
let mut snake = String::new();
|
||||
for (i, ch) in variant.char_indices() {
|
||||
if i > 0 && ch.is_uppercase() {
|
||||
snake.push('_');
|
||||
}
|
||||
snake.push(ch.to_ascii_lowercase());
|
||||
}
|
||||
snake
|
||||
}
|
||||
ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
|
||||
KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
|
||||
ScreamingKebabCase => ScreamingSnakeCase
|
||||
.apply_to_variant(variant)
|
||||
.replace('_', "-"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a renaming rule to a struct field, returning the version expected in the source.
|
||||
pub fn apply_to_field(&self, field: &str) -> String {
|
||||
match *self {
|
||||
LowerCase | SnakeCase => field.to_owned(),
|
||||
UpperCase => field.to_ascii_uppercase(),
|
||||
PascalCase => {
|
||||
let mut pascal = String::new();
|
||||
let mut capitalize = true;
|
||||
for ch in field.chars() {
|
||||
if ch == '_' {
|
||||
capitalize = true;
|
||||
} else if capitalize {
|
||||
pascal.push(ch.to_ascii_uppercase());
|
||||
capitalize = false;
|
||||
} else {
|
||||
pascal.push(ch);
|
||||
}
|
||||
}
|
||||
pascal
|
||||
}
|
||||
CamelCase => {
|
||||
let pascal = PascalCase.apply_to_field(field);
|
||||
pascal[..1].to_ascii_lowercase() + &pascal[1..]
|
||||
}
|
||||
ScreamingSnakeCase => field.to_ascii_uppercase(),
|
||||
KebabCase => field.replace('_', "-"),
|
||||
ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_variants() {
|
||||
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_variant(original), lower);
|
||||
assert_eq!(UpperCase.apply_to_variant(original), upper);
|
||||
assert_eq!(PascalCase.apply_to_variant(original), original);
|
||||
assert_eq!(CamelCase.apply_to_variant(original), camel);
|
||||
assert_eq!(SnakeCase.apply_to_variant(original), snake);
|
||||
assert_eq!(ScreamingSnakeCase.apply_to_variant(original), screaming);
|
||||
assert_eq!(KebabCase.apply_to_variant(original), kebab);
|
||||
assert_eq!(
|
||||
ScreamingKebabCase.apply_to_variant(original),
|
||||
screaming_kebab
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_fields() {
|
||||
for &(original, upper, pascal, camel, screaming, kebab, screaming_kebab) in &[
|
||||
(
|
||||
"outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
|
||||
),
|
||||
(
|
||||
"very_tasty",
|
||||
"VERY_TASTY",
|
||||
"VeryTasty",
|
||||
"veryTasty",
|
||||
"VERY_TASTY",
|
||||
"very-tasty",
|
||||
"VERY-TASTY",
|
||||
),
|
||||
("a", "A", "A", "a", "A", "a", "A"),
|
||||
("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"),
|
||||
] {
|
||||
assert_eq!(UpperCase.apply_to_field(original), upper);
|
||||
assert_eq!(PascalCase.apply_to_field(original), pascal);
|
||||
assert_eq!(CamelCase.apply_to_field(original), camel);
|
||||
assert_eq!(SnakeCase.apply_to_field(original), original);
|
||||
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);
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ use syn::{
|
||||
TypeParamBound,
|
||||
};
|
||||
|
||||
use crate::overrides::Overrides;
|
||||
use crate::{case::RenameRule, overrides::Overrides};
|
||||
|
||||
pub struct Field {
|
||||
pub name: String,
|
||||
@ -13,18 +13,26 @@ pub struct Field {
|
||||
}
|
||||
|
||||
impl Field {
|
||||
pub fn parse(raw: &syn::Field) -> Result<Field, Error> {
|
||||
pub fn parse(raw: &syn::Field, rename_all: Option<RenameRule>) -> Result<Field, Error> {
|
||||
let overrides = Overrides::extract(&raw.attrs)?;
|
||||
|
||||
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(),
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
use syn::{Error, Fields, Ident};
|
||||
|
||||
use crate::overrides::Overrides;
|
||||
use crate::{case::RenameRule, overrides::Overrides};
|
||||
|
||||
pub struct Variant {
|
||||
pub ident: Ident,
|
||||
@ -8,7 +8,7 @@ pub struct Variant {
|
||||
}
|
||||
|
||||
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 => {}
|
||||
_ => {
|
||||
@ -18,11 +18,16 @@ impl Variant {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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_variant(&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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,10 @@ pub fn expand_derive_fromsql(input: DeriveInput) -> Result<TokenStream, Error> {
|
||||
));
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -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),
|
||||
@ -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),
|
||||
|
@ -7,6 +7,7 @@ use proc_macro::TokenStream;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
mod accepts;
|
||||
mod case;
|
||||
mod composites;
|
||||
mod enums;
|
||||
mod fromsql;
|
||||
|
@ -1,8 +1,11 @@
|
||||
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,
|
||||
}
|
||||
|
||||
@ -10,6 +13,7 @@ impl Overrides {
|
||||
pub fn extract(attrs: &[Attribute]) -> Result<Overrides, Error> {
|
||||
let mut overrides = Overrides {
|
||||
name: None,
|
||||
rename_all: None,
|
||||
transparent: false,
|
||||
};
|
||||
|
||||
@ -28,7 +32,9 @@ 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 !name_override && !rename_all_override {
|
||||
return Err(Error::new_spanned(&meta.path, "unknown override"));
|
||||
}
|
||||
|
||||
@ -41,7 +47,29 @@ impl Overrides {
|
||||
}
|
||||
};
|
||||
|
||||
if name_override {
|
||||
overrides.name = Some(value);
|
||||
} else if rename_all_override {
|
||||
let rename_rule = RENAME_RULES
|
||||
.iter()
|
||||
.find(|rule| rule.0 == value)
|
||||
.map(|val| val.1)
|
||||
.ok_or_else(|| {
|
||||
Error::new_spanned(
|
||||
&meta.value,
|
||||
format!(
|
||||
"invalid rename_all rule, expected one of: {}",
|
||||
RENAME_RULES
|
||||
.iter()
|
||||
.map(|rule| format!("\"{}\"", rule.0))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
overrides.rename_all = Some(rename_rule);
|
||||
}
|
||||
}
|
||||
Meta::Path(path) => {
|
||||
if !path.is_ident("transparent") {
|
||||
|
@ -22,7 +22,10 @@ pub fn expand_derive_tosql(input: DeriveInput) -> Result<TokenStream, Error> {
|
||||
));
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -47,7 +50,7 @@ pub fn expand_derive_tosql(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),
|
||||
@ -69,7 +72,7 @@ pub fn expand_derive_tosql(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, "ToSql", &fields),
|
||||
|
Loading…
Reference in New Issue
Block a user