refactor: replace hand-written SortKey impls with define_sort_key! macro

Add define_sort_key! macro to listing.rs and convert all 8 SortKey
enum + impl blocks to use it, eliminating ~270 lines of repetitive
boilerplate across roasters, roasts, bags, brews, cups, cafes, gear,
and timeline.
This commit is contained in:
Jon Seager 2026-02-13 14:32:02 +00:00
parent de49f7c3b1
commit bd4cb023fb
No known key found for this signature in database
9 changed files with 125 additions and 319 deletions

View file

@ -1,9 +1,9 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::define_sort_key;
use crate::domain::entity_type::EntityType; use crate::domain::entity_type::EntityType;
use crate::domain::ids::{BagId, GearId, TimelineEventId}; use crate::domain::ids::{BagId, GearId, TimelineEventId};
use crate::domain::listing::{SortDirection, SortKey};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineEventDetail { pub struct TimelineEventDetail {
@ -54,32 +54,7 @@ pub struct NewTimelineEvent {
pub brew_data: Option<TimelineBrewData>, pub brew_data: Option<TimelineBrewData>,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] define_sort_key!(pub TimelineSortKey {
pub enum TimelineSortKey { #[default]
OccurredAt, OccurredAt("occurred-at", Desc),
} });
impl SortKey for TimelineSortKey {
fn default() -> Self {
TimelineSortKey::OccurredAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"occurred-at" => Some(TimelineSortKey::OccurredAt),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
TimelineSortKey::OccurredAt => "occurred-at",
}
}
fn default_direction(self) -> SortDirection {
match self {
TimelineSortKey::OccurredAt => SortDirection::Desc,
}
}
}

View file

@ -1,9 +1,9 @@
use chrono::{DateTime, NaiveDate, Utc}; use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::define_sort_key;
use crate::domain::entity_type::EntityType; use crate::domain::entity_type::EntityType;
use crate::domain::ids::{BagId, RoastId}; use crate::domain::ids::{BagId, RoastId};
use crate::domain::listing::{SortDirection, SortKey};
use crate::domain::roasters::Roaster; use crate::domain::roasters::Roaster;
use crate::domain::roasts::Roast; use crate::domain::roasts::Roast;
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
@ -93,54 +93,16 @@ impl BagFilter {
} }
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] define_sort_key!(pub BagSortKey {
pub enum BagSortKey { #[default]
RoastDate, CreatedAt("created-at", Desc),
CreatedAt, RoastDate("roast-date", Desc),
UpdatedAt, UpdatedAt("updated-at", Desc),
Roaster, Roaster("roaster", Asc),
Roast, Roast("roast", Asc),
Status, Status("status", Asc),
FinishedAt, FinishedAt("finished-at", Desc),
} });
impl SortKey for BagSortKey {
fn default() -> Self {
BagSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"roast-date" => Some(BagSortKey::RoastDate),
"created-at" => Some(BagSortKey::CreatedAt),
"updated-at" => Some(BagSortKey::UpdatedAt),
"roaster" => Some(BagSortKey::Roaster),
"roast" => Some(BagSortKey::Roast),
"status" => Some(BagSortKey::Status),
"finished-at" => Some(BagSortKey::FinishedAt),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
BagSortKey::RoastDate => "roast-date",
BagSortKey::CreatedAt => "created-at",
BagSortKey::UpdatedAt => "updated-at",
BagSortKey::Roaster => "roaster",
BagSortKey::Roast => "roast",
BagSortKey::Status => "status",
BagSortKey::FinishedAt => "finished-at",
}
}
fn default_direction(self) -> SortDirection {
match self {
BagSortKey::Roaster | BagSortKey::Roast | BagSortKey::Status => SortDirection::Asc,
_ => SortDirection::Desc,
}
}
}
pub fn bag_timeline_event( pub fn bag_timeline_event(
bag: &Bag, bag: &Bag,

View file

@ -3,9 +3,9 @@ use std::str::FromStr;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::define_sort_key;
use crate::domain::entity_type::EntityType; use crate::domain::entity_type::EntityType;
use crate::domain::ids::{BagId, BrewId, GearId}; use crate::domain::ids::{BagId, BrewId, GearId};
use crate::domain::listing::{SortDirection, SortKey};
use crate::domain::timeline::{NewTimelineEvent, TimelineBrewData, TimelineEventDetail}; use crate::domain::timeline::{NewTimelineEvent, TimelineBrewData, TimelineEventDetail};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@ -272,36 +272,9 @@ impl BrewFilter {
} }
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] define_sort_key!(pub BrewSortKey {
pub enum BrewSortKey { #[default]
CreatedAt, CreatedAt("created-at", Desc),
CoffeeWeight, CoffeeWeight("coffee-weight", Desc),
WaterVolume, WaterVolume("water-volume", Desc),
} });
impl SortKey for BrewSortKey {
fn default() -> Self {
BrewSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"created-at" => Some(BrewSortKey::CreatedAt),
"coffee-weight" => Some(BrewSortKey::CoffeeWeight),
"water-volume" => Some(BrewSortKey::WaterVolume),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
BrewSortKey::CreatedAt => "created-at",
BrewSortKey::CoffeeWeight => "coffee-weight",
BrewSortKey::WaterVolume => "water-volume",
}
}
fn default_direction(self) -> SortDirection {
SortDirection::Desc
}
}

