diesel_derives/
as_changeset.rs

1use proc_macro2::TokenStream;
2use quote::quote;
3use syn::spanned::Spanned as _;
4use syn::{parse_quote, DeriveInput, Expr, Path, Result, Type};
5
6use crate::attrs::AttributeSpanWrapper;
7use crate::field::Field;
8use crate::model::Model;
9use crate::util::{inner_of_option_ty, is_option_ty, wrap_in_dummy_mod};
10
11pub fn derive(item: DeriveInput) -> Result<TokenStream> {
12    let model = Model::from_item(&item, false, false)?;
13
14    let struct_name = &item.ident;
15    let table_name = &model.table_names()[0];
16
17    let fields_for_update = model
18        .fields()
19        .iter()
20        .filter(|f| {
21            !model
22                .primary_key_names
23                .iter()
24                .any(|p| f.column_name().map(|f| f == *p).unwrap_or_default())
25        })
26        .collect::<Vec<_>>();
27
28    if fields_for_update.is_empty() {
29        return Err(syn::Error::new(
30            proc_macro2::Span::call_site(),
31            "Deriving `AsChangeset` on a structure that only contains primary keys isn't supported.\n\
32             help: If you want to change the primary key of a row, you should do so with `.set(table::id.eq(new_id))`.\n\
33             note: `#[derive(AsChangeset)]` never changes the primary key of a row."
34        ));
35    }
36
37    let treat_none_as_null = model.treat_none_as_null();
38
39    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
40
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        // Use field-level attr. with fallback to the struct-level one.
50        let treat_none_as_null = match &field.treat_none_as_null {
51            Some(attr) => {
52                if !is_option_ty(&field.ty) {
53                    return Err(syn::Error::new(
54                        field.ty.span(),
55                        "expected `treat_none_as_null` field to be of type `Option<_>`",
56                    ));
57                }
58
59                attr.item
60            }
61            None => treat_none_as_null,
62        };
63
64        match field.serialize_as.as_ref() {
65            Some(AttributeSpanWrapper { item: ty, .. }) => {
66                direct_field_ty.push(field_changeset_ty_serialize_as(
67                    field,
68                    table_name,
69                    ty,
70                    treat_none_as_null,
71                )?);
72                direct_field_assign.push(field_changeset_expr_serialize_as(
73                    field,
74                    table_name,
75                    ty,
76                    treat_none_as_null,
77                )?);
78
79                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
80            }
81            None => {
82                direct_field_ty.push(field_changeset_ty(
83                    field,
84                    table_name,
85                    None,
86                    treat_none_as_null,
87                )?);
88                direct_field_assign.push(field_changeset_expr(
89                    field,
90                    table_name,
91                    None,
92                    treat_none_as_null,
93                )?);
94                ref_field_ty.push(field_changeset_ty(
95                    field,
96                    table_name,
97                    Some(quote!(&'update)),
98                    treat_none_as_null,
99                )?);
100                ref_field_assign.push(field_changeset_expr(
101                    field,
102                    table_name,
103                    Some(quote!(&)),
104                    treat_none_as_null,
105                )?);
106            }
107        }
108    }
109
110    let changeset_owned = quote! {
111        impl #impl_generics diesel::query_builder::AsChangeset for #struct_name #ty_generics
112        #where_clause
113        {
114            type Target = #table_name::table;
115            type Changeset = <(#(#direct_field_ty,)*) as diesel::query_builder::AsChangeset>::Changeset;
116
117            fn as_changeset(self) -> <Self as diesel::query_builder::AsChangeset>::Changeset {
118                diesel::query_builder::AsChangeset::as_changeset((#(#direct_field_assign,)*))
119            }
120        }
121    };
122
123    let changeset_borrowed = if generate_borrowed_changeset {
124        let mut impl_generics = item.generics.clone();
125        impl_generics.params.push(parse_quote!('update));
126        let (impl_generics, _, _) = impl_generics.split_for_impl();
127
128        quote! {
129            impl #impl_generics diesel::query_builder::AsChangeset for &'update #struct_name #ty_generics
130            #where_clause
131            {
132                type Target = #table_name::table;
133                type Changeset = <(#(#ref_field_ty,)*) as diesel::query_builder::AsChangeset>::Changeset;
134
135                fn as_changeset(self) -> <Self as diesel::query_builder::AsChangeset>::Changeset {
136                    diesel::query_builder::AsChangeset::as_changeset((#(#ref_field_assign,)*))
137                }
138            }
139        }
140    } else {
141        quote! {}
142    };
143
144    Ok(wrap_in_dummy_mod(quote!(
145        #changeset_owned
146
147        #changeset_borrowed
148    )))
149}
150
151fn field_changeset_ty(
152    field: &Field,
153    table_name: &Path,
154    lifetime: Option<TokenStream>,
155    treat_none_as_null: bool,
156) -> Result<TokenStream> {
157    let column_name = field.column_name()?.to_ident()?;
158    if !treat_none_as_null && is_option_ty(&field.ty) {
159        let field_ty = inner_of_option_ty(&field.ty);
160        Ok(
161            quote!(std::option::Option<diesel::dsl::Eq<#table_name::#column_name, #lifetime #field_ty>>),
162        )
163    } else {
164        let field_ty = &field.ty;
165        Ok(quote!(diesel::dsl::Eq<#table_name::#column_name, #lifetime #field_ty>))
166    }
167}
168
169fn field_changeset_expr(
170    field: &Field,
171    table_name: &Path,
172    lifetime: Option<TokenStream>,
173    treat_none_as_null: bool,
174) -> Result<TokenStream> {
175    let field_name = &field.name;
176    let column_name = field.column_name()?.to_ident()?;
177    if !treat_none_as_null && is_option_ty(&field.ty) {
178        if lifetime.is_some() {
179            Ok(
180                quote!(self.#field_name.as_ref().map(|x| diesel::ExpressionMethods::eq(#table_name::#column_name, x))),
181            )
182        } else {
183            Ok(
184                quote!(self.#field_name.map(|x| diesel::ExpressionMethods::eq(#table_name::#column_name, x))),
185            )
186        }
187    } else {
188        Ok(
189            quote!(diesel::ExpressionMethods::eq(#table_name::#column_name, #lifetime self.#field_name)),
190        )
191    }
192}
193
194fn field_changeset_ty_serialize_as(
195    field: &Field,
196    table_name: &Path,
197    ty: &Type,
198    treat_none_as_null: bool,
199) -> Result<TokenStream> {
200    let column_name = field.column_name()?.to_ident()?;
201    if !treat_none_as_null && is_option_ty(&field.ty) {
202        let inner_ty = inner_of_option_ty(ty);
203        Ok(quote!(std::option::Option<diesel::dsl::Eq<#table_name::#column_name, #inner_ty>>))
204    } else {
205        Ok(quote!(diesel::dsl::Eq<#table_name::#column_name, #ty>))
206    }
207}
208
209fn field_changeset_expr_serialize_as(
210    field: &Field,
211    table_name: &Path,
212    ty: &Type,
213    treat_none_as_null: bool,
214) -> Result<TokenStream> {
215    let field_name = &field.name;
216    let column_name = field.column_name()?.to_ident()?;
217    let column: Expr = parse_quote!(#table_name::#column_name);
218    if !treat_none_as_null && is_option_ty(&field.ty) {
219        Ok(
220            quote!(self.#field_name.map(|x| diesel::ExpressionMethods::eq(#column, ::std::convert::Into::<#ty>::into(x)))),
221        )
222    } else {
223        Ok(quote!(#column.eq(::std::convert::Into::<#ty>::into(self.#field_name))))
224    }
225}