lance_namespace_impls/context.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Dynamic context provider for per-request context overrides.
5//!
6//! This module provides the [`DynamicContextProvider`] trait that enables
7//! per-request context injection (e.g., dynamic authentication headers).
8//!
9//! ## Usage
10//!
11//! Implement the trait and pass to namespace builders:
12//!
13//! ```ignore
14//! use lance_namespace_impls::{RestNamespaceBuilder, DynamicContextProvider, OperationInfo};
15//! use std::collections::HashMap;
16//! use std::sync::Arc;
17//!
18//! #[derive(Debug)]
19//! struct MyProvider;
20//!
21//! impl DynamicContextProvider for MyProvider {
22//! fn provide_context(&self, info: &OperationInfo) -> HashMap<String, String> {
23//! let mut context = HashMap::new();
24//! context.insert("headers.Authorization".to_string(), format!("Bearer {}", get_current_token()));
25//! context.insert("headers.X-Request-Id".to_string(), generate_request_id());
26//! context
27//! }
28//! }
29//!
30//! let namespace = RestNamespaceBuilder::new("https://api.example.com")
31//! .context_provider(Arc::new(MyProvider))
32//! .build();
33//! ```
34//!
35//! For RestNamespace, context keys that start with `headers.` are converted to HTTP headers
36//! by stripping the prefix. For example, `{"headers.Authorization": "Bearer abc123"}`
37//! becomes the `Authorization: Bearer abc123` header. Keys without the `headers.` prefix
38//! are ignored for HTTP headers but may be used for other purposes.
39
40use std::collections::HashMap;
41
42/// Information about the namespace operation being executed.
43///
44/// This is passed to the [`DynamicContextProvider`] to allow it to make
45/// context decisions based on the operation.
46#[derive(Debug, Clone)]
47pub struct OperationInfo {
48 /// The operation name (e.g., "list_tables", "describe_table", "create_namespace")
49 pub operation: String,
50 /// The object ID for the operation (namespace or table identifier).
51 /// This is the delimited string form, e.g., "workspace$table_name".
52 pub object_id: String,
53}
54
55impl OperationInfo {
56 /// Create a new OperationInfo.
57 pub fn new(operation: impl Into<String>, object_id: impl Into<String>) -> Self {
58 Self {
59 operation: operation.into(),
60 object_id: object_id.into(),
61 }
62 }
63}
64
65/// Trait for providing dynamic request context.
66///
67/// Implementations can generate per-request context (e.g., authentication headers)
68/// based on the operation being performed. The provider is called synchronously
69/// before each namespace operation.
70///
71/// For RestNamespace, context keys that start with `headers.` are converted to
72/// HTTP headers by stripping the prefix. For example, `{"headers.Authorization": "Bearer token"}`
73/// becomes the `Authorization: Bearer token` header.
74///
75/// ## Thread Safety
76///
77/// Implementations must be `Send + Sync` as the provider may be called from
78/// multiple threads concurrently.
79///
80/// ## Error Handling
81///
82/// If the provider needs to signal an error, it should return an empty HashMap
83/// and log the error. The namespace operation will proceed without the
84/// additional context.
85pub trait DynamicContextProvider: Send + Sync + std::fmt::Debug {
86 /// Provide context for a namespace operation.
87 ///
88 /// # Arguments
89 ///
90 /// * `info` - Information about the operation being performed
91 ///
92 /// # Returns
93 ///
94 /// Returns a HashMap of context key-value pairs. For HTTP headers, use keys
95 /// with the `headers.` prefix (e.g., `headers.Authorization`).
96 /// Returns an empty HashMap if no additional context is needed.
97 fn provide_context(&self, info: &OperationInfo) -> HashMap<String, String>;
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[derive(Debug)]
105 struct MockContextProvider {
106 prefix: String,
107 }
108
109 impl DynamicContextProvider for MockContextProvider {
110 fn provide_context(&self, info: &OperationInfo) -> HashMap<String, String> {
111 let mut context = HashMap::new();
112 context.insert(
113 "test-header".to_string(),
114 format!("{}-{}", self.prefix, info.operation),
115 );
116 context.insert("object-id".to_string(), info.object_id.clone());
117 context
118 }
119 }
120
121 #[test]
122 fn test_operation_info_creation() {
123 let info = OperationInfo::new("describe_table", "workspace$my_table");
124 assert_eq!(info.operation, "describe_table");
125 assert_eq!(info.object_id, "workspace$my_table");
126 }
127
128 #[test]
129 fn test_context_provider_basic() {
130 let provider = MockContextProvider {
131 prefix: "test".to_string(),
132 };
133
134 let info = OperationInfo::new("list_tables", "workspace$ns");
135
136 let context = provider.provide_context(&info);
137 assert_eq!(
138 context.get("test-header"),
139 Some(&"test-list_tables".to_string())
140 );
141 assert_eq!(context.get("object-id"), Some(&"workspace$ns".to_string()));
142 }
143
144 #[test]
145 fn test_empty_context() {
146 #[derive(Debug)]
147 struct EmptyProvider;
148
149 impl DynamicContextProvider for EmptyProvider {
150 fn provide_context(&self, _info: &OperationInfo) -> HashMap<String, String> {
151 HashMap::new()
152 }
153 }
154
155 let provider = EmptyProvider;
156 let info = OperationInfo::new("list_tables", "ns");
157
158 let context = provider.provide_context(&info);
159 assert!(context.is_empty());
160 }
161}