hexser_potions/auth/
mod.rs1use hexser::prelude::*;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct User {
20 pub id: String,
21 pub email: String,
22}
23
24impl HexEntity for User {
25 type Id = String;
26}
27
28pub trait UserRepository: Repository<User> {
30 fn find_by_email(&self, email: &str) -> HexResult<Option<User>>;
31}
32
33#[derive(Default)]
35pub struct InMemoryUserRepository {
36 pub users: Vec<User>,
37}
38
39impl Repository<User> for InMemoryUserRepository {
40 fn save(&mut self, entity: User) -> HexResult<()> {
41 if let Some(existing) = self.users.iter_mut().find(|u| u.id == entity.id) {
43 *existing = entity;
44 } else {
45 self.users.push(entity);
46 }
47 Ok(())
48 }
49}
50
51impl UserRepository for InMemoryUserRepository {
52 fn find_by_email(&self, email: &str) -> HexResult<Option<User>> {
53 Ok(self.users.iter().find(|u| u.email == email).cloned())
54 }
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum UserFilter {
59 All,
60 ByEmail(String),
61 ById(String),
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum UserSortKey {
66 Email,
67 Id,
68}
69
70impl hexser::ports::repository::QueryRepository<User> for InMemoryUserRepository {
71 type Filter = UserFilter;
72 type SortKey = UserSortKey;
73
74 fn find_one(&self, filter: &UserFilter) -> HexResult<Option<User>> {
75 let found = match filter {
76 UserFilter::All => self.users.first().cloned(),
77 UserFilter::ByEmail(e) => self.users.iter().find(|u| &u.email == e).cloned(),
78 UserFilter::ById(id) => self.users.iter().find(|u| &u.id == id).cloned(),
79 };
80 Ok(found)
81 }
82
83 fn find(
84 &self,
85 filter: &UserFilter,
86 opts: hexser::ports::repository::FindOptions<UserSortKey>,
87 ) -> HexResult<Vec<User>> {
88 let mut items: Vec<User> = match filter {
89 UserFilter::All => self.users.clone(),
90 UserFilter::ByEmail(e) => self
91 .users
92 .iter()
93 .filter(|u| &u.email == e)
94 .cloned()
95 .collect(),
96 UserFilter::ById(id) => self.users.iter().filter(|u| &u.id == id).cloned().collect(),
97 };
98 if let Some(mut sorts) = opts.sort {
99 for s in sorts.drain(..).rev() {
100 items.sort_by(|a, b| {
101 let mut ord = match s.key {
102 UserSortKey::Email => a.email.cmp(&b.email),
103 UserSortKey::Id => a.id.cmp(&b.id),
104 };
105 if let hexser::ports::repository::Direction::Desc = s.direction {
106 ord = ord.reverse();
107 }
108 ord
109 });
110 }
111 }
112 if let Some(offset) = opts.offset {
113 let offset_usize: usize = std::convert::TryInto::try_into(offset).unwrap_or(usize::MAX);
114 if offset_usize < items.len() {
115 items = items.split_off(offset_usize);
116 } else {
117 items.clear();
118 }
119 }
120 if let Some(limit) = opts.limit {
121 let limit_usize: usize = std::convert::TryInto::try_into(limit).unwrap_or(usize::MAX);
122 if items.len() > limit_usize {
123 items.truncate(limit_usize);
124 }
125 }
126 Ok(items)
127 }
128
129 fn exists(&self, filter: &UserFilter) -> HexResult<bool> {
130 Ok(self.find_one(filter)?.is_some())
131 }
132
133 fn count(&self, filter: &UserFilter) -> HexResult<u64> {
134 let n = match filter {
135 UserFilter::All => self.users.len(),
136 UserFilter::ByEmail(e) => self.users.iter().filter(|u| &u.email == e).count(),
137 UserFilter::ById(id) => self.users.iter().filter(|u| &u.id == id).count(),
138 };
139 Ok(n as u64)
140 }
141
142 fn delete_where(&mut self, filter: &UserFilter) -> HexResult<u64> {
143 let before = self.users.len();
144 match filter {
145 UserFilter::All => {
146 self.users.clear();
147 }
148 UserFilter::ByEmail(e) => {
149 self.users.retain(|u| &u.email != e);
150 }
151 UserFilter::ById(id) => {
152 self.users.retain(|u| &u.id != id);
153 }
154 }
155 let removed = before.saturating_sub(self.users.len());
156 Ok(removed as u64)
157 }
158}
159
160pub struct SignUpUser {
162 pub email: String,
163}
164
165impl Directive for SignUpUser {
166 fn validate(&self) -> HexResult<()> {
167 if self.email.contains('@') {
168 Ok(())
169 } else {
170 Err(hexser::Hexserror::validation_field(
171 "Invalid email",
172 "email",
173 ))
174 }
175 }
176}
177
178pub fn execute_signup<R>(repo: &mut R, cmd: SignUpUser) -> HexResult<User>
183where
184 R: UserRepository + hexser::ports::repository::QueryRepository<User, Filter = UserFilter>,
185{
186 cmd.validate()?;
187
188 if repo.find_by_email(&cmd.email)?.is_some() {
189 return Err(hexser::Hexserror::domain(
190 "E_HEXSER_POTIONS_EMAIL_TAKEN",
191 "Email already registered",
192 ));
193 }
194
195 let count =
197 <R as hexser::ports::repository::QueryRepository<User>>::count(&*repo, &UserFilter::All)?;
198 let next_id = std::format!("user-{}", count + 1);
199
200 let user = User {
201 id: next_id,
202 email: cmd.email,
203 };
204
205 repo.save(user.clone())?;
206 Ok(user)
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn signup_happy_path() {
215 let mut repo = InMemoryUserRepository::default();
216 let user = execute_signup(
217 &mut repo,
218 SignUpUser {
219 email: "a@b.com".into(),
220 },
221 )
222 .unwrap();
223 assert_eq!(user.email, "a@b.com");
224 assert!(repo.find_by_email("a@b.com").unwrap().is_some());
225 }
226
227 #[test]
228 fn signup_rejects_invalid_email() {
229 let mut repo = InMemoryUserRepository::default();
230 let res = execute_signup(
231 &mut repo,
232 SignUpUser {
233 email: "not-an-email".into(),
234 },
235 );
236 assert!(res.is_err());
237 }
238
239 #[test]
240 fn signup_rejects_duplicates() {
241 let mut repo = InMemoryUserRepository::default();
242 execute_signup(
243 &mut repo,
244 SignUpUser {
245 email: "a@b.com".into(),
246 },
247 )
248 .unwrap();
249 let duplicate = execute_signup(
250 &mut repo,
251 SignUpUser {
252 email: "a@b.com".into(),
253 },
254 );
255 assert!(duplicate.is_err());
256 }
257}