Skip to main content

seam_server/
context.rs

1/* src/server/core/rust/src/context.rs */
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::errors::SeamError;
9
10/// Definition for a single context field: where to extract it and its schema.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ContextFieldDef {
13  pub extract: String,
14  pub schema: Value,
15}
16
17/// Map of context key -> field definition.
18pub type ContextConfig = BTreeMap<String, ContextFieldDef>;
19
20/// Raw extracted values from HTTP request (e.g. headers).
21/// `None` means the source was not present.
22pub type RawContextMap = BTreeMap<String, Option<String>>;
23
24/// Parse an extract rule like "header:authorization" into (source, key).
25pub 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
31/// Collect all HTTP header names that need extraction from context config.
32/// Deduplicates and returns lowercase header names.
33pub 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
47/// Resolve context values from raw extracted data for the given requested keys.
48/// Returns a JSON object with the requested context fields.
49pub 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        // Try JSON parse for complex types, fallback to string
68        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
80/// Extract property key names from a JTD schema's `properties` field.
81pub 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}