systemprompt_identifiers/user.rs
1//! User identifier — an opaque, checked string.
2//!
3//! Every `UserId` names a row in the `users` table. New users are minted with
4//! a UUID v4 string ([`UserId::generate`]), but the column is TEXT and existing
5//! deployments hold non-UUID ids (seeded admins, imported users, service
6//! accounts), so the shape is not part of the contract. `try_new` rejects
7//! only what can never be a user id: an empty or whitespace-bearing value,
8//! control characters, and the retired `"unset"` sentinel. It is the
9//! constructor for values arriving from outside (a JWT `sub`, a header, a
10//! path segment); `new` is for values already known to be valid, such as a
11//! decoded `users.id`.
12//!
13//! Copyright (c) systemprompt.io — Business Source License 1.1.
14//! See <https://systemprompt.io> for licensing details.
15
16use crate::error::IdValidationError;
17
18crate::define_id!(UserId, checked, validate_user_id);
19
20fn validate_user_id(value: &str) -> Result<(), IdValidationError> {
21 if value.is_empty() {
22 return Err(IdValidationError::empty("UserId"));
23 }
24 if value.chars().any(|c| c.is_whitespace() || c.is_control()) {
25 return Err(IdValidationError::invalid(
26 "UserId",
27 "must not contain whitespace or control characters",
28 ));
29 }
30 if value.eq_ignore_ascii_case("unset") {
31 return Err(IdValidationError::invalid(
32 "UserId",
33 "'unset' is a retired sentinel, not a user id",
34 ));
35 }
36 Ok(())
37}
38
39impl UserId {
40 pub fn generate() -> Self {
41 Self(uuid::Uuid::new_v4().to_string())
42 }
43}