Skip to main content

diesel_derive_composite/
lib.rs

1//! Generates boilerplate implementations of `ToSql` and `FromSql` for enums and
2//! structs representing PostgreSQL composite types. Works with Diesel 2.3.
3
4use 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/// Macro implementing traits for using a type with database composite types.
127///
128/// This macro will generate simple boilerplate implementations of the `ToSql`
129/// and `FromSql` traits from Diesel.
130///
131/// When deriving using this macro, you must also derive or otherwise implement
132/// [`std::fmt::Debug`]. Additionally, derive `AsExpression` if the type is to
133/// be used for serialisation, and/or derive `FromSqlRow` if the type is to be
134/// used for deserialisation.
135///
136/// If referencing a remote stub type (eg. one generated by the Diesel CLI into
137/// a "schema.rs" file), set the `sql_type` attribute to the path to this type.
138/// Otherwise, to instruct the macro to define a type, specify the database type
139/// with the `postgres_type` attribute, and pass a definable identifier as the
140/// `sql_type` attribute. The defined type inherits the visibility of the type
141/// on which the derive macro is called.
142///
143/// # Examples
144///
145/// ## Enum
146///
147/// Schema:
148///
149/// ```sql
150/// create type my_enum as enum (
151/// 	'variantOne',
152/// 	'variantTwo',
153/// 	'other variant'
154/// );
155/// ```
156///
157/// Model:
158///
159/// ```rs
160/// #[derive(Debug, Composite, FromSqlRow, AsExpression)]
161/// #[diesel(sql_type = PgMyEnum)]
162/// #[diesel_composite(postgres_type(name = "my_enum"), rename_all = "camelCase")]
163/// enum MyEnum {
164/// 	VariantOne,
165/// 	VariantTwo,
166/// 	#[diesel_composite(rename = "other variant")]
167/// 	Other,
168/// }
169/// ```
170///
171/// ## Struct
172///
173/// Schema:
174///
175/// ```sql
176/// create type my_struct as (
177/// 	field_one text,
178/// 	field_two integer
179/// );
180/// ```
181///
182/// Model:
183///
184/// ```rs
185/// #[derive(Debug, Composite, FromSqlRow, AsExpression)]
186/// #[diesel(sql_type = PgMyStruct)]
187/// #[diesel_composite(postgres_type(name = "my_struct"))]
188/// struct MyStruct {
189/// 	#[diesel_composite(sql_type = diesel::sql_types::Text)]
190/// 	field_one: String,
191/// 	#[diesel_composite(sql_type = diesel::sql_types::Integer)]
192/// 	field_two: i32,
193/// }
194///
195/// // or
196///
197/// #[derive(Debug, Composite, FromSqlRow, AsExpression)]
198/// #[diesel(sql_type = PgMyStruct)]
199/// #[diesel_composite(postgres_type(name = "my_struct"))]
200/// struct MyStruct(
201/// 	#[diesel_composite(sql_type = diesel::sql_types::Text)]
202/// 	String,
203/// 	#[diesel_composite(sql_type = diesel::sql_types::Integer)]
204/// 	i32,
205/// );
206/// ```
207///
208/// # Attributes
209///
210/// - **`sql_type`** (required): Specify the path to (or identifier of, if
211/// 	`postgres_type` is specified) the mapped type. The macro will recognise
212/// 	this field within `#[diesel]` attributes to avoid duplication when
213/// 	deriving `AsExpression` too.
214/// - **`postgres_type`**: Generate a stub type and specify the PostgreSQL
215/// 	identifier. See the Diesel documentation for the `SqlType` macro for
216/// 	information about the format of this attribute's content.
217/// - **`rename_all`** (enums only): Rename the enum variants according to the
218/// 	provided casing specification; valid values are `"lowercase"`,
219/// 	`"UPPERCASE"`, `"camelCase"`, `"PascalCase"`, `"snake_case"`,
220/// 	`"SCREAMING_SNAKE_CASE"`, `"kebab-case"`, and `"SCREAMING-KEBAB-CASE"`.
221/// 	Defaults to `"snake_case"` if omitted.
222/// - **`serialize`** and **`deserialize`**: Only generate the `ToSql` or
223/// 	`FromSql` implementation respectively. If neither is specified, both
224/// 	implementations are generated by default.
225///
226/// ## On variants
227///
228/// - **`rename`**: Rename this variant to a specific value.
229///
230/// ## On fields
231///
232/// - **`sql_type`** (required): Specify the path to the mapped type for this
233/// 	field.
234#[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}