mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
* feat(labrinth): initial preferences structure * feat(labrinth): use partially to derive partial structs * feat(labrinth): preference defaults * fix(labrinth): derive debug * feat(labrinth): user preferences db setup * feat(labrinth): user preferences routes * fix(labrinth): nested partial structs * fix(labrinth): serialize enums as snake case * refactor(labrinth-derive): rename to component-derive * feat(component-derive): nested components * refactor(component-derive): change suffixes to prefixes * refactor(component-derive): rename edit to partial * feat(component-derive): skip serializing empty option * refactor(labrinth): wrap errors * feat(component-derive): diff function * style(labrinth): fmt * refactor(labrinth): use component derive and only store overrides * chore: update query cache * docs(labrinth): add preferences to openapi * fix(labrinth): lock row for update * remove: partially * refactor(component-derive): split impls to separate functions * feat(component-derive): wrap impls to isolate naming * refactor(labrinth): split out auth conditions * feat(labrinth): store auto * feat(labrinth): store hosting privacy * style(labrinth): cargo fmt
280 lines
7.6 KiB
Rust
280 lines
7.6 KiB
Rust
use darling::{FromDeriveInput, FromField};
|
|
use proc_macro2::TokenStream;
|
|
use quote::{format_ident, quote};
|
|
use syn::{Attribute, DeriveInput, Error, Ident, Result, Type, Visibility};
|
|
|
|
#[derive(Debug, FromDeriveInput)]
|
|
#[darling(supports(struct_named))]
|
|
struct Component {
|
|
ident: Ident,
|
|
vis: Visibility,
|
|
data: darling::ast::Data<(), ComponentField>,
|
|
}
|
|
|
|
#[derive(Debug, FromField)]
|
|
#[darling(attributes(component), forward_attrs)]
|
|
struct ComponentField {
|
|
ident: Option<Ident>,
|
|
vis: Visibility,
|
|
ty: Type,
|
|
attrs: Vec<Attribute>,
|
|
#[darling(default)]
|
|
synthetic: bool,
|
|
#[darling(default)]
|
|
nested: bool,
|
|
}
|
|
|
|
pub fn derive(input: &DeriveInput) -> Result<TokenStream> {
|
|
let Component { ident, vis, data } = Component::from_derive_input(input)?;
|
|
let fields = data
|
|
.take_struct()
|
|
.expect("macro only works on structs with named fields");
|
|
|
|
let fields = &fields.fields;
|
|
let struct_serial = struct_serial(&vis, &ident, fields)?;
|
|
let struct_partial = struct_partial(&vis, &ident, fields)?;
|
|
let impl_apply_to = impl_apply_to(&ident, fields);
|
|
let impl_into_diff_from = impl_into_diff_from(&ident, fields);
|
|
|
|
// `#[validate(nested)]` needs `Validate` in scope; `as _` avoids a name clash
|
|
let validate_import = if fields.iter().any(|field| field.nested) {
|
|
quote! { use validator::Validate as _; }
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
Ok(quote! {
|
|
#validate_import
|
|
|
|
#struct_serial
|
|
#struct_partial
|
|
|
|
const _: () = {
|
|
#impl_apply_to
|
|
#impl_into_diff_from
|
|
};
|
|
})
|
|
}
|
|
|
|
fn struct_serial(
|
|
vis: &Visibility,
|
|
ident: &Ident,
|
|
fields: &[ComponentField],
|
|
) -> Result<TokenStream> {
|
|
let ident_serial = format_ident!("Serial{ident}");
|
|
|
|
let fields = fields
|
|
.iter()
|
|
.filter_map(|field| {
|
|
if field.synthetic {
|
|
return None;
|
|
}
|
|
|
|
let ident = &field
|
|
.ident
|
|
.as_ref()
|
|
.expect("macro only works on structs with named fields");
|
|
let vis = &field.vis;
|
|
let ty = &field.ty;
|
|
let attrs = &field.attrs;
|
|
|
|
let (field_ty, validate_attr) = if field.nested {
|
|
let field_ty = match nested_type(ty, "Serial") {
|
|
Ok(field_ty) => field_ty,
|
|
Err(err) => return Some(Err(err)),
|
|
};
|
|
(field_ty, quote! { #[validate(nested)] })
|
|
} else {
|
|
(quote! { #ty }, quote! {})
|
|
};
|
|
|
|
Some(Ok(quote! {
|
|
#(#attrs)*
|
|
#validate_attr
|
|
#vis #ident: #field_ty
|
|
}))
|
|
})
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
Ok(quote! {
|
|
#[derive(
|
|
Debug,
|
|
Clone,
|
|
::serde::Serialize,
|
|
::serde::Deserialize,
|
|
::validator::Validate,
|
|
::utoipa::ToSchema,
|
|
)]
|
|
#vis struct #ident_serial {
|
|
#(#fields),*
|
|
}
|
|
})
|
|
}
|
|
|
|
fn struct_partial(
|
|
vis: &Visibility,
|
|
ident: &Ident,
|
|
fields: &[ComponentField],
|
|
) -> Result<TokenStream> {
|
|
let ident_partial = format_ident!("Partial{ident}");
|
|
|
|
let fields = fields
|
|
.iter()
|
|
.filter_map(|field| {
|
|
if field.synthetic {
|
|
return None;
|
|
}
|
|
|
|
let ident = &field
|
|
.ident
|
|
.as_ref()
|
|
.expect("macro only works on structs with named fields");
|
|
let vis = &field.vis;
|
|
let ty = &field.ty;
|
|
let attrs = &field.attrs;
|
|
|
|
let (inner_ty, validate_attr) = if field.nested {
|
|
let inner_ty = match nested_type(ty, "Partial") {
|
|
Ok(inner_ty) => inner_ty,
|
|
Err(err) => return Some(Err(err)),
|
|
};
|
|
(inner_ty, quote! { #[validate(nested)] })
|
|
} else {
|
|
(quote! { #ty }, quote! {})
|
|
};
|
|
|
|
let serde_attr = if !field.nested
|
|
&& let Type::Path(path) = ty
|
|
&& path
|
|
.path
|
|
.segments
|
|
.first()
|
|
.is_some_and(|segment| segment.ident == "Option")
|
|
{
|
|
quote! {
|
|
#[serde(
|
|
default,
|
|
skip_serializing_if = "::core::option::Option::is_none",
|
|
with = "::serde_with::rust::double_option"
|
|
)]
|
|
}
|
|
} else {
|
|
quote! { #[serde(default, skip_serializing_if = "::core::option::Option::is_none")] }
|
|
};
|
|
|
|
Some(Ok(quote! {
|
|
#(#attrs)*
|
|
#validate_attr
|
|
#serde_attr
|
|
#vis #ident: ::core::option::Option<#inner_ty>
|
|
}))
|
|
})
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
Ok(quote! {
|
|
#[derive(
|
|
Debug,
|
|
Clone,
|
|
::serde::Serialize,
|
|
::serde::Deserialize,
|
|
::validator::Validate,
|
|
::utoipa::ToSchema,
|
|
)]
|
|
#vis struct #ident_partial {
|
|
#(#fields),*
|
|
}
|
|
})
|
|
}
|
|
|
|
fn impl_apply_to(ident: &Ident, fields: &[ComponentField]) -> TokenStream {
|
|
let ident_partial = format_ident!("Partial{ident}");
|
|
|
|
let apply_fields = fields
|
|
.iter()
|
|
.filter_map(|field| {
|
|
if field.synthetic {
|
|
return None;
|
|
}
|
|
|
|
let ident = field
|
|
.ident
|
|
.as_ref()
|
|
.expect("macro only works on structs with named fields");
|
|
let apply_value = if field.nested {
|
|
quote! { t.apply_to(&mut component.#ident) }
|
|
} else {
|
|
quote! { component.#ident = t }
|
|
};
|
|
|
|
Some(quote! {
|
|
if let Some(t) = self.#ident {
|
|
#apply_value;
|
|
}
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
quote! {
|
|
impl #ident_partial {
|
|
pub fn apply_to(self, component: &mut #ident) {
|
|
#(#apply_fields)*
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn impl_into_diff_from(
|
|
ident: &Ident,
|
|
fields: &[ComponentField],
|
|
) -> TokenStream {
|
|
let ident_partial = format_ident!("Partial{ident}");
|
|
|
|
let diff_fields = fields
|
|
.iter()
|
|
.filter_map(|field| {
|
|
if field.synthetic {
|
|
return None;
|
|
}
|
|
|
|
let ident = field
|
|
.ident
|
|
.as_ref()
|
|
.expect("macro only works on structs with named fields");
|
|
let diff_value = if field.nested {
|
|
quote! { self.#ident.into_diff_from(&base.#ident) }
|
|
} else {
|
|
quote! { self.#ident }
|
|
};
|
|
|
|
Some(quote! {
|
|
#ident: (self.#ident != base.#ident).then(|| #diff_value)
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
quote! {
|
|
impl #ident {
|
|
pub fn into_diff_from(self, base: &Self) -> #ident_partial {
|
|
#ident_partial {
|
|
#(#diff_fields),*
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn nested_type(ty: &Type, prefix: &str) -> Result<TokenStream> {
|
|
if let Type::Path(path) = ty
|
|
&& let Some(segment) = path.path.segments.last()
|
|
{
|
|
// FIXME: Validate that nested type also derives component, prob by checking for component impl
|
|
let nested = format_ident!("{}{}", prefix, segment.ident);
|
|
Ok(quote! { #nested })
|
|
} else {
|
|
Err(Error::new_spanned(
|
|
ty,
|
|
"nested component fields must be a named path type",
|
|
))
|
|
}
|
|
}
|