View file

@ -2,9 +2,9 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::normalize_optional_field; use super::normalize_optional_field;
use crate::define_sort_key;
use crate::domain::entity_type::EntityType; use crate::domain::entity_type::EntityType;
use crate::domain::ids::CafeId; use crate::domain::ids::CafeId;
use crate::domain::listing::{SortDirection, SortKey};
use crate::domain::roasters::is_valid_url_scheme; use crate::domain::roasters::is_valid_url_scheme;
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
@ -95,42 +95,10 @@ impl UpdateCafe {
} }
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] define_sort_key!(pub CafeSortKey {
pub enum CafeSortKey { #[default]
CreatedAt, CreatedAt("created-at", Desc),
Name, Name("name", Asc),
City, City("city", Asc),
Country, Country("country", Asc),
} });
impl SortKey for CafeSortKey {
fn default() -> Self {
CafeSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"created-at" => Some(CafeSortKey::CreatedAt),
"name" => Some(CafeSortKey::Name),
"city" => Some(CafeSortKey::City),
"country" => Some(CafeSortKey::Country),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
CafeSortKey::CreatedAt => "created-at",
CafeSortKey::Name => "name",
CafeSortKey::City => "city",
CafeSortKey::Country => "country",
}
}
fn default_direction(self) -> SortDirection {
match self {
CafeSortKey::CreatedAt => SortDirection::Desc,
_ => SortDirection::Asc,
}
}
}

View file

@ -1,9 +1,9 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::define_sort_key;
use crate::domain::entity_type::EntityType; use crate::domain::entity_type::EntityType;
use crate::domain::ids::{CafeId, CupId, RoastId}; use crate::domain::ids::{CafeId, CupId, RoastId};
use crate::domain::listing::{SortDirection, SortKey};
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -106,45 +106,11 @@ impl CupFilter {
} }
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] define_sort_key!(pub CupSortKey {
pub enum CupSortKey { #[default]
CreatedAt, CreatedAt("created-at", Desc),
CafeName, CafeName("cafe", Asc),
CafeCity, CafeCity("city", Asc),
RoastName, RoastName("roast", Asc),
RoasterName, RoasterName("roaster", Asc),
} });
impl SortKey for CupSortKey {
fn default() -> Self {
CupSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"created-at" => Some(CupSortKey::CreatedAt),
"cafe" => Some(CupSortKey::CafeName),
"city" => Some(CupSortKey::CafeCity),
"roast" => Some(CupSortKey::RoastName),
"roaster" => Some(CupSortKey::RoasterName),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
CupSortKey::CreatedAt => "created-at",
CupSortKey::CafeName => "cafe",
CupSortKey::CafeCity => "city",
CupSortKey::RoastName => "roast",
CupSortKey::RoasterName => "roaster",
}
}
fn default_direction(self) -> SortDirection {
match self {
CupSortKey::CreatedAt => SortDirection::Desc,
_ => SortDirection::Asc,
}
}
}

View file

@ -3,9 +3,9 @@ use std::str::FromStr;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::define_sort_key;
use crate::domain::entity_type::EntityType; use crate::domain::entity_type::EntityType;
use crate::domain::ids::GearId; use crate::domain::ids::GearId;
use crate::domain::listing::{SortDirection, SortKey};
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
@ -122,42 +122,10 @@ impl GearFilter {
} }
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] define_sort_key!(pub GearSortKey {
pub enum GearSortKey { #[default]
Make, CreatedAt("created-at", Desc),
Model, Make("make", Asc),
Category, Model("model", Asc),
CreatedAt, Category("category", Asc),
} });
impl SortKey for GearSortKey {
fn default() -> Self {
GearSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"make" => Some(GearSortKey::Make),
"model" => Some(GearSortKey::Model),
"category" => Some(GearSortKey::Category),
"created-at" => Some(GearSortKey::CreatedAt),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
GearSortKey::Make => "make",
GearSortKey::Model => "model",
GearSortKey::Category => "category",
GearSortKey::CreatedAt => "created-at",
}
}
fn default_direction(self) -> SortDirection {
match self {
GearSortKey::Make | GearSortKey::Model | GearSortKey::Category => SortDirection::Asc,
GearSortKey::CreatedAt => SortDirection::Desc,
}
}
}

View file

