1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Clone, Debug, Default, Serialize, Deserialize)]
8pub struct Claims {
9 pub values: HashMap<String, serde_json::Value>,
11}
12
13impl Claims {
14 pub fn new() -> Self {
16 Self::default()
17 }
18
19 pub fn get_str(&self, key: &str) -> Option<&str> {
21 self.values.get(key).and_then(|v| v.as_str())
22 }
23
24 pub fn get_i64(&self, key: &str) -> Option<i64> {
26 self.values.get(key).and_then(|v| v.as_i64())
27 }
28
29 pub fn get_bool(&self, key: &str) -> Option<bool> {
31 self.values.get(key).and_then(|v| v.as_bool())
32 }
33
34 pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
36 self.values.get(key)
37 }
38
39 pub fn set(&mut self, key: impl Into<String>, value: serde_json::Value) {
41 self.values.insert(key.into(), value);
42 }
43
44 pub fn to_json(&self) -> String {
46 serde_json::to_string(&self.values).unwrap_or_else(|_| "{}".to_string())
47 }
48
49 pub fn prefixed_entries(&self, prefix: &str) -> Vec<(String, String)> {
54 self.values
55 .iter()
56 .filter(|(key, _)| valid_setting_suffix(key))
57 .map(|(k, v)| {
58 let key = format!("{}{}", prefix, k);
59 let value = match v {
60 serde_json::Value::String(s) => s.clone(),
61 other => other.to_string(),
62 };
63 (key, value)
64 })
65 .collect()
66 }
67}
68
69fn valid_setting_suffix(value: &str) -> bool {
70 !value.is_empty()
71 && value.split('.').all(|segment| {
72 let mut characters = segment.chars();
73 characters
74 .next()
75 .is_some_and(|first| first.is_ascii_alphabetic() || first == '_')
76 && characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
77 })
78}
79
80impl From<HashMap<String, serde_json::Value>> for Claims {
81 fn from(values: HashMap<String, serde_json::Value>) -> Self {
82 Self { values }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_claims_get_str() {
92 let mut claims = Claims::new();
93 claims.set("role", serde_json::Value::String("admin".into()));
94
95 assert_eq!(claims.get_str("role"), Some("admin"));
96 assert_eq!(claims.get_str("missing"), None);
97 }
98
99 #[test]
100 fn test_claims_get_i64() {
101 let mut claims = Claims::new();
102 claims.set("user_id", serde_json::Value::Number(42.into()));
103
104 assert_eq!(claims.get_i64("user_id"), Some(42));
105 }
106
107 #[test]
108 fn test_claims_to_json() {
109 let mut claims = Claims::new();
110 claims.set("role", serde_json::Value::String("user".into()));
111 claims.set("id", serde_json::Value::Number(123.into()));
112
113 let json = claims.to_json();
114 assert!(json.contains("role"));
115 assert!(json.contains("user"));
116 }
117
118 #[test]
119 fn test_claims_prefixed_entries() {
120 let mut claims = Claims::new();
121 claims.set("role", serde_json::Value::String("admin".into()));
122 claims.set(
123 "email",
124 serde_json::Value::String("test@example.com".into()),
125 );
126
127 let entries = claims.prefixed_entries("request.jwt.claims.");
128 assert_eq!(entries.len(), 2);
129 assert!(entries.iter().any(|(k, _)| k == "request.jwt.claims.role"));
130 }
131
132 #[test]
133 fn prefixed_entries_render_structured_values_as_json() {
134 let mut claims = Claims::new();
135 claims.set("org_class", serde_json::json!(["defense", "datacenters"]));
136
137 let entries = claims.prefixed_entries("request.jwt.claims.");
138 assert_eq!(
139 entries,
140 vec![(
141 "request.jwt.claims.org_class".to_string(),
142 r#"["defense","datacenters"]"#.to_string()
143 )]
144 );
145 }
146
147 #[test]
148 fn prefixed_entries_skip_claim_names_that_are_not_postgresql_settings() {
149 let mut claims = Claims::new();
150 claims.set(
151 "https://hasura.io/jwt/claims",
152 serde_json::json!({"x-hasura-role": "user"}),
153 );
154 claims.set("2fa", serde_json::json!(true));
155 claims.set("user_id", serde_json::json!("user-1"));
156
157 let entries = claims.prefixed_entries("request.jwt.claims.");
158 assert_eq!(
159 entries,
160 vec![(
161 "request.jwt.claims.user_id".to_string(),
162 "user-1".to_string()
163 )]
164 );
165 }
166}