chore(lint): enable clippy pedantic and restriction lints
- Add [lints.clippy] section to Cargo.toml with pedantic group - Cherry-pick restriction lints: dbg_macro (deny), todo, unwrap_used, expect_used (warn) - Allow noisy pedantic lints (missing_errors_doc, module_name_repetitions, etc.) - Fix unnecessary Result wrappers in token and user repository to_domain functions - Merge duplicate match arms in TimelineEventViewModel - Add justified #[allow] attributes for startup code and tests
This commit is contained in:
parent
500389583f
commit
2bd008a8d7
18 changed files with 93 additions and 77 deletions
20
Cargo.toml
20
Cargo.toml
|
|
@ -60,3 +60,23 @@ opt-level = 0
|
||||||
name = "server"
|
name = "server"
|
||||||
path = "tests/server/main.rs"
|
path = "tests/server/main.rs"
|
||||||
harness = true
|
harness = true
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
# Enable pedantic lints with lower priority so individual allows take precedence
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
|
# Pedantic lints to disable (too noisy or not applicable)
|
||||||
|
missing_errors_doc = "allow" # Would require extensive doc changes
|
||||||
|
missing_panics_doc = "allow" # Would require extensive doc changes
|
||||||
|
module_name_repetitions = "allow" # Common pattern in domain types (e.g., RoasterId in roasters)
|
||||||
|
must_use_candidate = "allow" # Too aggressive for this codebase
|
||||||
|
return_self_not_must_use = "allow" # Builder methods returning Self are common
|
||||||
|
needless_pass_by_value = "allow" # Generic T: ToString pattern and consumed value types
|
||||||
|
cast_possible_truncation = "allow" # Pagination values won't exceed u32 in practice
|
||||||
|
cast_sign_loss = "allow" # Database counts are non-negative
|
||||||
|
|
||||||
|
# Cherry-picked restriction lints (production safety)
|
||||||
|
dbg_macro = "deny" # Prevent debug macros in production
|
||||||
|
todo = "warn" # Flag TODOs for attention
|
||||||
|
unwrap_used = "warn" # Flag unwrap() for review; allow where justified
|
||||||
|
expect_used = "warn" # Flag expect() for review; allow where justified
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ pub(crate) async fn create_bag(
|
||||||
entity_id: bag.id.into_inner(),
|
entity_id: bag.id.into_inner(),
|
||||||
action: "added".to_string(),
|
action: "added".to_string(),
|
||||||
occurred_at: chrono::Utc::now(),
|
occurred_at: chrono::Utc::now(),
|
||||||
title: roast.name.to_string(),
|
title: roast.name.clone(),
|
||||||
details: vec![
|
details: vec![
|
||||||
TimelineEventDetail {
|
TimelineEventDetail {
|
||||||
label: "Roaster".to_string(),
|
label: "Roaster".to_string(),
|
||||||
|
|
@ -213,11 +213,14 @@ pub(crate) async fn update_bag(
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let request = query.into_request::<BagSortKey>();
|
let request = query.into_request::<BagSortKey>();
|
||||||
|
|
||||||
let body_update = payload.map(|Json(p)| p).unwrap_or(UpdateBag {
|
let body_update = payload.map_or(
|
||||||
remaining: None,
|
UpdateBag {
|
||||||
closed: None,
|
remaining: None,
|
||||||
finished_at: None,
|
closed: None,
|
||||||
});
|
finished_at: None,
|
||||||
|
},
|
||||||
|
|Json(p)| p,
|
||||||
|
);
|
||||||
|
|
||||||
let mut update = UpdateBag {
|
let mut update = UpdateBag {
|
||||||
remaining: body_update.remaining.or(update_params.remaining),
|
remaining: body_update.remaining.or(update_params.remaining),
|
||||||
|
|
@ -247,7 +250,7 @@ pub(crate) async fn update_bag(
|
||||||
entity_id: bag.id.into_inner(),
|
entity_id: bag.id.into_inner(),
|
||||||
action: "finished".to_string(),
|
action: "finished".to_string(),
|
||||||
occurred_at: chrono::Utc::now(),
|
occurred_at: chrono::Utc::now(),
|
||||||
title: roast.name.to_string(),
|
title: roast.name.clone(),
|
||||||
details: vec![
|
details: vec![
|
||||||
TimelineEventDetail {
|
TimelineEventDetail {
|
||||||
label: "Roaster".to_string(),
|
label: "Roaster".to_string(),
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ pub(crate) async fn list_gear(
|
||||||
let filter = match params.category {
|
let filter = match params.category {
|
||||||
Some(ref cat_str) => {
|
Some(ref cat_str) => {
|
||||||
let category = GearCategory::from_str(cat_str)
|
let category = GearCategory::from_str(cat_str)
|
||||||
.map_err(|_| AppError::validation("invalid category"))?;
|
.map_err(|()| AppError::validation("invalid category"))?;
|
||||||
GearFilter::for_category(category)
|
GearFilter::for_category(category)
|
||||||
}
|
}
|
||||||
None => GearFilter::all(),
|
None => GearFilter::all(),
|
||||||
|
|
@ -207,7 +207,7 @@ pub(crate) struct NewGearSubmission {
|
||||||
impl NewGearSubmission {
|
impl NewGearSubmission {
|
||||||
fn into_new_gear(self) -> Result<NewGear, AppError> {
|
fn into_new_gear(self) -> Result<NewGear, AppError> {
|
||||||
let category = GearCategory::from_str(&self.category)
|
let category = GearCategory::from_str(&self.category)
|
||||||
.map_err(|_| AppError::validation("invalid category"))?;
|
.map_err(|()| AppError::validation("invalid category"))?;
|
||||||
|
|
||||||
if self.make.trim().is_empty() {
|
if self.make.trim().is_empty() {
|
||||||
return Err(AppError::validation("make cannot be empty"));
|
return Err(AppError::validation("make cannot be empty"));
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ pub(crate) async fn list_roasts(
|
||||||
let s = s.trim();
|
let s = s.trim();
|
||||||
Some(s.parse::<RoasterId>().map_err(|_| {
|
Some(s.parse::<RoasterId>().map_err(|_| {
|
||||||
tracing::warn!("Invalid roaster_id: '{}'", s);
|
tracing::warn!("Invalid roaster_id: '{}'", s);
|
||||||
ApiError::from(AppError::validation(format!("Invalid roaster_id: '{}'", s)))
|
ApiError::from(AppError::validation(format!("Invalid roaster_id: '{s}'")))
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,7 @@ pub fn is_datastar_request(headers: &HeaderMap) -> bool {
|
||||||
headers
|
headers
|
||||||
.get("datastar-request")
|
.get("datastar-request")
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.map(|value| value.eq_ignore_ascii_case("true"))
|
.is_some_and(|value| value.eq_ignore_ascii_case("true"))
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_datastar_patch_headers(headers: &mut HeaderMap, selector: &'static str) {
|
pub fn set_datastar_patch_headers(headers: &mut HeaderMap, selector: &'static str) {
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,7 @@ async fn bootstrap_admin_user(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::expect_used)] // Startup: panicking is appropriate if signal handlers fail
|
||||||
async fn shutdown_signal() {
|
async fn shutdown_signal() {
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
signal::ctrl_c()
|
signal::ctrl_c()
|
||||||
|
|
@ -180,7 +181,7 @@ async fn shutdown_signal() {
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = ctrl_c => {},
|
() = ctrl_c => {},
|
||||||
_ = terminate => {},
|
() = terminate => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ impl<K: SortKey> ListRequest<K> {
|
||||||
return Self::new(1, self.page_size, self.sort_key, self.sort_direction);
|
return Self::new(1, self.page_size, self.sort_key, self.sort_direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
let last_page = (total.div_ceil(limit as u64)) as u32;
|
let last_page = (total.div_ceil(u64::from(limit))) as u32;
|
||||||
let adjusted_page = self.page.min(last_page.max(1));
|
let adjusted_page = self.page.min(last_page.max(1));
|
||||||
Self::new(
|
Self::new(
|
||||||
adjusted_page,
|
adjusted_page,
|
||||||
|
|
@ -199,7 +199,7 @@ impl<T> Page<T> {
|
||||||
if self.total == 0 || self.showing_all {
|
if self.total == 0 || self.showing_all {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
let size = self.page_size as u64;
|
let size = u64::from(self.page_size);
|
||||||
(self.total.div_ceil(size)) as u32
|
(self.total.div_ceil(size)) as u32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,7 +216,7 @@ impl<T> Page<T> {
|
||||||
if self.total == 0 {
|
if self.total == 0 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
((self.page - 1) as u64) * self.page_size as u64 + 1
|
u64::from(self.page - 1) * u64::from(self.page_size) + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pub fn hash_password(password: &str) -> Result<String> {
|
||||||
|
|
||||||
let password_hash = argon2
|
let password_hash = argon2
|
||||||
.hash_password(password.as_bytes(), &salt)
|
.hash_password(password.as_bytes(), &salt)
|
||||||
.map_err(|e| anyhow::anyhow!("failed to hash password: {}", e))?
|
.map_err(|e| anyhow::anyhow!("failed to hash password: {e}"))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
Ok(password_hash)
|
Ok(password_hash)
|
||||||
|
|
@ -23,7 +23,7 @@ pub fn hash_password(password: &str) -> Result<String> {
|
||||||
/// Verifies a password against a hash
|
/// Verifies a password against a hash
|
||||||
pub fn verify_password(password: &str, password_hash: &str) -> Result<bool> {
|
pub fn verify_password(password: &str, password_hash: &str) -> Result<bool> {
|
||||||
let parsed_hash = PasswordHash::new(password_hash)
|
let parsed_hash = PasswordHash::new(password_hash)
|
||||||
.map_err(|e| anyhow::anyhow!("failed to parse password hash: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("failed to parse password hash: {e}"))?;
|
||||||
|
|
||||||
let argon2 = Argon2::default();
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
|
|
@ -57,6 +57,7 @@ pub fn generate_session_token() -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)] // Tests: unwrap is acceptable for test assertions
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::domain::listing::{ListRequest, Page, SortDirection};
|
||||||
use crate::domain::repositories::BagRepository;
|
use crate::domain::repositories::BagRepository;
|
||||||
use crate::infrastructure::database::DatabasePool;
|
use crate::infrastructure::database::DatabasePool;
|
||||||
|
|
||||||
const BASE_SELECT: &str = r#"
|
const BASE_SELECT: &str = r"
|
||||||
SELECT
|
SELECT
|
||||||
b.id, b.roast_id, b.roast_date, b.amount, b.remaining, b.closed, b.finished_at, b.created_at, b.updated_at,
|
b.id, b.roast_id, b.roast_date, b.amount, b.remaining, b.closed, b.finished_at, b.created_at, b.updated_at,
|
||||||
r.name as roast_name, r.slug as roast_slug,
|
r.name as roast_name, r.slug as roast_slug,
|
||||||
|
|
@ -18,7 +18,7 @@ const BASE_SELECT: &str = r#"
|
||||||
FROM bags b
|
FROM bags b
|
||||||
JOIN roasts r ON b.roast_id = r.id
|
JOIN roasts r ON b.roast_id = r.id
|
||||||
JOIN roasters rr ON r.roaster_id = rr.id
|
JOIN roasters rr ON r.roaster_id = rr.id
|
||||||
"#;
|
";
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SqlBagRepository {
|
pub struct SqlBagRepository {
|
||||||
|
|
@ -108,11 +108,11 @@ impl SqlBagRepository {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl BagRepository for SqlBagRepository {
|
impl BagRepository for SqlBagRepository {
|
||||||
async fn insert(&self, bag: NewBag) -> Result<Bag, RepositoryError> {
|
async fn insert(&self, bag: NewBag) -> Result<Bag, RepositoryError> {
|
||||||
let query = r#"
|
let query = r"
|
||||||
INSERT INTO bags (roast_id, roast_date, amount, remaining)
|
INSERT INTO bags (roast_id, roast_date, amount, remaining)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
RETURNING id, roast_id, roast_date, amount, remaining, closed, finished_at, created_at, updated_at
|
RETURNING id, roast_id, roast_date, amount, remaining, closed, finished_at, created_at, updated_at
|
||||||
"#;
|
";
|
||||||
|
|
||||||
let record = query_as::<_, BagRecord>(query)
|
let record = query_as::<_, BagRecord>(query)
|
||||||
.bind(bag.roast_id.into_inner())
|
.bind(bag.roast_id.into_inner())
|
||||||
|
|
@ -127,11 +127,11 @@ impl BagRepository for SqlBagRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: BagId) -> Result<Bag, RepositoryError> {
|
async fn get(&self, id: BagId) -> Result<Bag, RepositoryError> {
|
||||||
let query = r#"
|
let query = r"
|
||||||
SELECT id, roast_id, roast_date, amount, remaining, closed, finished_at, created_at, updated_at
|
SELECT id, roast_id, roast_date, amount, remaining, closed, finished_at, created_at, updated_at
|
||||||
FROM bags
|
FROM bags
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
"#;
|
";
|
||||||
|
|
||||||
let record = query_as::<_, BagRecord>(query)
|
let record = query_as::<_, BagRecord>(query)
|
||||||
.bind(id.into_inner())
|
.bind(id.into_inner())
|
||||||
|
|
@ -144,7 +144,7 @@ impl BagRepository for SqlBagRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_with_roast(&self, id: BagId) -> Result<BagWithRoast, RepositoryError> {
|
async fn get_with_roast(&self, id: BagId) -> Result<BagWithRoast, RepositoryError> {
|
||||||
let query = format!("{} WHERE b.id = ?", BASE_SELECT);
|
let query = format!("{BASE_SELECT} WHERE b.id = ?");
|
||||||
|
|
||||||
let record = query_as::<_, BagWithRoastRecord>(&query)
|
let record = query_as::<_, BagWithRoastRecord>(&query)
|
||||||
.bind(id.into_inner())
|
.bind(id.into_inner())
|
||||||
|
|
@ -167,12 +167,12 @@ impl BagRepository for SqlBagRepository {
|
||||||
let where_clause = Self::build_where_clause(&filter);
|
let where_clause = Self::build_where_clause(&filter);
|
||||||
|
|
||||||
let base_query = match &where_clause {
|
let base_query = match &where_clause {
|
||||||
Some(w) => format!("{} WHERE {}", BASE_SELECT, w),
|
Some(w) => format!("{BASE_SELECT} WHERE {w}"),
|
||||||
None => BASE_SELECT.to_string(),
|
None => BASE_SELECT.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let count_query = match &where_clause {
|
let count_query = match &where_clause {
|
||||||
Some(w) => format!("SELECT COUNT(*) FROM bags b WHERE {}", w),
|
Some(w) => format!("SELECT COUNT(*) FROM bags b WHERE {w}"),
|
||||||
None => "SELECT COUNT(*) FROM bags".to_string(),
|
None => "SELECT COUNT(*) FROM bags".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ impl SqlGearRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: GearRecord) -> Result<Gear, RepositoryError> {
|
fn to_domain(record: GearRecord) -> Result<Gear, RepositoryError> {
|
||||||
let category = GearCategory::from_str(&record.category).map_err(|_| {
|
let category = GearCategory::from_str(&record.category).map_err(|()| {
|
||||||
RepositoryError::unexpected(format!("invalid category: {}", record.category))
|
RepositoryError::unexpected(format!("invalid category: {}", record.category))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -62,11 +62,11 @@ impl SqlGearRepository {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl GearRepository for SqlGearRepository {
|
impl GearRepository for SqlGearRepository {
|
||||||
async fn insert(&self, gear: NewGear) -> Result<Gear, RepositoryError> {
|
async fn insert(&self, gear: NewGear) -> Result<Gear, RepositoryError> {
|
||||||
let query = r#"
|
let query = r"
|
||||||
INSERT INTO gear (category, make, model)
|
INSERT INTO gear (category, make, model)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
RETURNING id, category, make, model, created_at, updated_at
|
RETURNING id, category, make, model, created_at, updated_at
|
||||||
"#;
|
";
|
||||||
|
|
||||||
let record = query_as::<_, GearRecord>(query)
|
let record = query_as::<_, GearRecord>(query)
|
||||||
.bind(gear.category.as_str())
|
.bind(gear.category.as_str())
|
||||||
|
|
@ -80,11 +80,11 @@ impl GearRepository for SqlGearRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: GearId) -> Result<Gear, RepositoryError> {
|
async fn get(&self, id: GearId) -> Result<Gear, RepositoryError> {
|
||||||
let query = r#"
|
let query = r"
|
||||||
SELECT id, category, make, model, created_at, updated_at
|
SELECT id, category, make, model, created_at, updated_at
|
||||||
FROM gear
|
FROM gear
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
"#;
|
";
|
||||||
|
|
||||||
let record = query_as::<_, GearRecord>(query)
|
let record = query_as::<_, GearRecord>(query)
|
||||||
.bind(id.into_inner())
|
.bind(id.into_inner())
|
||||||
|
|
@ -106,8 +106,7 @@ impl GearRepository for SqlGearRepository {
|
||||||
|
|
||||||
let base_query = match &where_clause {
|
let base_query = match &where_clause {
|
||||||
Some(w) => format!(
|
Some(w) => format!(
|
||||||
"SELECT id, category, make, model, created_at, updated_at FROM gear WHERE {}",
|
"SELECT id, category, make, model, created_at, updated_at FROM gear WHERE {w}"
|
||||||
w
|
|
||||||
),
|
),
|
||||||
None => {
|
None => {
|
||||||
"SELECT id, category, make, model, created_at, updated_at FROM gear".to_string()
|
"SELECT id, category, make, model, created_at, updated_at FROM gear".to_string()
|
||||||
|
|
@ -115,7 +114,7 @@ impl GearRepository for SqlGearRepository {
|
||||||
};
|
};
|
||||||
|
|
||||||
let count_query = match &where_clause {
|
let count_query = match &where_clause {
|
||||||
Some(w) => format!("SELECT COUNT(*) FROM gear WHERE {}", w),
|
Some(w) => format!("SELECT COUNT(*) FROM gear WHERE {w}"),
|
||||||
None => "SELECT COUNT(*) FROM gear".to_string(),
|
None => "SELECT COUNT(*) FROM gear".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/// Helper macro for building dynamic UPDATE queries with QueryBuilder.
|
/// Helper macro for building dynamic UPDATE queries with `QueryBuilder`.
|
||||||
///
|
///
|
||||||
/// Handles the common pattern of conditionally adding SET clauses with proper
|
/// Handles the common pattern of conditionally adding SET clauses with proper
|
||||||
/// comma separation.
|
/// comma separation.
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ where
|
||||||
{
|
{
|
||||||
match request.page_size() {
|
match request.page_size() {
|
||||||
PageSize::All => {
|
PageSize::All => {
|
||||||
let query = format!("{} ORDER BY {}", base_query, order_clause);
|
let query = format!("{base_query} ORDER BY {order_clause}");
|
||||||
let records = query_as::<_, R>(&query)
|
let records = query_as::<_, R>(&query)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
|
|
@ -35,11 +35,11 @@ where
|
||||||
Ok(Page::new(items, 1, page_size.max(1), total, true))
|
Ok(Page::new(items, 1, page_size.max(1), total, true))
|
||||||
}
|
}
|
||||||
PageSize::Limited(page_size) => {
|
PageSize::Limited(page_size) => {
|
||||||
let limit = page_size as i64;
|
let limit = i64::from(page_size);
|
||||||
let mut page = request.page();
|
let mut page = request.page();
|
||||||
let offset = ((page - 1) as i64).saturating_mul(limit);
|
let offset = i64::from(page - 1).saturating_mul(limit);
|
||||||
|
|
||||||
let query_sql = format!("{} ORDER BY {} LIMIT ? OFFSET ?", base_query, order_clause);
|
let query_sql = format!("{base_query} ORDER BY {order_clause} LIMIT ? OFFSET ?");
|
||||||
|
|
||||||
let mut records = query_as::<_, R>(&query_sql)
|
let mut records = query_as::<_, R>(&query_sql)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
|
|
@ -56,7 +56,7 @@ where
|
||||||
if page > 1 && records.is_empty() && total > 0 {
|
if page > 1 && records.is_empty() && total > 0 {
|
||||||
let last_page = ((total + limit - 1) / limit) as u32;
|
let last_page = ((total + limit - 1) / limit) as u32;
|
||||||
page = last_page.max(1);
|
page = last_page.max(1);
|
||||||
let offset = ((page - 1) as i64).saturating_mul(limit);
|
let offset = i64::from(page - 1).saturating_mul(limit);
|
||||||
records = query_as::<_, R>(&query_sql)
|
records = query_as::<_, R>(&query_sql)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
.bind(offset)
|
.bind(offset)
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ impl SqlRoastRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)] // Repository impl has many methods
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl RoastRepository for SqlRoastRepository {
|
impl RoastRepository for SqlRoastRepository {
|
||||||
async fn insert(&self, new_roast: NewRoast) -> Result<Roast, RepositoryError> {
|
async fn insert(&self, new_roast: NewRoast) -> Result<Roast, RepositoryError> {
|
||||||
|
|
@ -198,7 +199,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.map(|record| record.into_roast())
|
.map(RoastRecord::into_roast)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.ok_or(RepositoryError::NotFound)
|
.ok_or(RepositoryError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +215,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.map(|record| record.into_with_roaster())
|
.map(RoastWithRoasterRecord::into_with_roaster)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.ok_or(RepositoryError::NotFound)
|
.ok_or(RepositoryError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +233,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.map(|record| record.into_roast())
|
.map(RoastRecord::into_roast)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.ok_or(RepositoryError::NotFound)
|
.ok_or(RepositoryError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
@ -270,7 +271,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
|
|
||||||
records
|
records
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|record| record.into_with_roaster())
|
.map(RoastWithRoasterRecord::into_with_roaster)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,11 @@ impl SqlTimelineEventRepository {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TimelineEventRepository for SqlTimelineEventRepository {
|
impl TimelineEventRepository for SqlTimelineEventRepository {
|
||||||
async fn insert(&self, event: NewTimelineEvent) -> Result<TimelineEvent, RepositoryError> {
|
async fn insert(&self, event: NewTimelineEvent) -> Result<TimelineEvent, RepositoryError> {
|
||||||
let query = r#"
|
let query = r"
|
||||||
INSERT INTO timeline_events (entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json)
|
INSERT INTO timeline_events (entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
RETURNING id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json
|
RETURNING id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json
|
||||||
"#;
|
";
|
||||||
|
|
||||||
let details_json = serde_json::to_string(&event.details).map_err(|err| {
|
let details_json = serde_json::to_string(&event.details).map_err(|err| {
|
||||||
RepositoryError::unexpected(format!("failed to encode timeline event details: {err}"))
|
RepositoryError::unexpected(format!("failed to encode timeline event details: {err}"))
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ impl SqlTokenRepository {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: TokenRecord) -> Result<Token, RepositoryError> {
|
fn to_domain(record: TokenRecord) -> Token {
|
||||||
let TokenRecord {
|
let TokenRecord {
|
||||||
id,
|
id,
|
||||||
user_id,
|
user_id,
|
||||||
|
|
@ -29,7 +29,7 @@ impl SqlTokenRepository {
|
||||||
revoked_at,
|
revoked_at,
|
||||||
} = record;
|
} = record;
|
||||||
|
|
||||||
Ok(Token::new(
|
Token::new(
|
||||||
TokenId::from(id),
|
TokenId::from(id),
|
||||||
UserId::from(user_id),
|
UserId::from(user_id),
|
||||||
token_hash,
|
token_hash,
|
||||||
|
|
@ -37,7 +37,7 @@ impl SqlTokenRepository {
|
||||||
created_at,
|
created_at,
|
||||||
last_used_at,
|
last_used_at,
|
||||||
revoked_at,
|
revoked_at,
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
RepositoryError::unexpected(err.to_string())
|
RepositoryError::unexpected(err.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
Ok(Self::to_domain(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: TokenId) -> Result<Token, RepositoryError> {
|
async fn get(&self, id: TokenId) -> Result<Token, RepositoryError> {
|
||||||
|
|
@ -80,7 +80,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
Ok(Self::to_domain(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Token, RepositoryError> {
|
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Token, RepositoryError> {
|
||||||
|
|
@ -93,7 +93,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
Ok(Self::to_domain(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_by_user(&self, user_id: UserId) -> Result<Vec<Token>, RepositoryError> {
|
async fn list_by_user(&self, user_id: UserId) -> Result<Vec<Token>, RepositoryError> {
|
||||||
|
|
@ -105,7 +105,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
records.into_iter().map(Self::to_domain).collect()
|
Ok(records.into_iter().map(Self::to_domain).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn revoke(&self, id: TokenId) -> Result<Token, RepositoryError> {
|
async fn revoke(&self, id: TokenId) -> Result<Token, RepositoryError> {
|
||||||
|
|
@ -120,7 +120,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
match record {
|
match record {
|
||||||
Some(record) => Self::to_domain(record),
|
Some(record) => Ok(Self::to_domain(record)),
|
||||||
None => Err(RepositoryError::NotFound),
|
None => Err(RepositoryError::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ impl SqlUserRepository {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: UserRecord) -> Result<User, RepositoryError> {
|
fn to_domain(record: UserRecord) -> User {
|
||||||
let UserRecord {
|
let UserRecord {
|
||||||
id,
|
id,
|
||||||
username,
|
username,
|
||||||
|
|
@ -26,12 +26,7 @@ impl SqlUserRepository {
|
||||||
created_at,
|
created_at,
|
||||||
} = record;
|
} = record;
|
||||||
|
|
||||||
Ok(User::new(
|
User::new(UserId::from(id), username, password_hash, created_at)
|
||||||
UserId::from(id),
|
|
||||||
username,
|
|
||||||
password_hash,
|
|
||||||
created_at,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +49,7 @@ impl UserRepository for SqlUserRepository {
|
||||||
RepositoryError::unexpected(err.to_string())
|
RepositoryError::unexpected(err.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
Ok(Self::to_domain(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: UserId) -> Result<User, RepositoryError> {
|
async fn get(&self, id: UserId) -> Result<User, RepositoryError> {
|
||||||
|
|
@ -67,7 +62,7 @@ impl UserRepository for SqlUserRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
Ok(Self::to_domain(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_by_username(&self, username: &str) -> Result<User, RepositoryError> {
|
async fn get_by_username(&self, username: &str) -> Result<User, RepositoryError> {
|
||||||
|
|
@ -80,7 +75,7 @@ impl UserRepository for SqlUserRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
Ok(Self::to_domain(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exists(&self) -> Result<bool, RepositoryError> {
|
async fn exists(&self) -> Result<bool, RepositoryError> {
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ where
|
||||||
/// Register a subscriber as global default to process span data.
|
/// Register a subscriber as global default to process span data.
|
||||||
///
|
///
|
||||||
/// This should only be called once!
|
/// This should only be called once!
|
||||||
|
#[allow(clippy::expect_used)] // Startup: panicking is appropriate if logging cannot be initialized
|
||||||
pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
|
pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
|
||||||
LogTracer::init().expect("Failed to set logger");
|
LogTracer::init().expect("Failed to set logger");
|
||||||
set_global_default(subscriber).expect("Failed to set subscriber");
|
set_global_default(subscriber).expect("Failed to set subscriber");
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ impl<T> Paginated<T> {
|
||||||
if self.total == 0 || self.showing_all {
|
if self.total == 0 || self.showing_all {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
let page_size = self.page_size as u64;
|
let page_size = u64::from(self.page_size);
|
||||||
self.total.div_ceil(page_size) as u32
|
self.total.div_ceil(page_size) as u32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +77,7 @@ impl<T> Paginated<T> {
|
||||||
if self.total == 0 {
|
if self.total == 0 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
((self.page - 1) as u64) * self.page_size as u64 + 1
|
u64::from(self.page - 1) * u64::from(self.page_size) + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,9 +181,7 @@ impl<K: SortKey> ListNavigator<K> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_sorted_by(&self, key: &str) -> bool {
|
pub fn is_sorted_by(&self, key: &str) -> bool {
|
||||||
K::from_query(key)
|
K::from_query(key).is_some_and(|candidate| candidate == self.request.sort_key())
|
||||||
.map(|candidate| candidate == self.request.sort_key())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_sort_dir(&self, key: &str) -> &'static str {
|
pub fn next_sort_dir(&self, key: &str) -> &'static str {
|
||||||
|
|
@ -212,6 +210,7 @@ impl<K: SortKey> ListNavigator<K> {
|
||||||
Self::query_string(Self::request_for_sort(self.request, key))
|
Self::query_string(Self::request_for_sort(self.request, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unused_self)] // Keeps consistent method interface
|
||||||
fn build_href(&self, path: &str, request: ListRequest<K>) -> String {
|
fn build_href(&self, path: &str, request: ListRequest<K>) -> String {
|
||||||
if let Some((base, fragment)) = path.split_once('#') {
|
if let Some((base, fragment)) = path.split_once('#') {
|
||||||
format!("{}?{}#{}", base, Self::query_string(request), fragment)
|
format!("{}?{}#{}", base, Self::query_string(request), fragment)
|
||||||
|
|
@ -460,10 +459,8 @@ impl TimelineEventView {
|
||||||
|
|
||||||
let link = match (entity_type.as_str(), slug, roaster_slug) {
|
let link = match (entity_type.as_str(), slug, roaster_slug) {
|
||||||
("roaster", Some(slug), _) => format!("/roasters/{slug}"),
|
("roaster", Some(slug), _) => format!("/roasters/{slug}"),
|
||||||
("roast", Some(slug), Some(roaster_slug)) => {
|
// Both roasts and bags link to the roast page
|
||||||
format!("/roasters/{roaster_slug}/roasts/{slug}")
|
("roast" | "bag", Some(slug), Some(roaster_slug)) => {
|
||||||
}
|
|
||||||
("bag", Some(slug), Some(roaster_slug)) => {
|
|
||||||
format!("/roasters/{roaster_slug}/roasts/{slug}")
|
format!("/roasters/{roaster_slug}/roasts/{slug}")
|
||||||
}
|
}
|
||||||
("gear", _, _) => "/gear".to_string(),
|
("gear", _, _) => "/gear".to_string(),
|
||||||
|
|
@ -546,8 +543,7 @@ impl BagView {
|
||||||
finished_at: bag
|
finished_at: bag
|
||||||
.bag
|
.bag
|
||||||
.finished_at
|
.finished_at
|
||||||
.map(|d| d.to_string())
|
.map_or_else(|| "—".to_string(), |d| d.to_string()),
|
||||||
.unwrap_or_else(|| "—".to_string()),
|
|
||||||
created_at: bag.bag.created_at.format("%Y-%m-%d").to_string(),
|
created_at: bag.bag.created_at.format("%Y-%m-%d").to_string(),
|
||||||
roast_name: bag.roast_name,
|
roast_name: bag.roast_name,
|
||||||
roaster_name: bag.roaster_name,
|
roaster_name: bag.roaster_name,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue