1use convert_case::{Case, Casing as _};
5
6use darling::util::{Flag, SpannedValue};
7use darling::{
8 Error, FromAttributes, FromDeriveInput, FromField, FromMeta, FromVariant,
9 Result,
10};
11use darling::ast::Data as AstData;
12
13use proc_macro::TokenStream;
14
15use quote::{format_ident, quote};
16
17use syn::{Attribute, Generics, Ident, Member, Meta, Path, Visibility};
18
19#[derive(Debug, FromDeriveInput)]
20#[darling(
21 attributes(diesel_composite),
22 forward_attrs(diesel),
23 supports(enum_unit, struct_named, struct_tuple),
24 and_then = Self::validate,
25)]
26struct Composite {
27 vis: Visibility,
28 ident: Ident,
29 generics: Generics,
30 data: AstData<CompositeVariant, CompositeField>,
31 rename_all: Option<SpannedValue<Casing>>,
32 sql_type: Option<Path>,
33 postgres_type: Option<Meta>,
34 serialize: Flag,
35 deserialize: Flag,
36 #[darling(with = Self::parse_diesel_attrs)]
37 attrs: Option<Path>,
38}
39
40impl Composite {
41 fn parse_diesel_attrs(attrs: Vec<Attribute>) -> Result<Option<Path>> {
42 #[derive(Debug, FromAttributes)]
43 #[darling(attributes(diesel))]
44 struct DieselAttrs {
45 sql_type: Option<Path>,
46 }
47
48 Ok(DieselAttrs::from_attributes(&attrs)?.sql_type)
49 }
50
51 fn validate(mut self) -> Result<Self> {
52 if self.data.is_struct() && self.rename_all.is_some() {
53 return Err(
54 Error::custom("rename_all has no effect on structs")
55 .with_span(&self.rename_all.unwrap().span())
56 )
57 }
58
59 if !self.serialize.is_present() && !self.deserialize.is_present() {
60 self.serialize = Flag::present();
61 self.deserialize = Flag::present();
62 }
63
64 if self.sql_type.is_none() {
65 self.sql_type = self.attrs.take();
66
67 if self.sql_type.is_none() {
68 return Err(Error::custom("missing sql_type"))
69 }
70 }
71
72 Ok(self)
73 }
74}
75
76#[derive(Debug, FromVariant)]
77#[darling(attributes(diesel_composite))]
78struct CompositeVariant {
79 ident: Ident,
80 rename: Option<String>,
81}
82
83#[derive(Debug, Default, FromMeta)]
84enum Casing {
85 #[darling(rename = "lowercase")]
86 Lower,
87 #[darling(rename = "UPPERCASE")]
88 Upper,
89 #[darling(rename = "camelCase")]
90 Camel,
91 #[darling(rename = "PascalCase")]
92 UpperCamel,
93 #[default]
94 #[darling(rename = "snake_case")]
95 Snake,
96 #[darling(rename = "SCREAMING_SNAKE_CASE")]
97 UpperSnake,
98 #[darling(rename = "kebab-case")]
99 Kebab,
100 #[darling(rename = "SCREAMING-KEBAB-CASE")]
101 UpperKebab,
102}
103
104impl Casing {
105 fn case(self) -> Case<'static> {
106 match self {
107 Self::Lower => Case::Flat,
108 Self::Upper => Case::UpperFlat,
109 Self::Camel => Case::Camel,
110 Self::UpperCamel => Case::UpperCamel,
111 Self::Snake => Case::Snake,
112 Self::UpperSnake => Case::UpperSnake,
113 Self::Kebab => Case::Kebab,
114 Self::UpperKebab => Case::UpperKebab,
115 }
116 }
117}
118
119#[derive(Debug, FromField)]
120#[darling(attributes(diesel_composite))]
121struct CompositeField {
122 ident: Option<Ident>,
123 sql_type: Path,
124}
125
126#[proc_macro_derive(Composite, attributes(diesel_composite))]
235pub fn derive(input: TokenStream) -> TokenStream {
236 let input = syn::parse_macro_input!(input);
237 let composite = match Composite::from_derive_input(&input) {
238 Ok(composite) => composite,
239 Err(err) => return err.write_errors().into(),
240 };
241
242 let Composite { vis, ident, sql_type, .. } = composite;
243
244 let mapped_type = composite.postgres_type.map(|postgres_type| quote! {
245 #[derive(Clone)]
246 #[derive(::diesel::query_builder::QueryId, ::diesel::sql_types::SqlType)]
247 #[diesel(#postgres_type)]
248 #vis struct #sql_type;
249 });
250
251 let (ser_body, de_body) = match composite.data {
252 AstData::Enum(variants) => {
253 let case = composite.rename_all.unwrap_or_default().into_inner().case();
254 let (ser_match_arms, de_match_arms) = variants
255 .into_iter()
256 .map(|variant| {
257 let name = variant.rename
258 .unwrap_or_else(|| variant.ident.to_string().to_case(case));
259 let ident = variant.ident;
260
261 (
262 quote! { Self::#ident => #name },
263 quote! { #name => Self::#ident },
264 )
265 })
266 .unzip::<_, _, Vec<_>, Vec<_>>();
267
268 (
269 quote! {
270 ::diesel::serialize::ToSql::<
271 ::diesel::sql_types::Text,
272 ::diesel::pg::Pg,
273 >
274 ::to_sql(
275 match self {
276 #(#ser_match_arms),*
277 },
278 out,
279 )
280 },
281 quote! {
282 <
283 ::std::string::String as ::diesel::deserialize::FromSql<
284 ::diesel::sql_types::Text,
285 ::diesel::pg::Pg,
286 >
287 >
288 ::from_sql(bytes)
289 .and_then(|string| Ok(match string.as_str() {
290 #(#de_match_arms),*,
291 _ => return Err("failed to match enum variant".into()),
292 }))
293 },
294 )
295 },
296 AstData::Struct(fields) => {
297 let ((sql_types, field_borrows), (bindings, field_assigns)) = fields
298 .into_iter()
299 .enumerate()
300 .map(|(i, field)| {
301 let member = field.ident
302 .map(Member::Named)
303 .unwrap_or(Member::Unnamed(i.into()));
304 let binding = format_ident!("_{i}");
305
306 (
307 (
308 field.sql_type,
309 quote! { &self.#member },
310 ),
311 (
312 binding.clone(),
313 quote! { #member: #binding },
314 ),
315 )
316 })
317 .unzip::<_, _, (Vec<_>, Vec<_>), (Vec<_>, Vec<_>)>();
318
319 (
320 quote! {
321 ::diesel::serialize::WriteTuple::<(#(#sql_types),*)>
322 ::write_tuple(
323 &(#(#field_borrows),*),
324 out,
325 )
326 },
327 quote! {
328 ::diesel::deserialize::FromSql::<
329 ::diesel::sql_types::Record<(#(#sql_types),*)>,
330 ::diesel::pg::Pg,
331 >
332 ::from_sql(bytes)
333 .map(|(#(#bindings),*)| Self {
334 #(#field_assigns),*
335 })
336 },
337 )
338 },
339 };
340
341 let (impl_gens, ty_gens, where_clause) = composite.generics.split_for_impl();
342
343 let ser_impl = composite.serialize.is_present().then(|| quote! {
344 impl #impl_gens
345 ::diesel::serialize::ToSql<#sql_type, ::diesel::pg::Pg> for
346 #ident #ty_gens #where_clause
347 {
348 fn to_sql(
349 &self, out: &mut ::diesel::serialize::Output<'_, '_, Pg>,
350 ) -> ::diesel::serialize::Result {
351 #ser_body
352 }
353 }
354 });
355 let de_impl = composite.deserialize.is_present().then(|| quote! {
356 impl #impl_gens
357 ::diesel::deserialize::FromSql<#sql_type, ::diesel::pg::Pg> for
358 #ident #ty_gens #where_clause
359 {
360 fn from_sql(
361 bytes: ::diesel::pg::PgValue<'_>,
362 ) -> ::diesel::deserialize::Result<Self> {
363 #de_body
364 }
365 }
366 });
367
368 quote! {
369 #mapped_type
370 #ser_impl
371 #de_impl
372 }.into()
373}