1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::errors::SeamError;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ContextFieldDef {
13 pub extract: String,
14 pub schema: Value,
15}
16
17pub type ContextConfig = BTreeMap<String, ContextFieldDef>;
19
20pub type RawContextMap = BTreeMap<String, Option<String>>;
23
24pub fn parse_extract_rule(rule: &str) -> Result<(&str, &str), SeamError> {
26 rule
27 .split_once(':')
28 .ok_or_else(|| SeamError::context_error(format!("Invalid extract rule: '{rule}'")))
29}
30
31pub fn context_extract_keys(config: &ContextConfig) -> Vec<String> {
34 let mut keys = Vec::new();
35 let mut seen = std::collections::HashSet::new();
36 for field in config.values() {
37 if let Ok(("header", header_name)) = parse_extract_rule(&field.extract) {
38 let lower = header_name.to_lowercase();
39 if seen.insert(lower.clone()) {
40 keys.push(lower);
41 }
42 }
43 }
44 keys
45}
46
47pub fn resolve_context(
50 config: &ContextConfig,
51 raw: &RawContextMap,
52 requested_keys: &[String],
53) -> Result<Value, SeamError> {
54 let mut ctx = serde_json::Map::new();
55
56 for key in requested_keys {
57 let Some(field_def) = config.get(key) else {
58 ctx.insert(key.clone(), Value::Null);
59 continue;
60 };
61
62 let (_source, header_name) = parse_extract_rule(&field_def.extract)?;
63 let lower = header_name.to_lowercase();
64
65 match raw.get(&lower) {
66 Some(Some(value)) => {
67 let parsed = serde_json::from_str(value).unwrap_or(Value::String(value.clone()));
69 ctx.insert(key.clone(), parsed);
70 }
71 _ => {
72 ctx.insert(key.clone(), Value::Null);
73 }
74 }
75 }
76
77 Ok(Value::Object(ctx))
78}
79
80pub fn context_keys_from_schema(schema: &Value) -> Vec<String> {
82 schema
83 .get("properties")
84 .and_then(|p| p.as_object())
85 .map(|obj| obj.keys().cloned().collect())
86 .unwrap_or_default()
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn parse_extract_rule_valid() {
95 let (source, key) = parse_extract_rule("header:authorization").unwrap();
96 assert_eq!(source, "header");
97 assert_eq!(key, "authorization");
98 }
99
100 #[test]
101 fn parse_extract_rule_invalid() {
102 assert!(parse_extract_rule("no-colon").is_err());
103 }
104
105 #[test]
106 fn context_extract_keys_deduplicates() {
107 let mut config = ContextConfig::new();
108 config.insert(
109 "token".into(),
110 ContextFieldDef {
111 extract: "header:Authorization".into(),
112 schema: serde_json::json!({"type": "string"}),
113 },
114 );
115 config.insert(
116 "auth".into(),
117 ContextFieldDef {
118 extract: "header:Authorization".into(),
119 schema: serde_json::json!({"type": "string"}),
120 },
121 );
122 let keys = context_extract_keys(&config);
123 assert_eq!(keys.len(), 1);
124 assert_eq!(keys[0], "authorization");
125 }
126
127 #[test]
128 fn resolve_context_string_value() {
129 let mut config = ContextConfig::new();
130 config.insert(
131 "token".into(),
132 ContextFieldDef {
133 extract: "header:authorization".into(),
134 schema: serde_json::json!({"type": "string"}),
135 },
136 );
137 let mut raw = RawContextMap::new();
138 raw.insert("authorization".into(), Some("Bearer abc".into()));
139
140 let ctx = resolve_context(&config, &raw, &["token".into()]).unwrap();
141 assert_eq!(ctx["token"], "Bearer abc");
142 }
143
144 #[test]
145 fn resolve_context_null_value() {
146 let mut config = ContextConfig::new();
147 config.insert(
148 "token".into(),
149 ContextFieldDef {
150 extract: "header:authorization".into(),
151 schema: serde_json::json!({"type": "string"}),
152 },
153 );
154 let raw = RawContextMap::new();
155
156 let ctx = resolve_context(&config, &raw, &["token".into()]).unwrap();
157 assert_eq!(ctx["token"], Value::Null);
158 }
159
160 #[test]
161 fn resolve_context_undefined_key() {
162 let config = ContextConfig::new();
163 let raw = RawContextMap::new();
164
165 let ctx = resolve_context(&config, &raw, &["missing".into()]).unwrap();
166 assert_eq!(ctx["missing"], Value::Null);
167 }
168
169 #[test]
170 fn context_keys_from_schema_extracts_properties() {
171 let schema = serde_json::json!({
172 "properties": {
173 "token": {"type": "string"},
174 "userId": {"type": "string"}
175 }
176 });
177 let mut keys = context_keys_from_schema(&schema);
178 keys.sort();
179 assert_eq!(keys, vec!["token", "userId"]);
180 }
181
182 #[test]
183 fn context_keys_from_schema_empty() {
184 let schema = serde_json::json!({"type": "string"});
185 let keys = context_keys_from_schema(&schema);
186 assert!(keys.is_empty());
187 }
188}