1use crate::observation::Observation;
4use crate::project::ProjectId;
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use ulid::Ulid;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct EntityId(pub Ulid);
13
14impl EntityId {
15 pub fn new() -> Self {
16 Self(Ulid::new())
17 }
18
19 pub fn from_string(s: &str) -> Result<Self, ulid::DecodeError> {
20 Ok(Self(Ulid::from_string(s)?))
21 }
22}
23
24impl Default for EntityId {
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl std::fmt::Display for EntityId {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 write!(f, "{}", self.0)
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct EntityType(pub String);
39
40impl EntityType {
41 pub fn new(s: impl Into<String>) -> Self {
42 Self(s.into())
43 }
44
45 pub fn as_str(&self) -> &str {
46 &self.0
47 }
48}
49
50impl From<&str> for EntityType {
51 fn from(s: &str) -> Self {
52 Self(s.to_string())
53 }
54}
55
56impl From<String> for EntityType {
57 fn from(s: String) -> Self {
58 Self(s)
59 }
60}
61
62impl From<&String> for EntityType {
63 fn from(s: &String) -> Self {
64 Self(s.clone())
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Entity {
71 pub id: EntityId,
73
74 pub project_id: ProjectId,
76
77 pub name: String,
79
80 pub entity_type: EntityType,
82
83 pub observations: Vec<Observation>,
85
86 pub tags: Vec<String>,
88
89 #[serde(default)]
91 pub metadata: HashMap<String, serde_json::Value>,
92
93 pub created_at: DateTime<Utc>,
95
96 pub updated_at: DateTime<Utc>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub embedding: Option<Vec<f32>>,
102}
103
104impl Entity {
105 pub fn new(
107 project_id: ProjectId,
108 name: impl Into<String>,
109 entity_type: impl Into<EntityType>,
110 ) -> Self {
111 let now = Utc::now();
112 Self {
113 id: EntityId::new(),
114 project_id,
115 name: name.into(),
116 entity_type: entity_type.into(),
117 observations: Vec::new(),
118 tags: Vec::new(),
119 metadata: HashMap::new(),
120 created_at: now,
121 updated_at: now,
122 embedding: None,
123 }
124 }
125
126 pub fn add_observation(&mut self, content: impl Into<String>) -> &Observation {
128 let obs = Observation::new(content);
129 self.observations.push(obs);
130 self.updated_at = Utc::now();
131 self.observations.last().expect("observations cannot be empty after push")
133 }
134
135 pub fn add_tag(&mut self, tag: impl Into<String>) {
137 let tag = tag.into();
138 if !self.tags.contains(&tag) {
139 self.tags.push(tag);
140 self.updated_at = Utc::now();
141 }
142 }
143
144 pub fn remove_tag(&mut self, tag: &str) -> bool {
146 if let Some(pos) = self.tags.iter().position(|t| t == tag) {
147 self.tags.remove(pos);
148 self.updated_at = Utc::now();
149 true
150 } else {
151 false
152 }
153 }
154
155 pub fn has_tag(&self, tag: &str) -> bool {
157 self.tags.iter().any(|t| t == tag)
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct NewEntity {
164 pub name: String,
165 pub entity_type: String,
166 pub observations: Vec<String>,
167 #[serde(default)]
168 pub tags: Vec<String>,
169 #[serde(default)]
170 pub metadata: HashMap<String, serde_json::Value>,
171}
172
173impl NewEntity {
174 pub fn new(name: impl Into<String>, entity_type: impl Into<String>) -> Self {
175 Self {
176 name: name.into(),
177 entity_type: entity_type.into(),
178 observations: Vec::new(),
179 tags: Vec::new(),
180 metadata: HashMap::new(),
181 }
182 }
183
184 pub fn with_observation(mut self, obs: impl Into<String>) -> Self {
185 self.observations.push(obs.into());
186 self
187 }
188
189 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
190 self.tags.push(tag.into());
191 self
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_entity_creation() {
201 let project_id = ProjectId::new();
202 let entity = Entity::new(project_id, "John_Smith", "person");
203
204 assert_eq!(entity.name, "John_Smith");
205 assert_eq!(entity.entity_type.as_str(), "person");
206 assert!(entity.observations.is_empty());
207 assert!(entity.tags.is_empty());
208 }
209
210 #[test]
211 fn test_add_observation() {
212 let project_id = ProjectId::new();
213 let mut entity = Entity::new(project_id, "John_Smith", "person");
214
215 entity.add_observation("Works at Google");
216 assert_eq!(entity.observations.len(), 1);
217 assert_eq!(entity.observations[0].content, "Works at Google");
218 }
219
220 #[test]
221 fn test_tags() {
222 let project_id = ProjectId::new();
223 let mut entity = Entity::new(project_id, "John_Smith", "person");
224
225 entity.add_tag("technical");
226 entity.add_tag("mentor");
227 entity.add_tag("technical"); assert_eq!(entity.tags.len(), 2);
230 assert!(entity.has_tag("technical"));
231 assert!(entity.has_tag("mentor"));
232
233 entity.remove_tag("mentor");
234 assert!(!entity.has_tag("mentor"));
235 }
236}