Skip to content

Commit c2b4b62

Browse files
authored
Merge pull request diesel-rs#2853 from flisky/master
support `serialize_as` for `AsChangeset`
2 parents 2a6dffc + db0f20a commit c2b4b62

5 files changed

Lines changed: 219 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
4848
* Added support for PostgreSQL's `SIMILAR TO` and `NOT SIMILAR TO`.
4949

5050
* Added `#[diesel(serialize_as)]` analogous to `#[diesel(deserialize_as)]`. This allows
51-
customization of the serialization behaviour of `Insertable` structs.
51+
customization of the serialization behaviour of `Insertable` and `AsChangeset` structs.
5252

5353
* Added support for `GROUP BY` clauses
5454

diesel_derives/src/as_changeset.rs

Lines changed: 115 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use proc_macro2::TokenStream;
2-
use syn::{DeriveInput, Path};
2+
use syn::{DeriveInput, Expr, Path, Type};
33

4+
use attrs::AttributeSpanWrapper;
45
use field::Field;
56
use model::Model;
67
use util::{inner_of_option_ty, is_option_ty, wrap_in_dummy_mod};
@@ -37,58 +38,105 @@ pub fn derive(item: DeriveInput) -> TokenStream {
3738
impl_generics.params.push(parse_quote!('update));
3839
let (impl_generics, _, _) = impl_generics.split_for_impl();
3940

40-
let ref_changeset_ty = fields_for_update.iter().map(|field| {
41-
field_changeset_ty(
42-
field,
43-
&table_name,
44-
treat_none_as_null,
45-
Some(quote!(&'update)),
46-
)
47-
});
48-
let ref_changeset_expr = fields_for_update
49-
.iter()
50-
.map(|field| field_changeset_expr(field, &table_name, treat_none_as_null, Some(quote!(&))));
51-
52-
let direct_changeset_ty = fields_for_update
53-
.iter()
54-
.map(|field| field_changeset_ty(field, &table_name, treat_none_as_null, None));
55-
let direct_changeset_expr = fields_for_update
56-
.iter()
57-
.map(|field| field_changeset_expr(field, &table_name, treat_none_as_null, None));
58-
59-
wrap_in_dummy_mod(quote!(
60-
use diesel::query_builder::AsChangeset;
61-
use diesel::prelude::*;
62-
63-
impl #impl_generics AsChangeset for &'update #struct_name #ty_generics
64-
#where_clause
65-
{
66-
type Target = #table_name::table;
67-
type Changeset = <(#(#ref_changeset_ty,)*) as AsChangeset>::Changeset;
68-
69-
fn as_changeset(self) -> Self::Changeset {
70-
(#(#ref_changeset_expr,)*).as_changeset()
41+
let mut generate_borrowed_changeset = true;
42+
43+
let mut direct_field_ty = Vec::with_capacity(fields_for_update.len());
44+
let mut direct_field_assign = Vec::with_capacity(fields_for_update.len());
45+
let mut ref_field_ty = Vec::with_capacity(fields_for_update.len());
46+
let mut ref_field_assign = Vec::with_capacity(fields_for_update.len());
47+
48+
for field in fields_for_update {
49+
match field.serialize_as.as_ref() {
50+
Some(AttributeSpanWrapper { item: ty, .. }) => {
51+
direct_field_ty.push(field_changeset_ty_serialize_as(
52+
field,
53+
table_name,
54+
ty,
55+
treat_none_as_null,
56+
));
57+
direct_field_assign.push(field_changeset_expr_serialize_as(
58+
field,
59+
table_name,
60+
ty,
61+
treat_none_as_null,
62+
));
63+
64+
generate_borrowed_changeset = false; // as soon as we hit one field with #[diesel(serialize_as)] there is no point in generating the impl of AsChangeset for borrowed structs
65+
}
66+
None => {
67+
direct_field_ty.push(field_changeset_ty(
68+
field,
69+
table_name,
70+
None,
71+
treat_none_as_null,
72+
));
73+
direct_field_assign.push(field_changeset_expr(
74+
field,
75+
table_name,
76+
None,
77+
treat_none_as_null,
78+
));
79+
ref_field_ty.push(field_changeset_ty(
80+
field,
81+
table_name,
82+
Some(quote!(&'update)),
83+
treat_none_as_null,
84+
));
85+
ref_field_assign.push(field_changeset_expr(
86+
field,
87+
table_name,
88+
Some(quote!(&)),
89+
treat_none_as_null,
90+
));
7191
}
7292
}
93+
}
7394

95+
let changeset_owned = quote! {
7496
impl #impl_generics AsChangeset for #struct_name #ty_generics
7597
#where_clause
7698
{
7799
type Target = #table_name::table;
78-
type Changeset = <(#(#direct_changeset_ty,)*) as AsChangeset>::Changeset;
100+
type Changeset = <(#(#direct_field_ty,)*) as AsChangeset>::Changeset;
79101

80102
fn as_changeset(self) -> Self::Changeset {
81-
(#(#direct_changeset_expr,)*).as_changeset()
103+
(#(#direct_field_assign,)*).as_changeset()
82104
}
83105
}
106+
};
107+
108+
let changeset_borrowed = if generate_borrowed_changeset {
109+
quote! {
110+
impl #impl_generics AsChangeset for &'update #struct_name #ty_generics
111+
#where_clause
112+
{
113+
type Target = #table_name::table;
114+
type Changeset = <(#(#ref_field_ty,)*) as AsChangeset>::Changeset;
115+
116+
fn as_changeset(self) -> Self::Changeset {
117+
(#(#ref_field_assign,)*).as_changeset()
118+
}
119+
}
120+
}
121+
} else {
122+
quote! {}
123+
};
124+
125+
wrap_in_dummy_mod(quote!(
126+
use diesel::query_builder::AsChangeset;
127+
use diesel::prelude::*;
128+
129+
#changeset_owned
130+
131+
#changeset_borrowed
84132
))
85133
}
86134

87135
fn field_changeset_ty(
88136
field: &Field,
89137
table_name: &Path,
90-
treat_none_as_null: bool,
91138
lifetime: Option<TokenStream>,
139+
treat_none_as_null: bool,
92140
) -> TokenStream {
93141
let column_name = field.column_name();
94142
if !treat_none_as_null && is_option_ty(&field.ty) {
@@ -103,8 +151,8 @@ fn field_changeset_ty(
103151
fn field_changeset_expr(
104152
field: &Field,
105153
table_name: &Path,
106-
treat_none_as_null: bool,
107154
lifetime: Option<TokenStream>,
155+
treat_none_as_null: bool,
108156
) -> TokenStream {
109157
let field_name = &field.name;
110158
let column_name = field.column_name();
@@ -118,3 +166,34 @@ fn field_changeset_expr(
118166
quote!(#table_name::#column_name.eq(#lifetime self.#field_name))
119167
}
120168
}
169+
170+
fn field_changeset_ty_serialize_as(
171+
field: &Field,
172+
table_name: &Path,
173+
ty: &Type,
174+
treat_none_as_null: bool,
175+
) -> TokenStream {
176+
let column_name = field.column_name();
177+
if !treat_none_as_null && is_option_ty(&field.ty) {
178+
let inner_ty = inner_of_option_ty(ty);
179+
quote!(std::option::Option<diesel::dsl::Eq<#table_name::#column_name, #inner_ty>>)
180+
} else {
181+
quote!(diesel::dsl::Eq<#table_name::#column_name, #ty>)
182+
}
183+
}
184+
185+
fn field_changeset_expr_serialize_as(
186+
field: &Field,
187+
table_name: &Path,
188+
ty: &Type,
189+
treat_none_as_null: bool,
190+
) -> TokenStream {
191+
let field_name = &field.name;
192+
let column_name = field.column_name();
193+
let column: Expr = parse_quote!(#table_name::#column_name);
194+
if !treat_none_as_null && is_option_ty(&field.ty) {
195+
quote!(self.#field_name.map(|x| #column.eq(::std::convert::Into::<#ty>::into(x))))
196+
} else {
197+
quote!(#column.eq(::std::convert::Into::<#ty>::into(self.#field_name)))
198+
}
199+
}

diesel_derives/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ mod valid_grouping;
6262
/// from the name of the corresponding column, you can annotate the field with
6363
/// `#[diesel(column_name = some_column_name)]`.
6464
///
65+
/// To provide custom serialization behavior for a field, you can use
66+
/// `#[diesel(serialize_as = SomeType)]`. If this attribute is present, Diesel
67+
/// will call `.into` on the corresponding field and serialize the instance of `SomeType`,
68+
/// rather than the actual field on your struct. This can be used to add custom behavior for a
69+
/// single field, or use types that are otherwise unsupported by Diesel.
70+
/// Normally, Diesel produces two implementations of the `AsChangeset` trait for your
71+
/// struct using this derive: one for an owned version and one for a borrowed version.
72+
/// Using `#[diesel(serialize_as)]` implies a conversion using `.into` which consumes the underlying value.
73+
/// Hence, once you use `#[diesel(serialize_as)]`, Diesel can no longer insert borrowed
74+
/// versions of your struct.
75+
///
6576
/// By default, any `Option` fields on the struct are skipped if their value is
6677
/// `None`. If you would like to assign `NULL` to the field instead, you can
6778
/// annotate your struct with `#[diesel(treat_none_as_null = true)]`.
@@ -87,6 +98,10 @@ mod valid_grouping;
8798
/// * `#[diesel(column_name = some_column_name)]`, overrides the column name
8899
/// of the current field to `some_column_name`. By default the field
89100
/// name is used as column name.
101+
/// * `#[diesel(serialize_as = SomeType)]`, instead of serializing the actual
102+
/// field type, Diesel will convert the field into `SomeType` using `.into` and
103+
/// serialize that instead. By default this derive will serialize directly using
104+
/// the actual field type.
90105
#[proc_macro_error]
91106
#[proc_macro_derive(
92107
AsChangeset,

diesel_derives/tests/as_changeset.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,57 @@ fn with_explicit_column_names() {
218218
assert_eq!(Ok(expected), actual);
219219
}
220220

221+
#[test]
222+
fn with_serialize_as() {
223+
#[derive(Debug, FromSqlRow, AsExpression)]
224+
#[diesel(sql_type = sql_types::Text)]
225+
struct UppercaseString(pub String);
226+
227+
impl Into<UppercaseString> for String {
228+
fn into(self) -> UppercaseString {
229+
UppercaseString(self.to_uppercase())
230+
}
231+
}
232+
233+
impl<DB> serialize::ToSql<sql_types::Text, DB> for UppercaseString
234+
where
235+
DB: backend::Backend,
236+
String: serialize::ToSql<sql_types::Text, DB>,
237+
{
238+
fn to_sql<W: std::io::Write>(
239+
&self,
240+
out: &mut serialize::Output<W, DB>,
241+
) -> serialize::Result {
242+
self.0.to_sql(out)
243+
}
244+
}
245+
246+
#[derive(AsChangeset)]
247+
struct User {
248+
#[diesel(serialize_as = UppercaseString)]
249+
name: String,
250+
#[diesel(serialize_as = UppercaseString)]
251+
hair_color: Option<String>,
252+
}
253+
254+
let connection = &mut connection_with_sean_and_tess_in_users_table();
255+
256+
update(users::table.find(1))
257+
.set(User {
258+
name: String::from("Jim"),
259+
hair_color: Some(String::from("blue")),
260+
})
261+
.execute(connection)
262+
.unwrap();
263+
264+
let expected = vec![
265+
(1, String::from("JIM"), Some(String::from("BLUE"))),
266+
(2, String::from("Tess"), Some(String::from("brown"))),
267+
];
268+
let actual = users::table.order(users::id).load(connection);
269+
assert_eq!(Ok(expected), actual);
270+
}
271+
221272
#[test]
222273
fn tuple_struct() {
223274
#[derive(AsChangeset)]

diesel_tests/tests/serialize_as.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,16 @@ struct InsertableUser {
3131
name: String,
3232
}
3333

34+
#[derive(Clone, Debug, AsChangeset, Identifiable)]
35+
#[diesel(table_name = users)]
36+
struct ChangeUser {
37+
id: i32,
38+
#[diesel(serialize_as = UppercaseString)]
39+
name: String,
40+
}
41+
3442
#[test]
35-
fn serialization_can_be_customized() {
43+
fn insert_serialization_can_be_customized() {
3644
use crate::schema::users::dsl::*;
3745
let connection = &mut connection();
3846

@@ -50,3 +58,31 @@ fn serialization_can_be_customized() {
5058
users.select(name).first(connection)
5159
);
5260
}
61+
62+
#[test]
63+
fn update_serialization_can_be_customized() {
64+
use crate::schema::users::dsl::*;
65+
let connection = &mut connection();
66+
67+
let user = InsertableUser {
68+
name: "thomas".to_string(),
69+
};
70+
diesel::insert_into(users)
71+
.values(user)
72+
.execute(connection)
73+
.unwrap();
74+
75+
let user = ChangeUser {
76+
id: users.select(id).first(connection).unwrap(),
77+
name: "eizinger".to_string(),
78+
};
79+
diesel::update(&user)
80+
.set(user.clone())
81+
.execute(connection)
82+
.unwrap();
83+
84+
assert_eq!(
85+
Ok("EIZINGER".to_string()),
86+
users.select(name).first(connection)
87+
);
88+
}

0 commit comments

Comments
 (0)