1use proc_macro2::TokenStream;
2use quote::{quote, quote_spanned};
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 if field.skip_update() {
51 continue;
52 }
53 let treat_none_as_null = match &field.treat_none_as_null {
55 Some(attr) => {
56 if let Some(embed) = &field.embed {
57 return Err(syn::Error::new(
58 embed.attribute_span,
59 "`embed` and `treat_none_as_default_value` are mutually exclusive",
60 ));
61 }
62
63 if !is_option_ty(&field.ty) {
64 return Err(syn::Error::new(
65 field.ty.span(),
66 "expected `treat_none_as_null` field to be of type `Option<_>`",
67 ));
68 }
69
70 attr.item
71 }
72 None => treat_none_as_null,
73 };
74
75 match (field.serialize_as.as_ref(), field.embed()) {
76 (Some(AttributeSpanWrapper { item: ty, .. }), false) => {
77 direct_field_ty.push(field_changeset_ty_serialize_as(
78 field,
79 table_name,
80 ty,
81 treat_none_as_null,
82 )?);
83 direct_field_assign.push(field_changeset_expr_serialize_as(
84 field,
85 table_name,
86 ty,
87 treat_none_as_null,
88 )?);
89
90 generate_borrowed_changeset = false; }
92 (Some(AttributeSpanWrapper { attribute_span, .. }), true) => {
93 return Err(syn::Error::new(
94 *attribute_span,
95 "`#[diesel(embed)]` cannot be combined with `#[diesel(serialize_as)]`",
96 ));
97 }
98 (None, true) => {
99 direct_field_ty.push(field_changeset_ty_embed(field, None));
100 direct_field_assign.push(field_changeset_expr_embed(field, None));
101 ref_field_ty.push(field_changeset_ty_embed(field, Some(quote!(&'update))));
102 ref_field_assign.push(field_changeset_expr_embed(field, Some(quote!(&))));
103 }
104 (None, false) => {
105 direct_field_ty.push(field_changeset_ty(
106 field,
107 table_name,
108 None,
109 treat_none_as_null,
110 )?);
111 direct_field_assign.push(field_changeset_expr(
112 field,
113 table_name,
114 None,
115 treat_none_as_null,
116 )?);
117 ref_field_ty.push(field_changeset_ty(
118 field,
119 table_name,
120 Some(quote!(&'update)),
121 treat_none_as_null,
122 )?);
123 ref_field_assign.push(field_changeset_expr(
124 field,
125 table_name,
126 Some(quote!(&)),
127 treat_none_as_null,
128 )?);
129 }
130 }
131 }
132
133 let changeset_owned = quote! {
134 impl #impl_generics diesel::query_builder::AsChangeset for #struct_name #ty_generics
135 #where_clause
136 {
137 type Target = #table_name::table;
138 type Changeset = <(#(#direct_field_ty,)*) as diesel::query_builder::AsChangeset>::Changeset;
139
140 fn as_changeset(self) -> <Self as diesel::query_builder::AsChangeset>::Changeset {
141 diesel::query_builder::AsChangeset::as_changeset((#(#direct_field_assign,)*))
142 }
143 }
144 };
145
146 let changeset_borrowed = if generate_borrowed_changeset {
147 let mut impl_generics = item.generics.clone();
148 impl_generics.params.push(parse_quote!('update));
149 let (impl_generics, _, _) = impl_generics.split_for_impl();
150
151 quote! {
152 impl #impl_generics diesel::query_builder::AsChangeset for &'update #struct_name #ty_generics
153 #where_clause
154 {
155 type Target = #table_name::table;
156 type Changeset = <(#(#ref_field_ty,)*) as diesel::query_builder::AsChangeset>::Changeset;
157
158 fn as_changeset(self) -> <Self as diesel::query_builder::AsChangeset>::Changeset {
159 diesel::query_builder::AsChangeset::as_changeset((#(#ref_field_assign,)*))
160 }
161 }
162 }
163 } else {
164 quote! {}
165 };
166
167 Ok(wrap_in_dummy_mod(quote!(
168 #changeset_owned
169
170 #changeset_borrowed
171 )))
172}
173
174fn field_changeset_ty_embed(field: &Field, lifetime: Option<TokenStream>) -> TokenStream {
175 let field_ty = &field.ty;
176 let span = field.span;
177 quote_spanned!(span=> #lifetime #field_ty)
178}
179
180fn field_changeset_expr_embed(field: &Field, lifetime: Option<TokenStream>) -> TokenStream {
181 let field_name = &field.name;
182 quote!(#lifetime self.#field_name)
183}
184
185fn field_changeset_ty(
186 field: &Field,
187 table_name: &Path,
188 lifetime: Option<TokenStream>,
189 treat_none_as_null: bool,
190) -> Result<TokenStream> {
191 let column_name = field.column_name()?.to_ident()?;
192 if !treat_none_as_null && is_option_ty(&field.ty) {
193 let field_ty = inner_of_option_ty(&field.ty);
194 Ok(
195 quote!(std::option::Option<diesel::dsl::Eq<#table_name::#column_name, #lifetime #field_ty>>),
196 )
197 } else {
198 let field_ty = &field.ty;
199 Ok(quote!(diesel::dsl::Eq<#table_name::#column_name, #lifetime #field_ty>))
200 }
201}
202
203fn field_changeset_expr(
204 field: &Field,
205 table_name: &Path,
206 lifetime: Option<TokenStream>,
207 treat_none_as_null: bool,
208) -> Result<TokenStream> {
209 let field_name = &field.name;
210 let column_name = field.column_name()?.to_ident()?;
211 if !treat_none_as_null && is_option_ty(&field.ty) {
212 if lifetime.is_some() {
213 Ok(
214 quote!(self.#field_name.as_ref().map(|x| diesel::ExpressionMethods::eq(#table_name::#column_name, x))),
215 )
216 } else {
217 Ok(
218 quote!(self.#field_name.map(|x| diesel::ExpressionMethods::eq(#table_name::#column_name, x))),
219 )
220 }
221 } else {
222 Ok(
223 quote!(diesel::ExpressionMethods::eq(#table_name::#column_name, #lifetime self.#field_name)),
224 )
225 }
226}
227
228fn field_changeset_ty_serialize_as(
229 field: &Field,
230 table_name: &Path,
231 ty: &Type,
232 treat_none_as_null: bool,
233) -> Result<TokenStream> {
234 let column_name = field.column_name()?.to_ident()?;
235 if !treat_none_as_null && is_option_ty(&field.ty) {
236 let inner_ty = inner_of_option_ty(ty);
237 Ok(quote!(std::option::Option<diesel::dsl::Eq<#table_name::#column_name, #inner_ty>>))
238 } else {
239 Ok(quote!(diesel::dsl::Eq<#table_name::#column_name, #ty>))
240 }
241}
242
243fn field_changeset_expr_serialize_as(
244 field: &Field,
245 table_name: &Path,
246 ty: &Type,
247 treat_none_as_null: bool,
248) -> Result<TokenStream> {
249 let field_name = &field.name;
250 let column_name = field.column_name()?.to_ident()?;
251 let column: Expr = parse_quote!(#table_name::#column_name);
252 if !treat_none_as_null && is_option_ty(&field.ty) {
253 Ok(
254 quote!(self.#field_name.map(|x| diesel::ExpressionMethods::eq(#column, ::std::convert::Into::<#ty>::into(x)))),
255 )
256 } else {
257 Ok(quote!(#column.eq(::std::convert::Into::<#ty>::into(self.#field_name))))
258 }
259}