refactor: convert to_domain methods to From/TryFrom trait impls

Replace ad-hoc to_domain/into_domain conversion methods on SQL
repository structs with idiomatic From and TryFrom trait implementations
on the record types, following standard Rust conventions.
This commit is contained in:
Jon Seager 2026-02-13 13:35:08 +00:00
parent 590868e51f
commit 7330e5b59e
No known key found for this signature in database
13 changed files with 345 additions and 370 deletions

View file

@ -17,20 +17,6 @@ impl SqlAiUsageRepository {
pub fn new(pool: DatabasePool) -> Self {
Self { pool }
}
fn to_domain(record: AiUsageRecord) -> AiUsage {
AiUsage {
id: AiUsageId::from(record.id),
user_id: UserId::from(record.user_id),
model: record.model,
endpoint: record.endpoint,
prompt_tokens: record.prompt_tokens,
completion_tokens: record.completion_tokens,
total_tokens: record.total_tokens,
cost: record.cost,
created_at: record.created_at,
}
}
}
#[async_trait]
@ -54,7 +40,7 @@ impl AiUsageRepository for SqlAiUsageRepository {
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn summary_for_user(&self, user_id: UserId) -> Result<AiUsageSummary, RepositoryError> {
@ -98,6 +84,22 @@ struct AiUsageRecord {
created_at: DateTime<Utc>,
}
impl From<AiUsageRecord> for AiUsage {
fn from(record: AiUsageRecord) -> Self {
AiUsage {
id: AiUsageId::from(record.id),
user_id: UserId::from(record.user_id),
model: record.model,
endpoint: record.endpoint,
prompt_tokens: record.prompt_tokens,
completion_tokens: record.completion_tokens,
total_tokens: record.total_tokens,
cost: record.cost,
created_at: record.created_at,
}
}
}
#[derive(sqlx::FromRow)]
#[allow(clippy::struct_field_names)]
struct AiUsageSummaryRecord {

View file

@ -17,26 +17,6 @@ impl SqlPasskeyCredentialRepository {
pub fn new(pool: DatabasePool) -> Self {
Self { pool }
}
fn to_domain(record: PasskeyCredentialRecord) -> PasskeyCredential {
let PasskeyCredentialRecord {
id,
user_id,
credential_json,
name,
created_at,
last_used_at,
} = record;
PasskeyCredential {
id: PasskeyCredentialId::from(id),
user_id: UserId::from(user_id),
credential_json,
name,
created_at,
last_used_at,
}
}
}
#[async_trait]
@ -61,7 +41,7 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
RepositoryError::unexpected(format!("failed to insert passkey credential: {err}"))
})?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get(&self, id: PasskeyCredentialId) -> Result<PasskeyCredential, RepositoryError> {
@ -82,7 +62,7 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
}
})?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn list_by_user(
@ -104,7 +84,7 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
RepositoryError::unexpected(format!("failed to list passkey credentials: {err}"))
})?;
Ok(records.into_iter().map(Self::to_domain).collect())
Ok(records.into_iter().map(Into::into).collect())
}
async fn list_all(&self) -> Result<Vec<PasskeyCredential>, RepositoryError> {
@ -123,7 +103,7 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
))
})?;
Ok(records.into_iter().map(Self::to_domain).collect())
Ok(records.into_iter().map(Into::into).collect())
}
async fn update_credential_json(
@ -183,3 +163,16 @@ struct PasskeyCredentialRecord {
created_at: DateTime<Utc>,
last_used_at: Option<DateTime<Utc>>,
}
impl From<PasskeyCredentialRecord> for PasskeyCredential {
fn from(record: PasskeyCredentialRecord) -> Self {
PasskeyCredential {
id: PasskeyCredentialId::from(record.id),
user_id: UserId::from(record.user_id),
credential_json: record.credential_json,
name: record.name,
created_at: record.created_at,
last_used_at: record.last_used_at,
}
}
}

View file

@ -17,26 +17,6 @@ impl SqlRegistrationTokenRepository {
pub fn new(pool: DatabasePool) -> Self {
Self { pool }
}
fn to_domain(record: RegistrationTokenRecord) -> RegistrationToken {
let RegistrationTokenRecord {
id,
token_hash,
created_at,
expires_at,
used_at,
used_by_user_id,
} = record;
RegistrationToken {
id: RegistrationTokenId::from(id),
token_hash,
created_at,
expires_at,
used_at,
used_by_user_id: used_by_user_id.map(UserId::from),
}
}
}
#[async_trait]
@ -61,7 +41,7 @@ impl RegistrationTokenRepository for SqlRegistrationTokenRepository {
RepositoryError::unexpected(format!("failed to insert registration token: {err}"))
})?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get_by_token_hash(
@ -83,7 +63,7 @@ impl RegistrationTokenRepository for SqlRegistrationTokenRepository {
})?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn mark_used(
@ -119,3 +99,16 @@ struct RegistrationTokenRecord {
used_at: Option<DateTime<Utc>>,
used_by_user_id: Option<i64>,
}
impl From<RegistrationTokenRecord> for RegistrationToken {
fn from(record: RegistrationTokenRecord) -> Self {
RegistrationToken {
id: RegistrationTokenId::from(record.id),
token_hash: record.token_hash,
created_at: record.created_at,
expires_at: record.expires_at,
used_at: record.used_at,
used_by_user_id: record.used_by_user_id.map(UserId::from),
}
}
}

View file

@ -16,24 +16,6 @@ impl SqlSessionRepository {
pub fn new(pool: DatabasePool) -> Self {
Self { pool }
}
fn to_domain(record: SessionRecord) -> Session {
let SessionRecord {
id,
user_id,
session_token_hash,
created_at,
expires_at,
} = record;
Session::new(
SessionId::from(id),
UserId::from(user_id),
session_token_hash,
created_at,
expires_at,
)
}
}
#[async_trait]
@ -59,7 +41,7 @@ impl SessionRepository for SqlSessionRepository {
RepositoryError::unexpected(format!("failed to insert session: {err}"))
})?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get(&self, id: SessionId) -> Result<Session, RepositoryError> {
@ -72,7 +54,7 @@ impl SessionRepository for SqlSessionRepository {
.map_err(|err| RepositoryError::unexpected(format!("failed to get session: {err}")))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Session, RepositoryError> {
@ -87,7 +69,7 @@ impl SessionRepository for SqlSessionRepository {
})?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn delete(&self, id: SessionId) -> Result<(), RepositoryError> {
@ -124,3 +106,15 @@ struct SessionRecord {
created_at: chrono::DateTime<Utc>,
expires_at: chrono::DateTime<Utc>,
}
impl From<SessionRecord> for Session {
fn from(record: SessionRecord) -> Self {
Session::new(
SessionId::from(record.id),
UserId::from(record.user_id),
record.session_token_hash,
record.created_at,
record.expires_at,
)
}
}

View file

@ -17,28 +17,6 @@ impl SqlTokenRepository {
pub fn new(pool: DatabasePool) -> Self {
Self { pool }
}
fn to_domain(record: TokenRecord) -> Token {
let TokenRecord {
id,
user_id,
token_hash,
name,
created_at,
last_used_at,
revoked_at,
} = record;
Token::new(
TokenId::from(id),
UserId::from(user_id),
token_hash,
name,
created_at,
last_used_at,
revoked_at,
)
}
}
#[async_trait]
@ -67,7 +45,7 @@ impl TokenRepository for SqlTokenRepository {
RepositoryError::unexpected(err.to_string())
})?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get(&self, id: TokenId) -> Result<Token, RepositoryError> {
@ -80,7 +58,7 @@ impl TokenRepository for SqlTokenRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Token, RepositoryError> {
@ -93,7 +71,7 @@ impl TokenRepository for SqlTokenRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn list_by_user(&self, user_id: UserId) -> Result<Vec<Token>, RepositoryError> {
@ -105,7 +83,7 @@ impl TokenRepository for SqlTokenRepository {
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Ok(records.into_iter().map(Self::to_domain).collect())
Ok(records.into_iter().map(Into::into).collect())
}
async fn revoke(&self, id: TokenId) -> Result<Token, RepositoryError> {
@ -120,7 +98,7 @@ impl TokenRepository for SqlTokenRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
match record {
Some(record) => Ok(Self::to_domain(record)),
Some(record) => Ok(record.into()),
None => Err(RepositoryError::NotFound),
}
}
@ -149,3 +127,17 @@ struct TokenRecord {
last_used_at: Option<DateTime<Utc>>,
revoked_at: Option<DateTime<Utc>>,
}
impl From<TokenRecord> for Token {
fn from(record: TokenRecord) -> Self {
Token::new(
TokenId::from(record.id),
UserId::from(record.user_id),
record.token_hash,
record.name,
record.created_at,
record.last_used_at,
record.revoked_at,
)
}
}

View file

@ -17,17 +17,6 @@ impl SqlUserRepository {
pub fn new(pool: DatabasePool) -> Self {
Self { pool }
}
fn to_domain(record: UserRecord) -> User {
let UserRecord {
id,
username,
uuid,
created_at,
} = record;
User::new(UserId::from(id), username, uuid, created_at)
}
}
#[async_trait]
@ -49,7 +38,7 @@ impl UserRepository for SqlUserRepository {
RepositoryError::unexpected(err.to_string())
})?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get(&self, id: UserId) -> Result<User, RepositoryError> {
@ -62,7 +51,7 @@ impl UserRepository for SqlUserRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get_by_username(&self, username: &str) -> Result<User, RepositoryError> {
@ -75,7 +64,7 @@ impl UserRepository for SqlUserRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get_by_uuid(&self, uuid: &str) -> Result<User, RepositoryError> {
@ -88,7 +77,7 @@ impl UserRepository for SqlUserRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn exists(&self) -> Result<bool, RepositoryError> {
@ -110,7 +99,7 @@ impl UserRepository for SqlUserRepository {
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Ok(records.into_iter().map(Self::to_domain).collect())
Ok(records.into_iter().map(Into::into).collect())
}
}
@ -121,3 +110,14 @@ struct UserRecord {
uuid: String,
created_at: DateTime<Utc>,
}
impl From<UserRecord> for User {
fn from(record: UserRecord) -> Self {
User::new(
UserId::from(record.id),
record.username,
record.uuid,
record.created_at,
)
}
}

View file

@ -49,40 +49,6 @@ impl SqlBagRepository {
}
}
fn to_domain(record: BagRecord) -> Bag {
Bag {
id: BagId::new(record.id),
roast_id: RoastId::new(record.roast_id),
roast_date: record.roast_date,
amount: record.amount,
remaining: record.remaining,
closed: record.closed,
finished_at: record.finished_at,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
fn to_domain_with_roast(record: BagWithRoastRecord) -> BagWithRoast {
BagWithRoast {
bag: Bag {
id: BagId::new(record.id),
roast_id: RoastId::new(record.roast_id),
roast_date: record.roast_date,
amount: record.amount,
remaining: record.remaining,
closed: record.closed,
finished_at: record.finished_at,
created_at: record.created_at,
updated_at: record.updated_at,
},
roast_name: record.roast_name,
roaster_name: record.roaster_name,
roast_slug: record.roast_slug,
roaster_slug: record.roaster_slug,
}
}
fn build_where_clause(filter: &BagFilter) -> Option<String> {
let mut conditions = Vec::new();
@ -130,7 +96,7 @@ impl BagRepository for SqlBagRepository {
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get(&self, id: BagId) -> Result<Bag, RepositoryError> {
@ -147,7 +113,7 @@ impl BagRepository for SqlBagRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get_with_roast(&self, id: BagId) -> Result<BagWithRoast, RepositoryError> {
@ -160,7 +126,7 @@ impl BagRepository for SqlBagRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain_with_roast(record))
Ok(record.into())
}
async fn list(
@ -197,7 +163,7 @@ impl BagRepository for SqlBagRepository {
&count_query,
&order_clause,
sf.as_ref(),
|record| Ok(Self::to_domain_with_roast(record)),
|record: BagWithRoastRecord| Ok(record.into()),
)
.await
}
@ -233,7 +199,7 @@ impl BagRepository for SqlBagRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn delete(&self, id: BagId) -> Result<(), RepositoryError> {
@ -266,6 +232,22 @@ struct BagRecord {
updated_at: DateTime<Utc>,
}
impl From<BagRecord> for Bag {
fn from(record: BagRecord) -> Self {
Bag {
id: BagId::new(record.id),
roast_id: RoastId::new(record.roast_id),
roast_date: record.roast_date,
amount: record.amount,
remaining: record.remaining,
closed: record.closed,
finished_at: record.finished_at,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
}
#[derive(sqlx::FromRow)]
struct BagWithRoastRecord {
id: i64,
@ -282,3 +264,25 @@ struct BagWithRoastRecord {
roaster_name: String,
roaster_slug: String,
}
impl From<BagWithRoastRecord> for BagWithRoast {
fn from(record: BagWithRoastRecord) -> Self {
BagWithRoast {
bag: Bag {
id: BagId::new(record.id),
roast_id: RoastId::new(record.roast_id),
roast_date: record.roast_date,
amount: record.amount,
remaining: record.remaining,
closed: record.closed,
finished_at: record.finished_at,
created_at: record.created_at,
updated_at: record.updated_at,
},
roast_name: record.roast_name,
roaster_name: record.roaster_name,
roast_slug: record.roast_slug,
roaster_slug: record.roaster_slug,
}
}
}

View file

@ -33,6 +33,17 @@ const BASE_SELECT: &str = r"
LEFT JOIN gear g_fp ON br.filter_paper_id = g_fp.id
";
fn decode_quick_notes(raw: Option<String>) -> Vec<QuickNote> {
match raw {
Some(s) if !s.is_empty() => serde_json::from_str::<Vec<String>>(&s)
.unwrap_or_default()
.iter()
.filter_map(|v| QuickNote::from_str_value(v))
.collect(),
_ => Vec::new(),
}
}
#[derive(Clone)]
pub struct SqlBrewRepository {
pool: DatabasePool,
@ -56,17 +67,6 @@ impl SqlBrewRepository {
}
}
fn decode_quick_notes(raw: Option<String>) -> Vec<QuickNote> {
match raw {
Some(s) if !s.is_empty() => serde_json::from_str::<Vec<String>>(&s)
.unwrap_or_default()
.iter()
.filter_map(|v| QuickNote::from_str_value(v))
.collect(),
_ => Vec::new(),
}
}
fn encode_quick_notes(notes: &[QuickNote]) -> Option<String> {
if notes.is_empty() {
None
@ -82,52 +82,6 @@ impl SqlBrewRepository {
}
}
fn to_domain(record: BrewRecord) -> Brew {
Brew {
id: BrewId::new(record.id),
bag_id: BagId::new(record.bag_id),
coffee_weight: record.coffee_weight,
grinder_id: GearId::new(record.grinder_id),
grind_setting: record.grind_setting,
brewer_id: GearId::new(record.brewer_id),
filter_paper_id: record.filter_paper_id.map(GearId::new),
water_volume: record.water_volume,
water_temp: record.water_temp,
quick_notes: Self::decode_quick_notes(record.quick_notes),
brew_time: record.brew_time,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
fn to_domain_with_details(record: BrewWithDetailsRecord) -> BrewWithDetails {
BrewWithDetails {
brew: Brew {
id: BrewId::new(record.id),
bag_id: BagId::new(record.bag_id),
coffee_weight: record.coffee_weight,
grinder_id: GearId::new(record.grinder_id),
grind_setting: record.grind_setting,
brewer_id: GearId::new(record.brewer_id),
filter_paper_id: record.filter_paper_id.map(GearId::new),
water_volume: record.water_volume,
water_temp: record.water_temp,
quick_notes: Self::decode_quick_notes(record.quick_notes),
brew_time: record.brew_time,
created_at: record.created_at,
updated_at: record.updated_at,
},
roast_name: record.roast_name,
roaster_name: record.roaster_name,
roast_slug: record.roast_slug,
roaster_slug: record.roaster_slug,
grinder_name: record.grinder_name,
grinder_model: record.grinder_model,
brewer_name: record.brewer_name,
filter_paper_name: record.filter_paper_name,
}
}
fn build_where_clause(filter: &BrewFilter) -> Option<String> {
// SAFETY: Direct interpolation is safe here because `bag_id` is an i64 from a typed wrapper.
filter
@ -201,7 +155,7 @@ impl BrewRepository for SqlBrewRepository {
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get(&self, id: BrewId) -> Result<Brew, RepositoryError> {
@ -218,7 +172,7 @@ impl BrewRepository for SqlBrewRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get_with_details(&self, id: BrewId) -> Result<BrewWithDetails, RepositoryError> {
@ -231,7 +185,7 @@ impl BrewRepository for SqlBrewRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain_with_details(record))
Ok(record.into())
}
async fn list(
@ -271,7 +225,7 @@ impl BrewRepository for SqlBrewRepository {
&count_query,
&order_clause,
sf.as_ref(),
|record| Ok(Self::to_domain_with_details(record)),
|record: BrewWithDetailsRecord| Ok(record.into()),
)
.await
}
@ -339,7 +293,7 @@ impl BrewRepository for SqlBrewRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn delete(&self, id: BrewId) -> Result<(), RepositoryError> {
@ -376,6 +330,26 @@ struct BrewRecord {
updated_at: DateTime<Utc>,
}
impl From<BrewRecord> for Brew {
fn from(record: BrewRecord) -> Self {
Brew {
id: BrewId::new(record.id),
bag_id: BagId::new(record.bag_id),
coffee_weight: record.coffee_weight,
grinder_id: GearId::new(record.grinder_id),
grind_setting: record.grind_setting,
brewer_id: GearId::new(record.brewer_id),
filter_paper_id: record.filter_paper_id.map(GearId::new),
water_volume: record.water_volume,
water_temp: record.water_temp,
quick_notes: decode_quick_notes(record.quick_notes),
brew_time: record.brew_time,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
}
#[derive(sqlx::FromRow)]
struct BrewWithDetailsRecord {
id: i64,
@ -400,3 +374,33 @@ struct BrewWithDetailsRecord {
brewer_name: String,
filter_paper_name: Option<String>,
}
impl From<BrewWithDetailsRecord> for BrewWithDetails {
fn from(record: BrewWithDetailsRecord) -> Self {
BrewWithDetails {
brew: Brew {
id: BrewId::new(record.id),
bag_id: BagId::new(record.bag_id),
coffee_weight: record.coffee_weight,
grinder_id: GearId::new(record.grinder_id),
grind_setting: record.grind_setting,
brewer_id: GearId::new(record.brewer_id),
filter_paper_id: record.filter_paper_id.map(GearId::new),
water_volume: record.water_volume,
water_temp: record.water_temp,
quick_notes: decode_quick_notes(record.quick_notes),
brew_time: record.brew_time,
created_at: record.created_at,
updated_at: record.updated_at,
},
roast_name: record.roast_name,
roaster_name: record.roaster_name,
roast_slug: record.roast_slug,
roaster_slug: record.roaster_slug,
grinder_name: record.grinder_name,
grinder_model: record.grinder_model,
brewer_name: record.brewer_name,
filter_paper_name: record.filter_paper_name,
}
}
}

View file

@ -33,34 +33,6 @@ impl SqlCafeRepository {
CafeSortKey::Country => format!("LOWER(country) {dir_sql}, LOWER(name) ASC"),
}
}
fn into_domain(record: CafeRecord) -> Cafe {
let CafeRecord {
id,
name,
slug,
city,
country,
latitude,
longitude,
website,
created_at,
updated_at,
} = record;
Cafe {
id: CafeId::from(id),
name,
slug,
city,
country,
latitude,
longitude,
website,
created_at,
updated_at,
}
}
}
#[async_trait]
@ -96,7 +68,7 @@ impl CafeRepository for SqlCafeRepository {
RepositoryError::unexpected(err.to_string())
})?;
Ok(Self::into_domain(record))
Ok(record.into())
}
async fn get(&self, id: CafeId) -> Result<Cafe, RepositoryError> {
@ -109,7 +81,7 @@ impl CafeRepository for SqlCafeRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
match record {
Some(record) => Ok(Self::into_domain(record)),
Some(record) => Ok(record.into()),
None => Err(RepositoryError::NotFound),
}
}
@ -124,7 +96,7 @@ impl CafeRepository for SqlCafeRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
match record {
Some(record) => Ok(Self::into_domain(record)),
Some(record) => Ok(record.into()),
None => Err(RepositoryError::NotFound),
}
}
@ -148,7 +120,7 @@ impl CafeRepository for SqlCafeRepository {
count_query,
&order_clause,
sf.as_ref(),
|record| Ok(Self::into_domain(record)),
|record: CafeRecord| Ok(record.into()),
)
.await
}
@ -210,3 +182,20 @@ struct CafeRecord {
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl From<CafeRecord> for Cafe {
fn from(record: CafeRecord) -> Self {
Cafe {
id: CafeId::from(record.id),
name: record.name,
slug: record.slug,
city: record.city,
country: record.country,
latitude: record.latitude,
longitude: record.longitude,
website: record.website,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
}

View file

@ -57,35 +57,6 @@ impl SqlCupRepository {
}
}
fn to_domain(record: CupRecord) -> Cup {
Cup {
id: CupId::new(record.id),
roast_id: RoastId::new(record.roast_id),
cafe_id: CafeId::new(record.cafe_id),
created_at: record.created_at,
updated_at: record.updated_at,
}
}
fn to_domain_with_details(record: CupWithDetailsRecord) -> CupWithDetails {
CupWithDetails {
cup: Cup {
id: CupId::new(record.id),
roast_id: RoastId::new(record.roast_id),
cafe_id: CafeId::new(record.cafe_id),
created_at: record.created_at,
updated_at: record.updated_at,
},
roast_name: record.roast_name,
roaster_name: record.roaster_name,
roast_slug: record.roast_slug,
roaster_slug: record.roaster_slug,
cafe_name: record.cafe_name,
cafe_slug: record.cafe_slug,
cafe_city: record.cafe_city,
}
}
fn build_where_clause(filter: &CupFilter) -> Option<String> {
let mut conditions = Vec::new();
@ -121,7 +92,7 @@ impl CupRepository for SqlCupRepository {
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn get(&self, id: CupId) -> Result<Cup, RepositoryError> {
@ -134,7 +105,7 @@ impl CupRepository for SqlCupRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
match record {
Some(record) => Ok(Self::to_domain(record)),
Some(record) => Ok(record.into()),
None => Err(RepositoryError::NotFound),
}
}
@ -149,7 +120,7 @@ impl CupRepository for SqlCupRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain_with_details(record))
Ok(record.into())
}
async fn list(
@ -189,7 +160,7 @@ impl CupRepository for SqlCupRepository {
&count_query,
&order_clause,
sf.as_ref(),
|record| Ok(Self::to_domain_with_details(record)),
|record: CupWithDetailsRecord| Ok(record.into()),
)
.await
}
@ -226,7 +197,7 @@ impl CupRepository for SqlCupRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
Ok(record.into())
}
async fn delete(&self, id: CupId) -> Result<(), RepositoryError> {
@ -253,6 +224,18 @@ struct CupRecord {
updated_at: DateTime<Utc>,
}
impl From<CupRecord> for Cup {
fn from(record: CupRecord) -> Self {
Cup {
id: CupId::new(record.id),
roast_id: RoastId::new(record.roast_id),
cafe_id: CafeId::new(record.cafe_id),
created_at: record.created_at,
updated_at: record.updated_at,
}
}
}
#[derive(sqlx::FromRow)]
struct CupWithDetailsRecord {
id: i64,
@ -268,3 +251,24 @@ struct CupWithDetailsRecord {
cafe_slug: String,
cafe_city: String,
}
impl From<CupWithDetailsRecord> for CupWithDetails {
fn from(record: CupWithDetailsRecord) -> Self {
CupWithDetails {
cup: Cup {
id: CupId::new(record.id),
roast_id: RoastId::new(record.roast_id),
cafe_id: CafeId::new(record.cafe_id),
created_at: record.created_at,
updated_at: record.updated_at,
},
roast_name: record.roast_name,
roaster_name: record.roaster_name,
roast_slug: record.roast_slug,
roaster_slug: record.roaster_slug,
cafe_name: record.cafe_name,
cafe_slug: record.cafe_slug,
cafe_city: record.cafe_city,
}
}
}

View file

@ -36,21 +36,6 @@ impl SqlGearRepository {
}
}
fn to_domain(record: GearRecord) -> Result<Gear, RepositoryError> {
let category = GearCategory::from_str(&record.category).map_err(|()| {
RepositoryError::unexpected(format!("invalid category: {}", record.category))
})?;
Ok(Gear {
id: GearId::new(record.id),
category,
make: record.make,
model: record.model,
created_at: record.created_at,
updated_at: record.updated_at,
})
}
fn build_where_clause(filter: &GearFilter) -> Option<&'static str> {
filter.category.as_ref().map(|category| match category {
GearCategory::Grinder => "category = 'grinder'",
@ -80,7 +65,7 @@ impl GearRepository for SqlGearRepository {
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Self::to_domain(record)
record.try_into()
}
async fn get(&self, id: GearId) -> Result<Gear, RepositoryError> {
@ -97,7 +82,7 @@ impl GearRepository for SqlGearRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Self::to_domain(record)
record.try_into()
}
async fn list(
@ -134,7 +119,7 @@ impl GearRepository for SqlGearRepository {
&count_query,
&order_clause,
sf.as_ref(),
Self::to_domain,
|record: GearRecord| record.try_into(),
)
.await
}
@ -159,7 +144,7 @@ impl GearRepository for SqlGearRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Self::to_domain(record)
record.try_into()
}
async fn delete(&self, id: GearId) -> Result<(), RepositoryError> {
@ -188,3 +173,22 @@ struct GearRecord {
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl TryFrom<GearRecord> for Gear {
type Error = RepositoryError;
fn try_from(record: GearRecord) -> Result<Self, Self::Error> {
let category = GearCategory::from_str(&record.category).map_err(|()| {
RepositoryError::unexpected(format!("invalid category: {}", record.category))
})?;
Ok(Gear {
id: GearId::new(record.id),
category,
make: record.make,
model: record.model,
created_at: record.created_at,
updated_at: record.updated_at,
})
}
}

View file

@ -33,28 +33,6 @@ impl SqlRoasterRepository {
RoasterSortKey::City => format!("LOWER(COALESCE(city, '')) {dir_sql}, LOWER(name) ASC"),
}
}
fn into_domain(record: RoasterRecord) -> Roaster {
let RoasterRecord {
id,
name,
slug,
country,
city,
homepage,
created_at,
} = record;
Roaster {
id: RoasterId::from(id),
name,
slug,
country,
city,
homepage,
created_at,
}
}
}
#[async_trait]
@ -87,7 +65,7 @@ impl RoasterRepository for SqlRoasterRepository {
RepositoryError::unexpected(err.to_string())
})?;
Ok(Self::into_domain(record))
Ok(record.into())
}
async fn get(&self, id: RoasterId) -> Result<Roaster, RepositoryError> {
@ -100,7 +78,7 @@ impl RoasterRepository for SqlRoasterRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
match record {
Some(record) => Ok(Self::into_domain(record)),
Some(record) => Ok(record.into()),
None => Err(RepositoryError::NotFound),
}
}
@ -115,7 +93,7 @@ impl RoasterRepository for SqlRoasterRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
match record {
Some(record) => Ok(Self::into_domain(record)),
Some(record) => Ok(record.into()),
None => Err(RepositoryError::NotFound),
}
}
@ -140,7 +118,7 @@ impl RoasterRepository for SqlRoasterRepository {
count_query,
&order_clause,
sf.as_ref(),
|record| Ok(Self::into_domain(record)),
|record: RoasterRecord| Ok(record.into()),
)
.await
}
@ -206,3 +184,17 @@ struct RoasterRecord {
homepage: Option<String>,
created_at: DateTime<Utc>,
}
impl From<RoasterRecord> for Roaster {
fn from(record: RoasterRecord) -> Self {
Roaster {
id: RoasterId::from(record.id),
name: record.name,
slug: record.slug,
country: record.country,
city: record.city,
homepage: record.homepage,
created_at: record.created_at,
}
}
}

View file

@ -104,7 +104,7 @@ impl RoastRepository for SqlRoastRepository {
map_insert_error(err, "unknown roaster reference")
})?;
record.into_roast()
record.try_into()
}
async fn get(&self, id: RoastId) -> Result<Roast, RepositoryError> {
@ -115,7 +115,7 @@ impl RoastRepository for SqlRoastRepository {
.fetch_optional(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.map(RoastRecord::into_roast)
.map(Roast::try_from)
.transpose()?
.ok_or(RepositoryError::NotFound)
}
@ -131,7 +131,7 @@ impl RoastRepository for SqlRoastRepository {
.fetch_optional(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.map(RoastWithRoasterRecord::into_with_roaster)
.map(RoastWithRoaster::try_from)
.transpose()?
.ok_or(RepositoryError::NotFound)
}
@ -149,7 +149,7 @@ impl RoastRepository for SqlRoastRepository {
.fetch_optional(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.map(RoastRecord::into_roast)
.map(Roast::try_from)
.transpose()?
.ok_or(RepositoryError::NotFound)
}
@ -184,7 +184,7 @@ impl RoastRepository for SqlRoastRepository {
count_query,
&order_clause,
sf.as_ref(),
|record: RoastWithRoasterRecord| record.into_with_roaster(),
|record: RoastWithRoasterRecord| record.try_into(),
)
.await
}
@ -203,7 +203,7 @@ impl RoastRepository for SqlRoastRepository {
records
.into_iter()
.map(RoastWithRoasterRecord::into_with_roaster)
.map(RoastWithRoaster::try_from)
.collect()
}
@ -307,9 +307,11 @@ struct RoastRecord {
created_at: DateTime<Utc>,
}
impl RoastRecord {
fn into_roast(self) -> Result<Roast, RepositoryError> {
let tasting_notes = match self.tasting_notes {
impl TryFrom<RoastRecord> for Roast {
type Error = RepositoryError;
fn try_from(record: RoastRecord) -> Result<Self, Self::Error> {
let tasting_notes = match record.tasting_notes {
Some(raw) => from_str::<Vec<String>>(&raw).map_err(|err| {
RepositoryError::unexpected(format!("failed to decode tasting notes: {err}"))
})?,
@ -317,16 +319,16 @@ impl RoastRecord {
};
Ok(Roast {
id: RoastId::from(self.id),
roaster_id: RoasterId::from(self.roaster_id),
name: self.name,
slug: self.slug,
origin: self.origin,
region: self.region,
producer: self.producer,
process: self.process,
id: RoastId::from(record.id),
roaster_id: RoasterId::from(record.roaster_id),
name: record.name,
slug: record.slug,
origin: record.origin,
region: record.region,
producer: record.producer,
process: record.process,
tasting_notes,
created_at: self.created_at,
created_at: record.created_at,
})
}
}
@ -347,9 +349,11 @@ struct RoastWithRoasterRecord {
roaster_slug: String,
}
impl RoastWithRoasterRecord {
fn into_with_roaster(self) -> Result<RoastWithRoaster, RepositoryError> {
let tasting_notes = match self.tasting_notes {
impl TryFrom<RoastWithRoasterRecord> for RoastWithRoaster {
type Error = RepositoryError;
fn try_from(record: RoastWithRoasterRecord) -> Result<Self, Self::Error> {
let tasting_notes = match record.tasting_notes {
Some(raw) => from_str::<Vec<String>>(&raw).map_err(|err| {
RepositoryError::unexpected(format!("failed to decode tasting notes: {err}"))
})?,
@ -358,19 +362,19 @@ impl RoastWithRoasterRecord {
Ok(RoastWithRoaster {
roast: Roast {
id: RoastId::from(self.id),
roaster_id: RoasterId::from(self.roaster_id),
name: self.name,
slug: self.slug,
origin: self.origin,
region: self.region,
producer: self.producer,
process: self.process,
id: RoastId::from(record.id),
roaster_id: RoasterId::from(record.roaster_id),
name: record.name,
slug: record.slug,
origin: record.origin,
region: record.region,
producer: record.producer,
process: record.process,
tasting_notes,
created_at: self.created_at,
created_at: record.created_at,
},
roaster_name: self.roaster_name,
roaster_slug: self.roaster_slug,
roaster_name: record.roaster_name,
roaster_slug: record.roaster_slug,
})
}
}