use quote::{ToTokens, format_ident};
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct FiltersNames {
pub query: syn::Ident,
pub filters: syn::Ident,
pub sort_field: syn::Ident,
pub sort_details: syn::Ident,
pub input: syn::Ident,
pub page: syn::Ident,
pub query_params: syn::Ident,
}
pub trait Conventions {
fn filters_names(input: &syn::Ident) -> syn::Result<FiltersNames>;
fn check_model_name(name: &syn::Ident) -> syn::Result<()>;
fn check_merge_patch_name<S: ToTokens + ?Sized>(
name: &syn::Ident,
span: &S,
what: &str,
) -> syn::Result<()>;
fn sort_variant(field: &syn::Ident, span: proc_macro2::Span) -> syn::Ident;
fn filter_field(field: &syn::Ident, operator: &str, span: proc_macro2::Span) -> syn::Ident;
fn check_wrapper_name<S: ToTokens + ?Sized>(
name: &syn::Ident,
span: &S,
what: &str,
) -> syn::Result<()>;
#[must_use]
fn core_crate() -> syn::Path {
syn::parse_quote!(::queryweave)
}
#[must_use]
fn api_crate() -> syn::Path {
syn::parse_quote!(::queryweave_api)
}
#[must_use]
fn sql_crate() -> syn::Path {
syn::parse_quote!(::queryweave_sql)
}
#[must_use]
fn integrations() -> Vec<&'static dyn crate::integrations::Integration> {
crate::integrations::BUILT_IN.to_vec()
}
}
pub struct DefaultConventions;
impl Conventions for DefaultConventions {
fn filters_names(input: &syn::Ident) -> syn::Result<FiltersNames> {
let parts = crate::names::pascal_case_segments(input, input, "struct", 2)?;
let (last, init) = parts
.split_last()
.map_or(("", &[][..]), |(last, init)| (last.as_str(), init));
let without_last = init.join("");
Ok(FiltersNames {
query: format_ident!("{without_last}sQuery{last}"),
filters: format_ident!("{without_last}sFilterOptions{last}"),
sort_field: format_ident!("{without_last}SortField{last}"),
sort_details: format_ident!("{without_last}SortDetails{last}"),
input: input.clone(),
page: format_ident!("{without_last}sPage{last}"),
query_params: format_ident!("{without_last}sQueryParams"),
})
}
fn check_model_name(name: &syn::Ident) -> syn::Result<()> {
crate::names::pascal_case_segments(name, name, "struct", 2).map(drop)
}
fn check_merge_patch_name<S: ToTokens + ?Sized>(
name: &syn::Ident,
span: &S,
what: &str,
) -> syn::Result<()> {
crate::names::merge_patch_segments(name, span, what).map(drop)
}
fn sort_variant(field: &syn::Ident, span: proc_macro2::Span) -> syn::Ident {
let pascal_cased = field
.to_string()
.split('_')
.map(crate::utils::uppercase_first_letter)
.collect::<String>();
format_ident!("{pascal_cased}", span = span)
}
fn filter_field(field: &syn::Ident, operator: &str, span: proc_macro2::Span) -> syn::Ident {
if operator == "bool" {
format_ident!("{field}", span = span)
} else {
format_ident!("{field}_{operator}", span = span)
}
}
fn check_wrapper_name<S: ToTokens + ?Sized>(
name: &syn::Ident,
span: &S,
what: &str,
) -> syn::Result<()> {
crate::names::pascal_case_segments(name, span, what, 1).map(drop)
}
}
#[cfg(test)]
mod tests {
use super::{Conventions, DefaultConventions, FiltersNames};
use pretty_assertions::assert_eq;
use quote::format_ident;
#[test]
fn the_default_filters_names_follow_the_struct_segments() {
assert_eq!(
DefaultConventions::filters_names(&format_ident!("OrderLineModel")).unwrap(),
FiltersNames {
query: format_ident!("OrderLinesQueryModel"),
filters: format_ident!("OrderLinesFilterOptionsModel"),
sort_field: format_ident!("OrderLineSortFieldModel"),
sort_details: format_ident!("OrderLineSortDetailsModel"),
input: format_ident!("OrderLineModel"),
page: format_ident!("OrderLinesPageModel"),
query_params: format_ident!("OrderLinesQueryParams"),
}
);
}
#[test]
fn the_default_filters_names_need_two_segments() {
assert!(DefaultConventions::filters_names(&format_ident!("Order")).is_err());
assert!(DefaultConventions::filters_names(&format_ident!("order_model")).is_err());
}
#[test]
fn the_default_model_name_needs_two_segments() {
assert!(DefaultConventions::check_model_name(&format_ident!("OrderModel")).is_ok());
assert!(DefaultConventions::check_model_name(&format_ident!("Order")).is_err());
}
#[test]
fn the_default_merge_patch_name_starts_with_merge_patch() {
let name = format_ident!("MergePatchOrderModel");
assert!(DefaultConventions::check_merge_patch_name(&name, &name, "struct").is_ok());
let name = format_ident!("OrderPatch");
assert!(DefaultConventions::check_merge_patch_name(&name, &name, "struct").is_err());
}
#[test]
fn the_default_sort_variant_is_the_field_pascal_cased() {
let span = proc_macro2::Span::call_site();
assert_eq!(
DefaultConventions::sort_variant(&format_ident!("delivery_date"), span),
format_ident!("DeliveryDate")
);
assert_eq!(
DefaultConventions::sort_variant(&format_ident!("value"), span),
format_ident!("Value")
);
}
#[test]
fn the_default_filter_field_is_suffixed_with_the_operator_except_bool() {
let span = proc_macro2::Span::call_site();
let field = format_ident!("username");
assert_eq!(
DefaultConventions::filter_field(&field, "sw", span),
format_ident!("username_sw")
);
assert_eq!(
DefaultConventions::filter_field(&field, "bool", span),
format_ident!("username")
);
}
#[test]
fn the_default_runtime_crates_are_named_by_absolute_path() {
let path = |path: syn::Path| quote::ToTokens::to_token_stream(&path).to_string();
assert_eq!(path(DefaultConventions::core_crate()), ":: queryweave");
assert_eq!(path(DefaultConventions::api_crate()), ":: queryweave_api");
assert_eq!(path(DefaultConventions::sql_crate()), ":: queryweave_sql");
}
#[test]
fn the_default_wrapper_name_is_pascal_cased() {
let name = format_ident!("OrdersQueryModelWrapper");
assert!(DefaultConventions::check_wrapper_name(&name, &name, "wrapper").is_ok());
let name = format_ident!("orders_wrapper");
assert!(DefaultConventions::check_wrapper_name(&name, &name, "wrapper").is_err());
}
}