diff --git a/postgres-derive-test/src/compile-fail/invalid-transparent.rs b/postgres-derive-test/src/compile-fail/invalid-transparent.rs new file mode 100644 index 00000000..43bd4826 --- /dev/null +++ b/postgres-derive-test/src/compile-fail/invalid-transparent.rs @@ -0,0 +1,35 @@ +use postgres_types::{FromSql, ToSql}; + +#[derive(ToSql, Debug)] +#[postgres(transparent)] +struct ToSqlTransparentStruct { + a: i32 +} + +#[derive(FromSql, Debug)] +#[postgres(transparent)] +struct FromSqlTransparentStruct { + a: i32 +} + +#[derive(ToSql, Debug)] +#[postgres(transparent)] +enum ToSqlTransparentEnum { + Foo +} + +#[derive(FromSql, Debug)] +#[postgres(transparent)] +enum FromSqlTransparentEnum { + Foo +} + +#[derive(ToSql, Debug)] +#[postgres(transparent)] +struct ToSqlTransparentTwoFieldTupleStruct(i32, i32); + +#[derive(FromSql, Debug)] +#[postgres(transparent)] +struct FromSqlTransparentTwoFieldTupleStruct(i32, i32); + +fn main() {} diff --git a/postgres-derive-test/src/compile-fail/invalid-transparent.stderr b/postgres-derive-test/src/compile-fail/invalid-transparent.stderr new file mode 100644 index 00000000..42e49f87 --- /dev/null +++ b/postgres-derive-test/src/compile-fail/invalid-transparent.stderr @@ -0,0 +1,49 @@ +error: #[postgres(transparent)] may only be applied to single field tuple structs + --> src/compile-fail/invalid-transparent.rs:4:1 + | +4 | / #[postgres(transparent)] +5 | | struct ToSqlTransparentStruct { +6 | | a: i32 +7 | | } + | |_^ + +error: #[postgres(transparent)] may only be applied to single field tuple structs + --> src/compile-fail/invalid-transparent.rs:10:1 + | +10 | / #[postgres(transparent)] +11 | | struct FromSqlTransparentStruct { +12 | | a: i32 +13 | | } + | |_^ + +error: #[postgres(transparent)] may only be applied to single field tuple structs + --> src/compile-fail/invalid-transparent.rs:16:1 + | +16 | / #[postgres(transparent)] +17 | | enum ToSqlTransparentEnum { +18 | | Foo +19 | | } + | |_^ + +error: #[postgres(transparent)] may only be applied to single field tuple structs + --> src/compile-fail/invalid-transparent.rs:22:1 + | +22 | / #[postgres(transparent)] +23 | | enum FromSqlTransparentEnum { +24 | | Foo +25 | | } + | |_^ + +error: #[postgres(transparent)] may only be applied to single field tuple structs + --> src/compile-fail/invalid-transparent.rs:28:1 + | +28 | / #[postgres(transparent)] +29 | | struct ToSqlTransparentTwoFieldTupleStruct(i32, i32); + | |_____________________________________________________^ + +error: #[postgres(transparent)] may only be applied to single field tuple structs + --> src/compile-fail/invalid-transparent.rs:32:1 + | +32 | / #[postgres(transparent)] +33 | | struct FromSqlTransparentTwoFieldTupleStruct(i32, i32); + | |_______________________________________________________^ diff --git a/postgres-derive-test/src/lib.rs b/postgres-derive-test/src/lib.rs index 7da75af8..279ed141 100644 --- a/postgres-derive-test/src/lib.rs +++ b/postgres-derive-test/src/lib.rs @@ -7,6 +7,7 @@ use std::fmt; mod composites; mod domains; mod enums; +mod transparent; pub fn test_type(conn: &mut Client, sql_type: &str, checks: &[(T, S)]) where diff --git a/postgres-derive-test/src/transparent.rs b/postgres-derive-test/src/transparent.rs new file mode 100644 index 00000000..1614553d --- /dev/null +++ b/postgres-derive-test/src/transparent.rs @@ -0,0 +1,18 @@ +use postgres::{Client, NoTls}; +use postgres_types::{FromSql, ToSql}; + +#[test] +fn round_trip() { + #[derive(FromSql, ToSql, Debug, PartialEq)] + #[postgres(transparent)] + struct UserId(i32); + + assert_eq!( + Client::connect("user=postgres host=localhost port=5433", NoTls) + .unwrap() + .query_one("SELECT $1::integer", &[&UserId(123)]) + .unwrap() + .get::<_, UserId>(0), + UserId(123) + ); +} diff --git a/postgres-derive/src/accepts.rs b/postgres-derive/src/accepts.rs index 530badd0..63473863 100644 --- a/postgres-derive/src/accepts.rs +++ b/postgres-derive/src/accepts.rs @@ -6,6 +6,14 @@ use syn::Ident; use crate::composites::Field; use crate::enums::Variant; +pub fn transparent_body(field: &syn::Field) -> TokenStream { + let ty = &field.ty; + + quote! { + <#ty as ::postgres_types::ToSql>::accepts(type_) + } +} + pub fn domain_body(name: &str, field: &syn::Field) -> TokenStream { let ty = &field.ty; diff --git a/postgres-derive/src/fromsql.rs b/postgres-derive/src/fromsql.rs index 3a59d622..c89cbb5e 100644 --- a/postgres-derive/src/fromsql.rs +++ b/postgres-derive/src/fromsql.rs @@ -11,9 +11,36 @@ use crate::overrides::Overrides; pub fn expand_derive_fromsql(input: DeriveInput) -> Result { let overrides = Overrides::extract(&input.attrs)?; + if overrides.name.is_some() && overrides.transparent { + return Err(Error::new_spanned( + &input, + "#[postgres(transparent)] is not allowed with #[postgres(name = \"...\")]", + )); + } + let name = overrides.name.unwrap_or_else(|| input.ident.to_string()); - let (accepts_body, to_sql_body) = match input.data { + let (accepts_body, to_sql_body) = if overrides.transparent { + match input.data { + Data::Struct(DataStruct { + fields: Fields::Unnamed(ref fields), + .. + }) if fields.unnamed.len() == 1 => { + let field = fields.unnamed.first().unwrap(); + ( + accepts::transparent_body(field), + transparent_body(&input.ident, field), + ) + } + _ => { + return Err(Error::new_spanned( + input, + "#[postgres(transparent)] may only be applied to single field tuple structs", + )) + } + } + } else { + match input.data { Data::Enum(ref data) => { let variants = data .variants @@ -55,6 +82,7 @@ pub fn expand_derive_fromsql(input: DeriveInput) -> Result { "#[derive(FromSql)] may only be applied to structs, single field tuple structs, and enums", )) } + } }; let ident = &input.ident; @@ -77,6 +105,13 @@ pub fn expand_derive_fromsql(input: DeriveInput) -> Result { Ok(out) } +fn transparent_body(ident: &Ident, field: &syn::Field) -> TokenStream { + let ty = &field.ty; + quote! { + <#ty as postgres_types::FromSql>::from_sql(_type, buf).map(#ident) + } +} + fn enum_body(ident: &Ident, variants: &[Variant]) -> TokenStream { let variant_names = variants.iter().map(|v| &v.name); let idents = iter::repeat(ident); diff --git a/postgres-derive/src/lib.rs b/postgres-derive/src/lib.rs index fd17b9de..98e6add2 100644 --- a/postgres-derive/src/lib.rs +++ b/postgres-derive/src/lib.rs @@ -4,6 +4,7 @@ extern crate proc_macro; use proc_macro::TokenStream; +use syn::parse_macro_input; mod accepts; mod composites; @@ -14,7 +15,8 @@ mod tosql; #[proc_macro_derive(ToSql, attributes(postgres))] pub fn derive_tosql(input: TokenStream) -> TokenStream { - let input = syn::parse(input).unwrap(); + let input = parse_macro_input!(input); + tosql::expand_derive_tosql(input) .unwrap_or_else(|e| e.to_compile_error()) .into() @@ -22,7 +24,8 @@ pub fn derive_tosql(input: TokenStream) -> TokenStream { #[proc_macro_derive(FromSql, attributes(postgres))] pub fn derive_fromsql(input: TokenStream) -> TokenStream { - let input = syn::parse(input).unwrap(); + let input = parse_macro_input!(input); + fromsql::expand_derive_fromsql(input) .unwrap_or_else(|e| e.to_compile_error()) .into() diff --git a/postgres-derive/src/overrides.rs b/postgres-derive/src/overrides.rs index 08e6f3a7..c00d5a94 100644 --- a/postgres-derive/src/overrides.rs +++ b/postgres-derive/src/overrides.rs @@ -2,17 +2,18 @@ use syn::{Attribute, Error, Lit, Meta, NestedMeta}; pub struct Overrides { pub name: Option, + pub transparent: bool, } impl Overrides { pub fn extract(attrs: &[Attribute]) -> Result { - let mut overrides = Overrides { name: None }; + let mut overrides = Overrides { + name: None, + transparent: false, + }; for attr in attrs { - let attr = match attr.parse_meta() { - Ok(meta) => meta, - Err(_) => continue, - }; + let attr = attr.parse_meta()?; if !attr.path().is_ident("postgres") { continue; @@ -39,7 +40,14 @@ impl Overrides { overrides.name = Some(value); } - bad => return Err(Error::new_spanned(bad, "expected a name-value meta item")), + NestedMeta::Meta(Meta::Path(ref path)) => { + if !path.is_ident("transparent") { + return Err(Error::new_spanned(path, "unknown override")); + } + + overrides.transparent = true; + } + bad => return Err(Error::new_spanned(bad, "unknown attribute")), } } } diff --git a/postgres-derive/src/tosql.rs b/postgres-derive/src/tosql.rs index 1808e787..96f26138 100644 --- a/postgres-derive/src/tosql.rs +++ b/postgres-derive/src/tosql.rs @@ -11,46 +11,73 @@ use crate::overrides::Overrides; pub fn expand_derive_tosql(input: DeriveInput) -> Result { let overrides = Overrides::extract(&input.attrs)?; + if overrides.name.is_some() && overrides.transparent { + return Err(Error::new_spanned( + &input, + "#[postgres(transparent)] is not allowed with #[postgres(name = \"...\")]", + )); + } + let name = overrides.name.unwrap_or_else(|| input.ident.to_string()); - let (accepts_body, to_sql_body) = match input.data { - Data::Enum(ref data) => { - let variants = data - .variants - .iter() - .map(Variant::parse) - .collect::, _>>()?; - ( - accepts::enum_body(&name, &variants), - enum_body(&input.ident, &variants), - ) + let (accepts_body, to_sql_body) = if overrides.transparent { + match input.data { + Data::Struct(DataStruct { + fields: Fields::Unnamed(ref fields), + .. + }) if fields.unnamed.len() == 1 => { + let field = fields.unnamed.first().unwrap(); + + (accepts::transparent_body(field), transparent_body()) + } + _ => { + return Err(Error::new_spanned( + input, + "#[postgres(transparent)] may only be applied to single field tuple structs", + )); + } } - Data::Struct(DataStruct { - fields: Fields::Unnamed(ref fields), - .. - }) if fields.unnamed.len() == 1 => { - let field = fields.unnamed.first().unwrap(); - (accepts::domain_body(&name, field), domain_body()) - } - Data::Struct(DataStruct { - fields: Fields::Named(ref fields), - .. - }) => { - let fields = fields - .named - .iter() - .map(Field::parse) - .collect::, _>>()?; - ( - accepts::composite_body(&name, "ToSql", &fields), - composite_body(&fields), - ) - } - _ => { - return Err(Error::new_spanned( - input, - "#[derive(ToSql)] may only be applied to structs, single field tuple structs, and enums", - )); + } else { + match input.data { + Data::Enum(ref data) => { + let variants = data + .variants + .iter() + .map(Variant::parse) + .collect::, _>>()?; + ( + accepts::enum_body(&name, &variants), + enum_body(&input.ident, &variants), + ) + } + Data::Struct(DataStruct { + fields: Fields::Unnamed(ref fields), + .. + }) if fields.unnamed.len() == 1 => { + let field = fields.unnamed.first().unwrap(); + + (accepts::domain_body(&name, field), domain_body()) + } + Data::Struct(DataStruct { + fields: Fields::Named(ref fields), + .. + }) => { + let fields = fields + .named + .iter() + .map(Field::parse) + .collect::, _>>()?; + ( + accepts::composite_body(&name, "ToSql", &fields), + composite_body(&fields), + ) + } + _ => { + return Err(Error::new_spanned( + input, + "#[derive(ToSql)] may only be applied to structs, single field tuple structs, and enums", + )); + } } }; @@ -78,6 +105,12 @@ pub fn expand_derive_tosql(input: DeriveInput) -> Result { Ok(out) } +fn transparent_body() -> TokenStream { + quote! { + postgres_types::ToSql::to_sql(&self.0, _type, buf) + } +} + fn enum_body(ident: &Ident, variants: &[Variant]) -> TokenStream { let idents = iter::repeat(ident); let variant_idents = variants.iter().map(|v| &v.ident); diff --git a/postgres-types/src/lib.rs b/postgres-types/src/lib.rs index 84354cf3..873c5ec1 100644 --- a/postgres-types/src/lib.rs +++ b/postgres-types/src/lib.rs @@ -55,6 +55,21 @@ //! struct SessionId(Vec); //! ``` //! +//! ## Newtypes +//! +//! The `#[postgres(transparent)]` attribute can be used on a single-field tuple struct to create a +//! Rust-only wrapper type that will use the [`ToSql`] & [`FromSql`] implementation of the inner +//! value : +//! ```rust +//! # #[cfg(feature = "derive")] +//! use postgres_types::{ToSql, FromSql}; +//! +//! # #[cfg(feature = "derive")] +//! #[derive(Debug, ToSql, FromSql)] +//! #[postgres(transparent)] +//! struct UserId(i32); +//! ``` +//! //! ## Composites //! //! Postgres composite types correspond to structs in Rust: