data_connector/
context.rs1use std::collections::HashMap;
12
13use crate::hooks::ExtraColumns;
14
15#[derive(Debug, Clone, Default)]
24pub struct RequestContext {
25 data: HashMap<String, String>,
26}
27
28impl RequestContext {
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn with_data(data: HashMap<String, String>) -> Self {
35 Self { data }
36 }
37
38 pub fn get(&self, key: &str) -> Option<&str> {
40 self.data.get(key).map(String::as_str)
41 }
42
43 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
45 self.data.insert(key.into(), value.into());
46 }
47
48 pub fn data(&self) -> &HashMap<String, String> {
50 &self.data
51 }
52}
53
54tokio::task_local! {
59 static REQUEST_CONTEXT: RequestContext;
60 static HOOK_EXTRA_COLUMNS: ExtraColumns;
61}
62
63pub async fn with_request_context<F, T>(ctx: RequestContext, f: F) -> T
68where
69 F: std::future::Future<Output = T>,
70{
71 REQUEST_CONTEXT.scope(ctx, f).await
72}
73
74pub fn current_request_context() -> Option<RequestContext> {
78 REQUEST_CONTEXT.try_with(|ctx| ctx.clone()).ok()
79}
80
81pub async fn with_extra_columns<F, T>(extra: ExtraColumns, f: F) -> T
86where
87 F: std::future::Future<Output = T>,
88{
89 HOOK_EXTRA_COLUMNS.scope(extra, f).await
90}
91
92pub fn current_extra_columns() -> Option<ExtraColumns> {
97 HOOK_EXTRA_COLUMNS.try_with(|ec| ec.clone()).ok()
98}
99
100#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn request_context_new_is_empty() {
110 let ctx = RequestContext::new();
111 assert!(ctx.data().is_empty());
112 assert!(ctx.get("anything").is_none());
113 }
114
115 #[test]
116 fn request_context_set_and_get() {
117 let mut ctx = RequestContext::new();
118 ctx.set("tenant_id", "abc");
119 assert_eq!(ctx.get("tenant_id"), Some("abc"));
120 assert!(ctx.get("missing").is_none());
121 }
122
123 #[test]
124 fn request_context_with_data() {
125 let mut data = HashMap::new();
126 data.insert("key".to_string(), "value".to_string());
127 let ctx = RequestContext::with_data(data);
128 assert_eq!(ctx.get("key"), Some("value"));
129 }
130
131 #[tokio::test]
132 async fn current_request_context_returns_none_outside_scope() {
133 assert!(current_request_context().is_none());
134 }
135
136 #[tokio::test]
137 async fn with_request_context_makes_context_available() {
138 let mut ctx = RequestContext::new();
139 ctx.set("store_id", "123");
140
141 let result = with_request_context(ctx, async {
142 let inner = current_request_context().expect("should be set");
143 inner.get("store_id").unwrap().to_string()
144 })
145 .await;
146
147 assert_eq!(result, "123");
148 }
149
150 #[tokio::test]
151 async fn context_not_available_after_scope_exits() {
152 let ctx = RequestContext::new();
153 with_request_context(ctx, async {}).await;
154 assert!(current_request_context().is_none());
155 }
156
157 #[tokio::test]
160 async fn extra_columns_returns_none_outside_scope() {
161 assert!(current_extra_columns().is_none());
162 }
163
164 #[tokio::test]
165 async fn extra_columns_available_inside_scope() {
166 let mut extra = ExtraColumns::new();
167 extra.insert(
168 "tenant_id".to_string(),
169 serde_json::Value::String("t-123".to_string()),
170 );
171
172 let result = with_extra_columns(extra, async {
173 let ec = current_extra_columns().expect("should be set");
174 ec.get("tenant_id").unwrap().as_str().unwrap().to_string()
175 })
176 .await;
177
178 assert_eq!(result, "t-123");
179 }
180
181 #[tokio::test]
182 async fn extra_columns_not_leaked_after_scope() {
183 let extra = ExtraColumns::new();
184 with_extra_columns(extra, async {}).await;
185 assert!(current_extra_columns().is_none());
186 }
187}