brewlog/src/domain/gear.rs
Jon Seager b794d8a63f
feat(domain): add Gear entity with database migrations
Add Gear entity to track brewing equipment (grinders and brewers) with complete domain layer implementation.

Database changes:
- migrations/0006_add_gear.sql: Create gear table with category CHECK constraint and indexes
- migrations/0007_update_timeline_for_gear.sql: Document 'gear' as valid timeline entity type

Domain layer:
- Add GearId typed ID wrapper
- Create domain/gear.rs with:
  - GearCategory enum (Grinder/Brewer) with string conversion methods
  - Gear entity with make, model, notes fields
  - NewGear and UpdateGear DTOs
  - GearFilter for category-based filtering
  - GearSortKey with Make (default), Model, Category, CreatedAt options
- Add GearRepository trait to domain/repositories.rs with standard CRUD operations
- Register gear module in domain/mod.rs

This follows the same architectural pattern as the Bag entity.
2026-02-02 15:14:23 +00:00

119 lines
2.8 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::ids::GearId;
use super::listing::{SortDirection, SortKey};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GearCategory {
Grinder,
Brewer,
}
impl GearCategory {
pub fn as_str(&self) -> &'static str {
match self {
GearCategory::Grinder => "grinder",
GearCategory::Brewer => "brewer",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"grinder" => Some(GearCategory::Grinder),
"brewer" => Some(GearCategory::Brewer),
_ => None,
}
}
pub fn display_label(&self) -> &'static str {
match self {
GearCategory::Grinder => "Grinder",
GearCategory::Brewer => "Brewer",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Gear {
pub id: GearId,
pub category: GearCategory,
pub make: String,
pub model: String,
pub notes: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewGear {
pub category: GearCategory,
pub make: String,
pub model: String,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateGear {
pub make: Option<String>,
pub model: Option<String>,
pub notes: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct GearFilter {
pub category: Option<GearCategory>,
}
impl GearFilter {
pub fn all() -> Self {
Self::default()
}
pub fn for_category(category: GearCategory) -> Self {
Self {
category: Some(category),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum GearSortKey {
Make,
Model,
Category,
CreatedAt,
}
impl SortKey for GearSortKey {
fn default() -> Self {
GearSortKey::Make
}
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,
}
}
}