1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use ulid::Ulid;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct ProjectId(pub Ulid);
10
11impl ProjectId {
12 pub fn new() -> Self {
13 Self(Ulid::new())
14 }
15
16 pub fn from_string(s: &str) -> Result<Self, ulid::DecodeError> {
17 Ok(Self(Ulid::from_string(s)?))
18 }
19}
20
21impl Default for ProjectId {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl std::fmt::Display for ProjectId {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 write!(f, "{}", self.0)
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Project {
36 pub id: ProjectId,
38
39 pub name: String,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub description: Option<String>,
45
46 pub created_at: DateTime<Utc>,
48
49 #[serde(default)]
51 pub settings: ProjectSettings,
52}
53
54impl Project {
55 pub fn new(name: impl Into<String>) -> Self {
57 Self {
58 id: ProjectId::new(),
59 name: name.into(),
60 description: None,
61 created_at: Utc::now(),
62 settings: ProjectSettings::default(),
63 }
64 }
65
66 pub fn with_description(mut self, description: impl Into<String>) -> Self {
68 self.description = Some(description.into());
69 self
70 }
71
72 pub fn validate_name(name: &str) -> bool {
74 !name.is_empty()
75 && name.len() <= 100
76 && name
77 .chars()
78 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
79 }
80}
81
82#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84pub struct ProjectSettings {
85 #[serde(default = "default_true")]
87 pub fulltext_enabled: bool,
88
89 #[serde(default = "default_fuzzy_threshold")]
91 pub fuzzy_threshold: f32,
92}
93
94fn default_true() -> bool {
95 true
96}
97
98fn default_fuzzy_threshold() -> f32 {
99 0.3
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn test_project_creation() {
108 let project = Project::new("my-project");
109 assert_eq!(project.name, "my-project");
110 assert!(project.description.is_none());
111 }
112
113 #[test]
114 fn test_project_with_description() {
115 let project = Project::new("security-research")
116 .with_description("Security vulnerability findings");
117 assert_eq!(project.description, Some("Security vulnerability findings".to_string()));
118 }
119
120 #[test]
121 fn test_validate_project_name() {
122 assert!(Project::validate_name("my-project"));
123 assert!(Project::validate_name("my_project_123"));
124 assert!(Project::validate_name("MyProject"));
125 assert!(!Project::validate_name(""));
126 assert!(!Project::validate_name("my project")); assert!(!Project::validate_name("my.project")); }
129}