max / goingson
248 files changed,
+2047 insertions,
-2201 deletions
| @@ -118,3 +118,35 @@ kberg = { path = "../../MNW/shared/kberg", default-features = false, features = | |||
| 118 | 118 | # Internal crates | |
| 119 | 119 | goingson-core = { path = "crates/core" } | |
| 120 | 120 | goingson-db-sqlite = { path = "crates/db-sqlite" } | |
| 121 | + | ||
| 122 | + | [workspace.lints.rust] | |
| 123 | + | unused = "warn" | |
| 124 | + | unreachable_pub = "warn" | |
| 125 | + | ||
| 126 | + | [workspace.lints.clippy] | |
| 127 | + | pedantic = { level = "warn", priority = -1 } | |
| 128 | + | # Allow-list tuned from a measured breakdown across server/multithreaded/pter | |
| 129 | + | # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything | |
| 130 | + | # else in `pedantic` stays a warning. Keep this block identical across repos. | |
| 131 | + | module_name_repetitions = "allow" | |
| 132 | + | # Doc lints. No docs-completeness push is underway. | |
| 133 | + | missing_errors_doc = "allow" | |
| 134 | + | missing_panics_doc = "allow" | |
| 135 | + | doc_markdown = "allow" | |
| 136 | + | # Numeric casts. Endemic and mostly intentional in size and byte math. | |
| 137 | + | cast_possible_truncation = "allow" | |
| 138 | + | cast_sign_loss = "allow" | |
| 139 | + | cast_precision_loss = "allow" | |
| 140 | + | cast_possible_wrap = "allow" | |
| 141 | + | cast_lossless = "allow" | |
| 142 | + | # Subjective structure and style nags. High churn, low signal. | |
| 143 | + | must_use_candidate = "allow" | |
| 144 | + | too_many_lines = "allow" | |
| 145 | + | struct_excessive_bools = "allow" | |
| 146 | + | similar_names = "allow" | |
| 147 | + | items_after_statements = "allow" | |
| 148 | + | single_match_else = "allow" | |
| 149 | + | # Frequent false-positives in TUI and router-heavy code. | |
| 150 | + | match_same_arms = "allow" | |
| 151 | + | unnecessary_wraps = "allow" | |
| 152 | + | type_complexity = "allow" |
| @@ -19,3 +19,6 @@ strum_macros = { workspace = true } | |||
| 19 | 19 | sqlx = { workspace = true, features = ["sqlite", "uuid"], optional = true } | |
| 20 | 20 | tagtree = { workspace = true } | |
| 21 | 21 | sha2 = { workspace = true } | |
| 22 | + | ||
| 23 | + | [lints] | |
| 24 | + | workspace = true |
| @@ -3,7 +3,7 @@ | |||
| 3 | 3 | //! This module centralizes magic numbers and configuration values | |
| 4 | 4 | //! to improve maintainability and documentation. | |
| 5 | 5 | ||
| 6 | - | // ============ Time Constants ============ | |
| 6 | + | // Time Constants | |
| 7 | 7 | ||
| 8 | 8 | /// Hours in a day. | |
| 9 | 9 | pub const HOURS_PER_DAY: f64 = 24.0; | |
| @@ -14,7 +14,7 @@ pub const DAYS_PER_WEEK: i64 = 7; | |||
| 14 | 14 | /// Approximate days in a month (for relative date calculations). | |
| 15 | 15 | pub const APPROXIMATE_DAYS_PER_MONTH: i64 = 30; | |
| 16 | 16 | ||
| 17 | - | // ============ Parser Defaults ============ | |
| 17 | + | // Parser Defaults | |
| 18 | 18 | ||
| 19 | 19 | /// Default hour for parsed dates (9:00 AM). | |
| 20 | 20 | pub const DEFAULT_PARSE_HOUR: u32 = 9; | |
| @@ -22,7 +22,7 @@ pub const DEFAULT_PARSE_HOUR: u32 = 9; | |||
| 22 | 22 | /// Default minute for parsed dates. | |
| 23 | 23 | pub const DEFAULT_PARSE_MINUTE: u32 = 0; | |
| 24 | 24 | ||
| 25 | - | // ============ Urgency Thresholds ============ | |
| 25 | + | // Urgency Thresholds | |
| 26 | 26 | ||
| 27 | 27 | /// Urgency score threshold for "high" classification (on a 0–10 scale). | |
| 28 | 28 | /// Scores are computed by `calculate_urgency()` from priority, due date | |
| @@ -34,21 +34,21 @@ pub const URGENCY_HIGH_THRESHOLD: f64 = 8.0; | |||
| 34 | 34 | /// Tasks between 5.0 and 8.0 get "urgency-medium"; below 5.0 gets "urgency-low". | |
| 35 | 35 | pub const URGENCY_MEDIUM_THRESHOLD: f64 = 5.0; | |
| 36 | 36 | ||
| 37 | - | // ============ Display Thresholds ============ | |
| 37 | + | // Display Thresholds | |
| 38 | 38 | ||
| 39 | 39 | /// Number of days for short-form due date display (e.g., "+3d" instead of "Mar 15"). | |
| 40 | 40 | /// Used by `TaskResponse::from()` and `GoingsOn.utils.formatDue()` in the frontend | |
| 41 | 41 | /// to decide between relative ("today", "+3d") and absolute ("Mar 15") formats. | |
| 42 | 42 | pub const DAYS_THRESHOLD_SHORT_FORMAT: i64 = 7; | |
| 43 | 43 | ||
| 44 | - | // ============ Preview Lengths ============ | |
| 44 | + | // Preview Lengths | |
| 45 | 45 | ||
| 46 | 46 | /// Maximum length for email body preview snippets in the email list view. | |
| 47 | 47 | /// Truncates the plain-text body to this many characters for the compact | |
| 48 | 48 | /// thread listing, with an ellipsis appended if truncated. | |
| 49 | 49 | pub const EMAIL_BODY_PREVIEW_LENGTH: usize = 100; | |
| 50 | 50 | ||
| 51 | - | // ============ Validation Limits ============ | |
| 51 | + | // Validation Limits | |
| 52 | 52 | ||
| 53 | 53 | /// Maximum length for project names. | |
| 54 | 54 | pub const MAX_PROJECT_NAME_LENGTH: usize = 255; |
| @@ -8,7 +8,7 @@ use crate::id_types::{ContactEmailId, ContactId, ContactPhoneId, CustomFieldId, | |||
| 8 | 8 | use chrono::{DateTime, NaiveDate, Utc}; | |
| 9 | 9 | use serde::{Deserialize, Serialize}; | |
| 10 | 10 | ||
| 11 | - | // ============ Main Entity ============ | |
| 11 | + | // Main Entity | |
| 12 | 12 | ||
| 13 | 13 | /// A contact (person) with optional sub-collections. | |
| 14 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| @@ -75,11 +75,11 @@ impl Contact { | |||
| 75 | 75 | } | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | - | // ============ Sub-collection Entities ============ | |
| 78 | + | // Sub-collection Entities | |
| 79 | 79 | ||
| 80 | 80 | /// A flattened (name, email) pair for compose autocomplete. | |
| 81 | 81 | /// | |
| 82 | - | /// Returned by `list_email_directory` — a single JOIN that skips the per-contact | |
| 82 | + | /// Returned by `list_email_directory`, a single JOIN that skips the per-contact | |
| 83 | 83 | /// sub-collection hydration the compose screen does not need. | |
| 84 | 84 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 85 | 85 | #[serde(rename_all = "camelCase")] | |
| @@ -137,7 +137,7 @@ pub struct ContactCustomField { | |||
| 137 | 137 | pub url: Option<String>, | |
| 138 | 138 | } | |
| 139 | 139 | ||
| 140 | - | // ============ DTOs ============ | |
| 140 | + | // DTOs | |
| 141 | 141 | ||
| 142 | 142 | /// Data for creating a new contact. | |
| 143 | 143 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| @@ -198,7 +198,7 @@ pub struct NewContactCustomField { | |||
| 198 | 198 | pub url: Option<String>, | |
| 199 | 199 | } | |
| 200 | 200 | ||
| 201 | - | // ============ Activity Feed ============ | |
| 201 | + | // Activity Feed | |
| 202 | 202 | ||
| 203 | 203 | /// Which kind of entity an activity item represents. | |
| 204 | 204 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
| @@ -124,7 +124,7 @@ fn parse_weekday( | |||
| 124 | 124 | default_time: NaiveTime, | |
| 125 | 125 | ) -> Option<NaiveDateTime> { | |
| 126 | 126 | let is_next = tokens.first() == Some(&"next"); | |
| 127 | - | let name_idx = if is_next { 1 } else { 0 }; | |
| 127 | + | let name_idx = usize::from(is_next); | |
| 128 | 128 | let target = WEEKDAYS | |
| 129 | 129 | .iter() | |
| 130 | 130 | .position(|d| Some(d) == tokens.get(name_idx))? as i64; |
| @@ -18,10 +18,10 @@ use chrono_tz::Tz; | |||
| 18 | 18 | /// rather than panicking (mirrors `tz::local_civil_to_utc` in the app crate). | |
| 19 | 19 | pub fn civil_midnight_utc(date: NaiveDate, tz: Tz) -> DateTime<Utc> { | |
| 20 | 20 | let naive = date.and_hms_opt(0, 0, 0).expect("midnight is always valid"); | |
| 21 | - | tz.from_local_datetime(&naive) | |
| 22 | - | .earliest() | |
| 23 | - | .map(|dt| dt.with_timezone(&Utc)) | |
| 24 | - | .unwrap_or_else(|| DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc)) | |
| 21 | + | tz.from_local_datetime(&naive).earliest().map_or_else( | |
| 22 | + | || DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc), | |
| 23 | + | |dt| dt.with_timezone(&Utc), | |
| 24 | + | ) | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | 27 | /// Formats a date relative to now, bidirectional: "2d ago", "today", "tomorrow", "+3d", "Mar 15". | |
| @@ -38,7 +38,7 @@ pub fn format_relative_date(dt: DateTime<Utc>, now: DateTime<Utc>) -> String { | |||
| 38 | 38 | } else if diff_days == 1 { | |
| 39 | 39 | "tomorrow".to_string() | |
| 40 | 40 | } else if diff_days < 7 { | |
| 41 | - | format!("+{}d", diff_days) | |
| 41 | + | format!("+{diff_days}d") | |
| 42 | 42 | } else { | |
| 43 | 43 | dt.format("%b %d").to_string() | |
| 44 | 44 | } | |
| @@ -54,7 +54,7 @@ pub fn format_relative_future(dt: DateTime<Utc>, now: DateTime<Utc>) -> String { | |||
| 54 | 54 | } else if diff_days == 1 { | |
| 55 | 55 | "tomorrow".to_string() | |
| 56 | 56 | } else if diff_days < 7 { | |
| 57 | - | format!("+{}d", diff_days) | |
| 57 | + | format!("+{diff_days}d") | |
| 58 | 58 | } else { | |
| 59 | 59 | dt.format("%b %d").to_string() | |
| 60 | 60 | } | |
| @@ -69,11 +69,11 @@ pub fn format_elapsed_time(dt: DateTime<Utc>, now: DateTime<Utc>) -> String { | |||
| 69 | 69 | if hours < 1 { | |
| 70 | 70 | "Just now".to_string() | |
| 71 | 71 | } else if hours < 24 { | |
| 72 | - | format!("{}h ago", hours) | |
| 72 | + | format!("{hours}h ago") | |
| 73 | 73 | } else { | |
| 74 | 74 | let days = diff.num_days(); | |
| 75 | 75 | if days < 7 { | |
| 76 | - | format!("{}d ago", days) | |
| 76 | + | format!("{days}d ago") | |
| 77 | 77 | } else { | |
| 78 | 78 | dt.with_timezone(&Local).format("%b %d").to_string() | |
| 79 | 79 | } | |
| @@ -91,7 +91,7 @@ mod tests { | |||
| 91 | 91 | Utc.from_utc_datetime(&date.and_time(time)) | |
| 92 | 92 | } | |
| 93 | 93 | ||
| 94 | - | // ---- format_relative_date ---- | |
| 94 | + | // format_relative_date | |
| 95 | 95 | ||
| 96 | 96 | #[test] | |
| 97 | 97 | fn relative_date_today() { | |
| @@ -149,7 +149,7 @@ mod tests { | |||
| 149 | 149 | assert_eq!(format_relative_date(dt, now), "Apr 22"); | |
| 150 | 150 | } | |
| 151 | 151 | ||
| 152 | - | // ---- format_relative_future ---- | |
| 152 | + | // format_relative_future | |
| 153 | 153 | ||
| 154 | 154 | #[test] | |
| 155 | 155 | fn relative_future_today() { | |
| @@ -179,7 +179,7 @@ mod tests { | |||
| 179 | 179 | assert_eq!(format_relative_future(dt, now), "Jun 01"); | |
| 180 | 180 | } | |
| 181 | 181 | ||
| 182 | - | // ---- format_elapsed_time ---- | |
| 182 | + | // format_elapsed_time | |
| 183 | 183 | ||
| 184 | 184 | #[test] | |
| 185 | 185 | fn elapsed_just_now() { |
| @@ -80,7 +80,7 @@ mod tests { | |||
| 80 | 80 | TimelineItem { | |
| 81 | 81 | id: Uuid::new_v4(), | |
| 82 | 82 | item_type: "event".to_string(), | |
| 83 | - | title: format!("Event at {}:{:02}", hour, minute), | |
| 83 | + | title: format!("Event at {hour}:{minute:02}"), | |
| 84 | 84 | start_time: start, | |
| 85 | 85 | end_time: Some(start + Duration::minutes(duration_mins as i64)), | |
| 86 | 86 | duration: Some(duration_mins), |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | //! Reply/forward compose-prefill construction. | |
| 2 | 2 | //! | |
| 3 | - | //! Building a reply or forward is domain logic — recipient assembly with | |
| 3 | + | //! Building a reply or forward is domain logic, recipient assembly with | |
| 4 | 4 | //! own-address exclusion, `Re:`/`Fwd:` subject prefixing, and body quoting. | |
| 5 | 5 | //! It lives here in `core` (not the JS frontend) so the rules are uniform, | |
| 6 | 6 | //! unit-tested, and driven by the authoritative account list rather than a | |
| @@ -45,7 +45,7 @@ fn prefix_once(subject: &str, prefix: &str) -> String { | |||
| 45 | 45 | if already { | |
| 46 | 46 | subject.to_string() | |
| 47 | 47 | } else { | |
| 48 | - | format!("{} {}", prefix, subject) | |
| 48 | + | format!("{prefix} {subject}") | |
| 49 | 49 | } | |
| 50 | 50 | } | |
| 51 | 51 | ||
| @@ -89,10 +89,10 @@ pub fn reply_recipients(from: &str, to: &str, own_addresses: &[String], reply_al | |||
| 89 | 89 | pub fn quoted_reply_body(from: &str, date: &str, body: &str) -> String { | |
| 90 | 90 | let quoted: String = body | |
| 91 | 91 | .split('\n') | |
| 92 | - | .map(|l| format!("> {}", l)) | |
| 92 | + | .map(|l| format!("> {l}")) | |
| 93 | 93 | .collect::<Vec<_>>() | |
| 94 | 94 | .join("\n"); | |
| 95 | - | format!("\n\nOn {}, {} wrote:\n>\n{}", date, from, quoted) | |
| 95 | + | format!("\n\nOn {date}, {from} wrote:\n>\n{quoted}") | |
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | 98 | /// Build the body for a forwarded message: a header block followed by the | |
| @@ -100,12 +100,11 @@ pub fn quoted_reply_body(from: &str, date: &str, body: &str) -> String { | |||
| 100 | 100 | pub fn forward_body(from: &str, date: &str, subject: &str, to: &str, body: &str) -> String { | |
| 101 | 101 | format!( | |
| 102 | 102 | "\n\n---------- Forwarded message ----------\n\ | |
| 103 | - | From: {}\n\ | |
| 104 | - | Date: {}\n\ | |
| 105 | - | Subject: {}\n\ | |
| 106 | - | To: {}\n\n\ | |
| 107 | - | {}", | |
| 108 | - | from, date, subject, to, body | |
| 103 | + | From: {from}\n\ | |
| 104 | + | Date: {date}\n\ | |
| 105 | + | Subject: {subject}\n\ | |
| 106 | + | To: {to}\n\n\ | |
| 107 | + | {body}" | |
| 109 | 108 | ) | |
| 110 | 109 | } | |
| 111 | 110 |
| @@ -138,7 +138,7 @@ pub async fn process_fetched_emails( | |||
| 138 | 138 | }); | |
| 139 | 139 | } | |
| 140 | 140 | ||
| 141 | - | // Batch insert — single transaction, no post-insert SELECTs | |
| 141 | + | // Batch insert, single transaction, no post-insert SELECTs | |
| 142 | 142 | result.emails_saved = email_repo | |
| 143 | 143 | .create_with_tracking_batch(user_id, new_emails) | |
| 144 | 144 | .await?; |
| @@ -65,6 +65,10 @@ impl CoreError { | |||
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | 67 | /// Creates a not-found error with resource type and identifier. | |
| 68 | + | #[allow( | |
| 69 | + | clippy::needless_pass_by_value, | |
| 70 | + | reason = "generic `impl ToString` builder contract: callers pass Copy id newtypes, &str literals, and owned String by value at ~90 sites; a borrow would force `&` everywhere for no gain" | |
| 71 | + | )] | |
| 68 | 72 | pub fn not_found(resource: &'static str, id: impl ToString) -> Self { | |
| 69 | 73 | CoreError::NotFound { | |
| 70 | 74 | resource, |
| @@ -180,7 +180,7 @@ mod tests { | |||
| 180 | 180 | let uuid = Uuid::new_v4(); | |
| 181 | 181 | let task_id = TaskId::from(uuid); | |
| 182 | 182 | let project_id = ProjectId::from(uuid); | |
| 183 | - | // Same inner value but different types — can't accidentally swap them. | |
| 183 | + | // Same inner value but different types, can't accidentally swap them. | |
| 184 | 184 | assert_eq!(*task_id, *project_id); | |
| 185 | 185 | } | |
| 186 | 186 |
| @@ -89,7 +89,7 @@ pub fn mime_from_extension(filename: &str) -> &'static str { | |||
| 89 | 89 | /// Format a byte count as a human-readable string. | |
| 90 | 90 | pub fn format_file_size(bytes: i64) -> String { | |
| 91 | 91 | if bytes < 1024 { | |
| 92 | - | format!("{} B", bytes) | |
| 92 | + | format!("{bytes} B") | |
| 93 | 93 | } else if bytes < 1024 * 1024 { | |
| 94 | 94 | format!("{:.1} KB", bytes as f64 / 1024.0) | |
| 95 | 95 | } else if bytes < 1024 * 1024 * 1024 { | |
| @@ -126,9 +126,9 @@ mod tests { | |||
| 126 | 126 | assert_eq!(format_file_size(512), "512 B"); | |
| 127 | 127 | assert_eq!(format_file_size(1024), "1.0 KB"); | |
| 128 | 128 | assert_eq!(format_file_size(1536), "1.5 KB"); | |
| 129 | - | assert_eq!(format_file_size(1048576), "1.0 MB"); | |
| 130 | - | assert_eq!(format_file_size(1572864), "1.5 MB"); | |
| 131 | - | assert_eq!(format_file_size(1073741824), "1.0 GB"); | |
| 129 | + | assert_eq!(format_file_size(1_048_576), "1.0 MB"); | |
| 130 | + | assert_eq!(format_file_size(1_572_864), "1.5 MB"); | |
| 131 | + | assert_eq!(format_file_size(1_073_741_824), "1.0 GB"); | |
| 132 | 132 | } | |
| 133 | 133 | ||
| 134 | 134 | #[test] |
| @@ -4,7 +4,7 @@ use chrono::{DateTime, Utc}; | |||
| 4 | 4 | use serde::{Deserialize, Serialize}; | |
| 5 | 5 | use uuid::Uuid; | |
| 6 | 6 | ||
| 7 | - | // ============ Backup Settings ============ | |
| 7 | + | // Backup Settings | |
| 8 | 8 | ||
| 9 | 9 | /// User's backup configuration. | |
| 10 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| @@ -11,7 +11,7 @@ use crate::id_types::{EmailAccountId, EmailId, ProjectId}; | |||
| 11 | 11 | use chrono::{DateTime, Utc}; | |
| 12 | 12 | use serde::{Deserialize, Serialize}; | |
| 13 | 13 | ||
| 14 | - | // ============ Email ============ | |
| 14 | + | ||
| 15 | 15 | ||
| 16 | 16 | /// An email message synced from IMAP or sent via SMTP. | |
| 17 | 17 | /// | |
| @@ -39,7 +39,7 @@ pub struct Email { | |||
| 39 | 39 | /// body can be re-fetched on demand via the provider id. | |
| 40 | 40 | pub body_truncated: bool, | |
| 41 | 41 | /// Provider email id for JMAP accounts, used to re-fetch a truncated body | |
| 42 | - | /// (internal — never serialized to the frontend). | |
| 42 | + | /// (internal, never serialized to the frontend). | |
| 43 | 43 | #[serde(skip_serializing)] | |
| 44 | 44 | pub jmap_id: Option<String>, | |
| 45 | 45 | /// Whether the email has been read. | |
| @@ -100,9 +100,9 @@ impl Email { | |||
| 100 | 100 | if hours < 1 { | |
| 101 | 101 | "Just now".to_string() | |
| 102 | 102 | } else if hours < 24 { | |
| 103 | - | format!("{}h ago", hours) | |
| 103 | + | format!("{hours}h ago") | |
| 104 | 104 | } else if days < DAYS_THRESHOLD_SHORT_FORMAT { | |
| 105 | - | format!("{}d ago", days) | |
| 105 | + | format!("{days}d ago") | |
| 106 | 106 | } else { | |
| 107 | 107 | self.received_at.format("%b %d").to_string() | |
| 108 | 108 | } | |
| @@ -143,9 +143,7 @@ impl Email { | |||
| 143 | 143 | ||
| 144 | 144 | /// Returns true if the email is currently snoozed (snoozed_until is in the future). | |
| 145 | 145 | pub fn is_snoozed(&self) -> bool { | |
| 146 | - | self.snoozed_until | |
| 147 | - | .map(|until| until > Utc::now()) | |
| 148 | - | .unwrap_or(false) | |
| 146 | + | self.snoozed_until.is_some_and(|until| until > Utc::now()) | |
| 149 | 147 | } | |
| 150 | 148 | ||
| 151 | 149 | /// Returns true if the email is waiting for a reply. | |
| @@ -158,8 +156,7 @@ impl Email { | |||
| 158 | 156 | self.waiting_for_response | |
| 159 | 157 | && self | |
| 160 | 158 | .expected_response_date | |
| 161 | - | .map(|date| date < Utc::now()) | |
| 162 | - | .unwrap_or(false) | |
| 159 | + | .is_some_and(|date| date < Utc::now()) | |
| 163 | 160 | } | |
| 164 | 161 | } | |
| 165 | 162 | ||
| @@ -177,7 +174,7 @@ pub struct EmailThread { | |||
| 177 | 174 | pub has_unread: bool, | |
| 178 | 175 | } | |
| 179 | 176 | ||
| 180 | - | // ============ Email DTOs ============ | |
| 177 | + | // Email DTOs | |
| 181 | 178 | ||
| 182 | 179 | #[cfg(test)] | |
| 183 | 180 | mod tests { | |
| @@ -352,7 +349,7 @@ mod tests { | |||
| 352 | 349 | let mut email = make_email(); | |
| 353 | 350 | email.received_at = Utc::now() - Duration::days(30); | |
| 354 | 351 | let formatted = email.received_formatted(); | |
| 355 | - | // Should be like "Jan 26" — not "30d ago" | |
| 352 | + | // Should be like "Jan 26", not "30d ago" | |
| 356 | 353 | assert!(!formatted.contains("ago")); | |
| 357 | 354 | } | |
| 358 | 355 | } |
| @@ -7,7 +7,7 @@ use strum_macros::EnumString; | |||
| 7 | 7 | ||
| 8 | 8 | use super::shared::DbValue; | |
| 9 | 9 | ||
| 10 | - | // ============ Email Account ============ | |
| 10 | + | // Email Account | |
| 11 | 11 | ||
| 12 | 12 | /// Authentication method for email accounts. | |
| 13 | 13 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, EnumString)] |
| @@ -11,7 +11,7 @@ use crate::id_types::{ContactId, EventId, ProjectId, TaskId, UserId}; | |||
| 11 | 11 | use chrono::{DateTime, Duration, TimeZone, Timelike, Utc}; | |
| 12 | 12 | use serde::{Deserialize, Serialize}; | |
| 13 | 13 | ||
| 14 | - | // ============ Event ============ | |
| 14 | + | // Event | |
| 15 | 15 | ||
| 16 | 16 | /// A calendar event with optional time-blocking link to a task. | |
| 17 | 17 | /// | |
| @@ -178,11 +178,11 @@ impl Event { | |||
| 178 | 178 | /// *exclusive* local-midnight boundary after the last covered day: | |
| 179 | 179 | /// - no end given → the day after the start (a single-day span); | |
| 180 | 180 | /// - an end already at local midnight is treated as already-exclusive and kept | |
| 181 | - | /// as-is (so re-snapping a stored all-day span is idempotent — it does not grow | |
| 181 | + | /// as-is (so re-snapping a stored all-day span is idempotent, it does not grow | |
| 182 | 182 | /// by a day on every edit); | |
| 183 | 183 | /// - a mid-day end covers the whole of its day, so it rounds up to the next midnight. | |
| 184 | 184 | /// | |
| 185 | - | /// Both bounds are returned as UTC instants — the shape [`Event::is_all_day_in`] | |
| 185 | + | /// Both bounds are returned as UTC instants, the shape [`Event::is_all_day_in`] | |
| 186 | 186 | /// detects, so a snapped event round-trips as all-day. | |
| 187 | 187 | /// | |
| 188 | 188 | /// On a DST spring-forward gap the civil midnight doesn't exist; it falls back to | |
| @@ -215,13 +215,13 @@ pub fn snap_all_day_span<Tz: TimeZone>( | |||
| 215 | 215 | /// Convert a calendar date's local midnight in `tz` to the corresponding UTC instant. | |
| 216 | 216 | fn local_midnight_utc<Tz: TimeZone>(date: chrono::NaiveDate, tz: &Tz) -> DateTime<Utc> { | |
| 217 | 217 | let naive = date.and_hms_opt(0, 0, 0).expect("midnight is always valid"); | |
| 218 | - | tz.from_local_datetime(&naive) | |
| 219 | - | .earliest() | |
| 220 | - | .map(|dt| dt.with_timezone(&Utc)) | |
| 221 | - | .unwrap_or_else(|| DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc)) | |
| 218 | + | tz.from_local_datetime(&naive).earliest().map_or_else( | |
| 219 | + | || DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc), | |
| 220 | + | |dt| dt.with_timezone(&Utc), | |
| 221 | + | ) | |
| 222 | 222 | } | |
| 223 | 223 | ||
| 224 | - | // ============ Event DTOs ============ | |
| 224 | + | // Event DTOs | |
| 225 | 225 | ||
| 226 | 226 | /// Data for creating a new event. | |
| 227 | 227 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| @@ -342,60 +342,70 @@ impl NewEventBuilder { | |||
| 342 | 342 | } | |
| 343 | 343 | ||
| 344 | 344 | /// Sets the user ID. | |
| 345 | + | #[must_use] | |
| 345 | 346 | pub fn user_id(mut self, user_id: UserId) -> Self { | |
| 346 | 347 | self.user_id = Some(user_id); | |
| 347 | 348 | self | |
| 348 | 349 | } | |
| 349 | 350 | ||
| 350 | 351 | /// Sets the project ID. | |
| 352 | + | #[must_use] | |
| 351 | 353 | pub fn project_id(mut self, project_id: ProjectId) -> Self { | |
| 352 | 354 | self.project_id = Some(project_id); | |
| 353 | 355 | self | |
| 354 | 356 | } | |
| 355 | 357 | ||
| 356 | 358 | /// Sets the contact ID. | |
| 359 | + | #[must_use] | |
| 357 | 360 | pub fn contact_id(mut self, contact_id: ContactId) -> Self { | |
| 358 | 361 | self.contact_id = Some(contact_id); | |
| 359 | 362 | self | |
| 360 | 363 | } | |
| 361 | 364 | ||
| 362 | 365 | /// Sets the description. | |
| 366 | + | #[must_use] | |
| 363 | 367 | pub fn description(mut self, description: impl Into<String>) -> Self { | |
| 364 | 368 | self.description = description.into(); | |
| 365 | 369 | self | |
| 366 | 370 | } | |
| 367 | 371 | ||
| 368 | 372 | /// Sets the end time. | |
| 373 | + | #[must_use] | |
| 369 | 374 | pub fn end_time(mut self, end_time: DateTime<Utc>) -> Self { | |
| 370 | 375 | self.end_time = Some(end_time); | |
| 371 | 376 | self | |
| 372 | 377 | } | |
| 373 | 378 | ||
| 374 | 379 | /// Sets the location. | |
| 380 | + | #[must_use] | |
| 375 | 381 | pub fn location(mut self, location: impl Into<String>) -> Self { | |
| 376 | 382 | self.location = Some(location.into()); | |
| 377 | 383 | self | |
| 378 | 384 | } | |
| 379 | 385 | ||
| 380 | 386 | /// Sets the linked task ID (for time-blocking). | |
| 387 | + | #[must_use] | |
| 381 | 388 | pub fn linked_task_id(mut self, task_id: TaskId) -> Self { | |
| 382 | 389 | self.linked_task_id = Some(task_id); | |
| 383 | 390 | self | |
| 384 | 391 | } | |
| 385 | 392 | ||
| 386 | 393 | /// Sets the recurrence pattern. | |
| 394 | + | #[must_use] | |
| 387 | 395 | pub fn recurrence(mut self, recurrence: Recurrence) -> Self { | |
| 388 | 396 | self.recurrence = recurrence; | |
| 389 | 397 | self | |
| 390 | 398 | } | |
| 391 | 399 | ||
| 392 | 400 | /// Sets the rich recurrence rule. | |
| 401 | + | #[must_use] | |
| 393 | 402 | pub fn recurrence_rule(mut self, rule: RecurrenceRule) -> Self { | |
| 394 | 403 | self.recurrence_rule = Some(rule); | |
| 395 | 404 | self | |
| 396 | 405 | } | |
| 397 | 406 | ||
| 398 | 407 | /// Sets the block type. | |
| 408 | + | #[must_use] | |
| 399 | 409 | pub fn block_type(mut self, block_type: BlockType) -> Self { | |
| 400 | 410 | self.block_type = Some(block_type); | |
| 401 | 411 | self | |
| @@ -521,7 +531,7 @@ mod is_all_day_tests { | |||
| 521 | 531 | #[test] | |
| 522 | 532 | fn snap_keeps_midnight_end_exclusive_no_growth() { | |
| 523 | 533 | // Editing an existing single-day all-day event (stored 00:00..next-00:00) | |
| 524 | - | // must not grow it by a day — the midnight end is already exclusive. | |
| 534 | + | // must not grow it by a day, the midnight end is already exclusive. | |
| 525 | 535 | let start = Utc.with_ymd_and_hms(2026, 7, 4, 0, 0, 0).unwrap(); | |
| 526 | 536 | let end = Utc.with_ymd_and_hms(2026, 7, 5, 0, 0, 0).unwrap(); | |
| 527 | 537 | let (s, e) = snap_all_day_span(start, Some(end), &Utc); |
| @@ -6,7 +6,7 @@ use chrono::{DateTime, Utc}; | |||
| 6 | 6 | use serde::{Deserialize, Serialize}; | |
| 7 | 7 | use strum_macros::EnumString; | |
| 8 | 8 | ||
| 9 | - | // ============ Milestones ============ | |
| 9 | + | // Milestones | |
| 10 | 10 | ||
| 11 | 11 | /// Lifecycle status of a milestone. | |
| 12 | 12 | // `ascii_case_insensitive` so the lowercase `db_value()` form ("open"/"completed") |
| @@ -4,7 +4,7 @@ use crate::id_types::{MonthlyGoalId, MonthlyReflectionId, UserId}; | |||
| 4 | 4 | use chrono::{DateTime, Utc}; | |
| 5 | 5 | use serde::{Deserialize, Serialize}; | |
| 6 | 6 | ||
| 7 | - | // ============ Monthly Goal ============ | |
| 7 | + | // Monthly Goal | |
| 8 | 8 | ||
| 9 | 9 | /// Status of a monthly goal. | |
| 10 | 10 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] | |
| @@ -38,7 +38,7 @@ impl std::str::FromStr for MonthlyGoalStatus { | |||
| 38 | 38 | } | |
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | - | /// A monthly goal — free-text item set at the start of each month. | |
| 41 | + | /// A monthly goal, free-text item set at the start of each month. | |
| 42 | 42 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 43 | 43 | #[serde(rename_all = "camelCase")] | |
| 44 | 44 | pub struct MonthlyGoal { | |
| @@ -54,7 +54,7 @@ pub struct MonthlyGoal { | |||
| 54 | 54 | pub updated_at: DateTime<Utc>, | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | - | // ============ Monthly Reflection ============ | |
| 57 | + | // Monthly Reflection | |
| 58 | 58 | ||
| 59 | 59 | /// A monthly reflection capturing highlights and areas for improvement. | |
| 60 | 60 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| @@ -11,7 +11,7 @@ use chrono::{DateTime, Utc}; | |||
| 11 | 11 | use serde::{Deserialize, Serialize}; | |
| 12 | 12 | use strum_macros::EnumString; | |
| 13 | 13 | ||
| 14 | - | // ============ Project Types ============ | |
| 14 | + | // Project Types | |
| 15 | 15 | ||
| 16 | 16 | /// Classification of project types. | |
| 17 | 17 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, EnumString)] | |
| @@ -165,7 +165,7 @@ pub struct Project { | |||
| 165 | 165 | pub group_id: Option<String>, | |
| 166 | 166 | } | |
| 167 | 167 | ||
| 168 | - | // ============ Project DTOs ============ | |
| 168 | + | // Project DTOs | |
| 169 | 169 | ||
| 170 | 170 | /// Data for creating a new project. | |
| 171 | 171 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| @@ -11,7 +11,7 @@ use strum_macros::EnumString; | |||
| 11 | 11 | ||
| 12 | 12 | use super::shared::{DbValue, ParseableEnum, SortDirection}; | |
| 13 | 13 | ||
| 14 | - | // ============ Saved Views ============ | |
| 14 | + | // Saved Views | |
| 15 | 15 | ||
| 16 | 16 | /// Type of view for saved filters. | |
| 17 | 17 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, EnumString)] | |
| @@ -113,7 +113,7 @@ pub struct SavedView { | |||
| 113 | 113 | /// Unique view ID. | |
| 114 | 114 | pub id: SavedViewId, | |
| 115 | 115 | /// User who owns this view. Not serialized into API responses; `default` lets a | |
| 116 | - | /// backup (which omits it) still deserialize -- restore re-homes the row to the | |
| 116 | + | /// backup (which omits it) still deserialize, restore re-homes the row to the | |
| 117 | 117 | /// target user, so the placeholder is never persisted. | |
| 118 | 118 | #[serde(default, skip_serializing)] | |
| 119 | 119 | pub user_id: UserId, | |
| @@ -137,7 +137,7 @@ pub struct SavedView { | |||
| 137 | 137 | pub updated_at: DateTime<Utc>, | |
| 138 | 138 | } | |
| 139 | 139 | ||
| 140 | - | // ============ Saved View DTOs ============ | |
| 140 | + | // Saved View DTOs | |
| 141 | 141 | ||
| 142 | 142 | /// Data for creating a new saved view. | |
| 143 | 143 | #[derive(Debug, Clone, Serialize, Deserialize)] |