Skip to main content

max / goingson

6.8 KB · 231 lines History Blame Raw
1 //! Project domain types and DTOs.
2 //!
3 //! Projects group related tasks, events, and emails under a single umbrella.
4 //! Each project has a type classification (Job, SideProject, Company, Essay,
5 //! Article, Painting, Other) and a lifecycle status (Active, OnHold, Completed,
6 //! Archived). Projects can contain milestones for phased tracking.
7
8 use chrono::{DateTime, Utc};
9 use serde::{Deserialize, Serialize};
10 use strum_macros::EnumString;
11 use crate::id_types::ProjectId;
12 use super::shared::{CssClass, DbValue, ParseableEnum};
13
14 // ============ Project Types ============
15
16 /// Classification of project types.
17 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, EnumString)]
18 pub enum ProjectType {
19 /// Employment or client work.
20 #[strum(serialize = "Job")]
21 Job,
22 /// Personal side project.
23 #[strum(serialize = "SideProject")]
24 SideProject,
25 /// Company or business venture.
26 #[strum(serialize = "Company")]
27 Company,
28 /// Long-form writing project.
29 #[strum(serialize = "Essay")]
30 Essay,
31 /// Article or blog post.
32 #[strum(serialize = "Article")]
33 Article,
34 /// Art or visual project.
35 #[strum(serialize = "Painting")]
36 Painting,
37 /// Uncategorized project.
38 #[strum(serialize = "Other")]
39 #[default]
40 Other,
41 }
42
43 impl ProjectType {
44 /// Returns a human-readable display string.
45 pub fn as_str(&self) -> &'static str {
46 match self {
47 ProjectType::Job => "Job",
48 ProjectType::SideProject => "Side Project",
49 ProjectType::Company => "Company",
50 ProjectType::Essay => "Essay",
51 ProjectType::Article => "Article",
52 ProjectType::Painting => "Painting",
53 ProjectType::Other => "Other",
54 }
55 }
56
57 }
58
59 impl ParseableEnum for ProjectType {}
60
61 impl DbValue for ProjectType {
62 fn db_value(&self) -> &'static str {
63 match self {
64 ProjectType::Job => "Job",
65 ProjectType::SideProject => "SideProject",
66 ProjectType::Company => "Company",
67 ProjectType::Essay => "Essay",
68 ProjectType::Article => "Article",
69 ProjectType::Painting => "Painting",
70 ProjectType::Other => "Other",
71 }
72 }
73 }
74
75 impl CssClass for ProjectType {
76 fn css_class(&self) -> &'static str {
77 match self {
78 ProjectType::Job => "type-job",
79 ProjectType::SideProject => "type-sideproject",
80 ProjectType::Company => "type-company",
81 ProjectType::Essay => "type-essay",
82 ProjectType::Article => "type-article",
83 ProjectType::Painting => "type-painting",
84 ProjectType::Other => "type-other",
85 }
86 }
87 }
88
89 /// Lifecycle status of a project.
90 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, EnumString)]
91 pub enum ProjectStatus {
92 /// Currently being worked on.
93 #[strum(serialize = "Active")]
94 #[default]
95 Active,
96 /// Temporarily paused.
97 #[strum(serialize = "OnHold")]
98 OnHold,
99 /// Successfully finished.
100 #[strum(serialize = "Completed")]
101 Completed,
102 /// No longer relevant, hidden from views.
103 #[strum(serialize = "Archived")]
104 Archived,
105 }
106
107 impl ProjectStatus {
108 /// Returns a human-readable display string.
109 pub fn as_str(&self) -> &'static str {
110 match self {
111 ProjectStatus::Active => "Active",
112 ProjectStatus::OnHold => "On Hold",
113 ProjectStatus::Completed => "Completed",
114 ProjectStatus::Archived => "Archived",
115 }
116 }
117
118 }
119
120 impl ParseableEnum for ProjectStatus {}
121
122 impl DbValue for ProjectStatus {
123 fn db_value(&self) -> &'static str {
124 match self {
125 ProjectStatus::Active => "Active",
126 ProjectStatus::OnHold => "OnHold",
127 ProjectStatus::Completed => "Completed",
128 ProjectStatus::Archived => "Archived",
129 }
130 }
131 }
132
133 impl CssClass for ProjectStatus {
134 fn css_class(&self) -> &'static str {
135 match self {
136 ProjectStatus::Active => "status-active",
137 ProjectStatus::OnHold => "status-onhold",
138 ProjectStatus::Completed => "status-completed",
139 ProjectStatus::Archived => "status-archived",
140 }
141 }
142 }
143
144 /// A project that groups related tasks, events, and emails.
145 #[derive(Debug, Clone, Serialize, Deserialize)]
146 #[serde(rename_all = "camelCase")]
147 pub struct Project {
148 /// Unique identifier.
149 pub id: ProjectId,
150 /// Project name.
151 pub name: String,
152 /// Optional description.
153 pub description: String,
154 /// Project classification.
155 pub project_type: ProjectType,
156 /// Current lifecycle status.
157 pub status: ProjectStatus,
158 /// When the project was created.
159 pub created_at: DateTime<Utc>,
160 }
161
162 // ============ Project DTOs ============
163
164 /// Data for creating a new project.
165 #[derive(Debug, Clone, Serialize, Deserialize)]
166 pub struct NewProject {
167 pub name: String,
168 pub description: String,
169 pub project_type: ProjectType,
170 pub status: ProjectStatus,
171 }
172
173 /// Data for updating an existing project.
174 #[derive(Debug, Clone, Serialize, Deserialize)]
175 pub struct UpdateProject {
176 pub name: String,
177 pub description: String,
178 pub project_type: ProjectType,
179 pub status: ProjectStatus,
180 }
181
182 #[cfg(test)]
183 mod tests {
184 use super::*;
185 use crate::models::shared::{DbValue, ParseableEnum};
186
187 #[test]
188 fn project_type_db_value_round_trips() {
189 for pt in [
190 ProjectType::Job,
191 ProjectType::SideProject,
192 ProjectType::Company,
193 ProjectType::Essay,
194 ProjectType::Article,
195 ProjectType::Painting,
196 ProjectType::Other,
197 ] {
198 let parsed = ProjectType::from_str_or_default(pt.db_value());
199 assert_eq!(parsed, pt, "db_value must round-trip for {pt:?}");
200 }
201 }
202
203 #[test]
204 fn project_status_db_value_round_trips() {
205 for st in [
206 ProjectStatus::Active,
207 ProjectStatus::OnHold,
208 ProjectStatus::Completed,
209 ProjectStatus::Archived,
210 ] {
211 let parsed = ProjectStatus::from_str_or_default(st.db_value());
212 assert_eq!(parsed, st, "db_value must round-trip for {st:?}");
213 }
214 }
215
216 #[test]
217 fn unknown_project_enum_strings_fall_back_to_default() {
218 assert_eq!(ProjectType::from_str_or_default("bogus"), ProjectType::Other);
219 assert_eq!(ProjectStatus::from_str_or_default("bogus"), ProjectStatus::Active);
220 }
221
222 #[test]
223 fn display_and_db_value_diverge_where_expected() {
224 // as_str is human-facing ("Side Project"); db_value is the stored token ("SideProject").
225 assert_eq!(ProjectType::SideProject.as_str(), "Side Project");
226 assert_eq!(ProjectType::SideProject.db_value(), "SideProject");
227 assert_eq!(ProjectStatus::OnHold.as_str(), "On Hold");
228 assert_eq!(ProjectStatus::OnHold.db_value(), "OnHold");
229 }
230 }
231