@ -2,9 +2,9 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::normalize_optional_field; use super::normalize_optional_field;
use crate::define_sort_key;
use crate::domain::entity_type::EntityType; use crate::domain::entity_type::EntityType;
use crate::domain::ids::RoasterId; use crate::domain::ids::RoasterId;
use crate::domain::listing::{SortDirection, SortKey};
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -99,42 +99,10 @@ impl UpdateRoaster {
} }
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] define_sort_key!(pub RoasterSortKey {
pub enum RoasterSortKey { #[default]
CreatedAt, CreatedAt("created-at", Desc),
Name, Name("name", Asc),
Country, Country("country", Asc),
City, City("city", Asc),
} });
impl SortKey for RoasterSortKey {
fn default() -> Self {
RoasterSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"created-at" => Some(RoasterSortKey::CreatedAt),
"name" => Some(RoasterSortKey::Name),
"country" => Some(RoasterSortKey::Country),
"city" => Some(RoasterSortKey::City),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
RoasterSortKey::CreatedAt => "created-at",
RoasterSortKey::Name => "name",
RoasterSortKey::Country => "country",
RoasterSortKey::City => "city",
}
}
fn default_direction(self) -> SortDirection {
match self {
RoasterSortKey::CreatedAt => SortDirection::Desc,
_ => SortDirection::Asc,
}
}
}

View file

@ -1,9 +1,9 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::define_sort_key;
use crate::domain::entity_type::EntityType; use crate::domain::entity_type::EntityType;
use crate::domain::ids::{RoastId, RoasterId}; use crate::domain::ids::{RoastId, RoasterId};
use crate::domain::listing::{SortDirection, SortKey};
use crate::domain::roasters::Roaster; use crate::domain::roasters::Roaster;
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
@ -61,48 +61,14 @@ pub struct UpdateRoast {
pub created_at: Option<DateTime<Utc>>, pub created_at: Option<DateTime<Utc>>,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] define_sort_key!(pub RoastSortKey {
pub enum RoastSortKey { #[default]
CreatedAt, CreatedAt("created-at", Desc),
Name, Name("name", Asc),
Roaster, Roaster("roaster", Asc),
Origin, Origin("origin", Asc),
Producer, Producer("producer", Asc),
} });
impl SortKey for RoastSortKey {
fn default() -> Self {
RoastSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"created-at" => Some(RoastSortKey::CreatedAt),
"name" => Some(RoastSortKey::Name),
"roaster" => Some(RoastSortKey::Roaster),
"origin" => Some(RoastSortKey::Origin),
"producer" => Some(RoastSortKey::Producer),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
RoastSortKey::CreatedAt => "created-at",
RoastSortKey::Name => "name",
RoastSortKey::Roaster => "roaster",
RoastSortKey::Origin => "origin",
RoastSortKey::Producer => "producer",
}
}
fn default_direction(self) -> SortDirection {
match self {
RoastSortKey::CreatedAt => SortDirection::Desc,
_ => SortDirection::Asc,
}
}
}
pub fn roast_timeline_event(roast: &Roast, roaster: &Roaster) -> NewTimelineEvent { pub fn roast_timeline_event(roast: &Roast, roaster: &Roaster) -> NewTimelineEvent {
let mut details = vec![TimelineEventDetail { let mut details = vec![TimelineEventDetail {

View file

@ -29,6 +29,66 @@ pub trait SortKey: Copy + Eq {
fn default_direction(self) -> SortDirection; fn default_direction(self) -> SortDirection;
} }
/// Generates a sort key enum and its `SortKey` trait implementation.
///
/// # Example
///
/// ```ignore
/// define_sort_key!(RoasterSortKey {
/// #[default]
/// CreatedAt("created-at", Desc),
/// Name("name", Asc),
/// Country("country", Asc),
/// City("city", Asc),
/// });
/// ```
#[macro_export]
macro_rules! define_sort_key {
(
$vis:vis $name:ident {
$(#[default] $default_variant:ident($default_query:literal, $default_dir:ident),)?
$($variant:ident($query:literal, $dir:ident)),*
$(,)?
}
) => {
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
$vis enum $name {
$($default_variant,)?
$($variant),*
}
impl $crate::domain::listing::SortKey for $name {
fn default() -> Self {
// The first arm expands for the #[default] variant
$( return $name::$default_variant; )?
// If no #[default], this won't compile — every invocation needs one
}
fn from_query(value: &str) -> Option<Self> {
match value {
$($default_query => Some($name::$default_variant),)?
$($query => Some($name::$variant),)*
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
$($name::$default_variant => $default_query,)?
$($name::$variant => $query,)*
}
}
fn default_direction(self) -> $crate::domain::listing::SortDirection {
match self {
$($name::$default_variant => $crate::domain::listing::SortDirection::$default_dir,)?
$($name::$variant => $crate::domain::listing::SortDirection::$dir,)*
}
}
}
};
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum PageSize { pub enum PageSize {
Limited(u32), Limited(u32),