sfackler / rust-postgres

Native PostgreSQL driver for the Rust programming language
Apache License 2.0
3.48k stars 443 forks source link

Enhancement #[derive(ToSqlJson, FromSqlJson)] #622

Open godofdream opened 4 years ago

godofdream commented 4 years ago

One common case is to have a json column which should be deserialised to a struct. Currently the postgres_type Json doesn't work if you also want Serialize and Deserialize.

use postgres_types::Json;

#[derive(Serialize, Deserialize, ToSql, FromSql, Debug)]
pub struct Product {
    pub id: String,
    pub data: Json<ProductData>,
}
#[derive(Serialize, Deserialize, ToSql, FromSql, Debug)]
pub struct ProductData {
    pub a: String,
    pub b: String,
}

It would be nice to add a derive which implements ToSql using Json.

The following could be the derive.

#[proc_macro_derive(ToSqlJson)]
pub fn postgres_mapper(input: TokenStream) -> TokenStream {
    let mut ast: DeriveInput = syn::parse(input).expect("Couldn't parse item");
    let name = &ast.ident;
    let tokens = quote! {

        use bytes::BytesMut;
        use postgres_types::{accepts, to_sql_checked, IsNull, Json, Type};

        impl ToSql for #name {
            fn to_sql(
                &self,
                ty: &Type,
                out: &mut BytesMut,
            ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
                Json(self).to_sql(ty, out)
            }
            accepts!(JSON, JSONB);
            to_sql_checked!();
        }
    };
    tokens.into()
}
#[proc_macro_derive(FromSqlJson)]
pub fn postgres_mapper(input: TokenStream) -> TokenStream {
    let mut ast: DeriveInput = syn::parse(input).expect("Couldn't parse item");
    let name = &ast.ident;
    let tokens = quote! {

        use bytes::BytesMut;
        use postgres_types::{accepts, Json, Type};

        impl<'a> FromSql<'a> for #name {
            fn from_sql(
                ty: &Type,
                raw: &[u8],
            ) -> Result<#name, Box<dyn std::error::Error + Sync + Send>> {
                Json::<#name>::from_sql(ty, raw).map(|json| json.0)
            }
            accepts!(JSON, JSONB);
        }
    };
    tokens.into()
}

As a Result the struct would now be:

#[derive(Serialize, Deserialize, ToSql, FromSql, Debug)]
pub struct Product {
    pub id: String,
    pub data: ProductData,
}
#[derive(Serialize, Deserialize, ToSqlJson, FromSqlJson, Debug)]
pub struct ProductData {
    pub a: String,
    pub b: String,
}
sfackler commented 3 years ago

This seems reasonable, but I think I'd configure it this way instead of adding a new derive:

#[derive(Serialize, Deserialize, ToSql, FromSql, Debug)]
pub struct Product {
    pub id: String,
    pub data: ProductData,
}
#[derive(Serialize, Deserialize, ToSql, FromSql, Debug)]
#[postgres(json)]
pub struct ProductData {
    pub a: String,
    pub b: String,
}