a2a_protocol_server/
request_context.rs1use a2a_protocol_types::message::Message;
13use a2a_protocol_types::task::{Task, TaskId};
14use tokio_util::sync::CancellationToken;
15
16#[derive(Debug, Clone)]
24pub struct RequestContext {
25 pub message: Message,
27
28 pub task_id: TaskId,
30
31 pub context_id: String,
33
34 pub stored_task: Option<Task>,
36
37 pub metadata: Option<serde_json::Value>,
39
40 pub cancellation_token: CancellationToken,
45}
46
47impl RequestContext {
48 #[must_use]
50 pub fn new(message: Message, task_id: TaskId, context_id: String) -> Self {
51 Self {
52 message,
53 task_id,
54 context_id,
55 stored_task: None,
56 metadata: None,
57 cancellation_token: CancellationToken::new(),
58 }
59 }
60
61 #[must_use]
63 pub fn with_stored_task(mut self, task: Task) -> Self {
64 self.stored_task = Some(task);
65 self
66 }
67
68 #[must_use]
70 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
71 self.metadata = Some(metadata);
72 self
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use a2a_protocol_types::message::{MessageId, MessageRole, Part};
80 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
81
82 fn make_message(text: &str) -> Message {
84 Message {
85 id: MessageId::new("msg-1"),
86 role: MessageRole::User,
87 parts: vec![Part::text(text)],
88 task_id: None,
89 context_id: None,
90 reference_task_ids: None,
91 extensions: None,
92 metadata: None,
93 }
94 }
95
96 fn make_task() -> Task {
98 Task {
99 id: TaskId::new("task-1"),
100 context_id: ContextId::new("ctx-1"),
101 status: TaskStatus::new(TaskState::Submitted),
102 history: None,
103 artifacts: None,
104 metadata: None,
105 }
106 }
107
108 #[test]
111 fn new_sets_required_fields() {
112 let msg = make_message("hello");
113 let ctx = RequestContext::new(msg.clone(), TaskId::new("t-1"), "ctx-1".to_owned());
114
115 assert_eq!(ctx.message, msg, "message should match the input");
116 assert_eq!(ctx.task_id, TaskId::new("t-1"), "task_id should match");
117 assert_eq!(ctx.context_id, "ctx-1", "context_id should match");
118 }
119
120 #[test]
121 fn new_defaults_optional_fields_to_none() {
122 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-2"), "ctx-2".to_owned());
123
124 assert!(
125 ctx.stored_task.is_none(),
126 "stored_task should default to None"
127 );
128 assert!(ctx.metadata.is_none(), "metadata should default to None");
129 }
130
131 #[test]
132 fn new_provides_uncancelled_token() {
133 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-3"), "ctx-3".to_owned());
134 assert!(
135 !ctx.cancellation_token.is_cancelled(),
136 "fresh token should not be cancelled"
137 );
138 }
139
140 #[test]
143 fn with_stored_task_sets_task() {
144 let task = make_task();
145 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-4"), "ctx-4".to_owned())
146 .with_stored_task(task);
147
148 assert_eq!(
149 ctx.stored_task.as_ref().map(|t| &t.id),
150 Some(&TaskId::new("task-1")),
151 "stored_task should contain the provided task"
152 );
153 }
154
155 #[test]
156 fn with_stored_task_preserves_other_fields() {
157 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-5"), "ctx-5".to_owned())
158 .with_stored_task(make_task());
159
160 assert_eq!(
161 ctx.task_id,
162 TaskId::new("t-5"),
163 "task_id should be unchanged"
164 );
165 assert_eq!(ctx.context_id, "ctx-5", "context_id should be unchanged");
166 }
167
168 #[test]
171 fn with_metadata_sets_value() {
172 let meta = serde_json::json!({"key": "value", "num": 42});
173 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-6"), "ctx-6".to_owned())
174 .with_metadata(meta.clone());
175
176 assert_eq!(
177 ctx.metadata.as_ref(),
178 Some(&meta),
179 "metadata should match the provided value"
180 );
181 }
182
183 #[test]
186 fn builder_methods_can_be_chained() {
187 let task = make_task();
188 let meta = serde_json::json!({"chained": true});
189 let ctx = RequestContext::new(
190 make_message("chain"),
191 TaskId::new("t-7"),
192 "ctx-7".to_owned(),
193 )
194 .with_stored_task(task)
195 .with_metadata(meta.clone());
196
197 assert!(
198 ctx.stored_task.is_some(),
199 "stored_task should be set after chaining"
200 );
201 assert_eq!(
202 ctx.metadata,
203 Some(meta),
204 "metadata should be set after chaining"
205 );
206 }
207
208 #[test]
211 fn request_context_is_cloneable() {
212 let ctx = RequestContext::new(
213 make_message("clone me"),
214 TaskId::new("t-8"),
215 "ctx-8".to_owned(),
216 );
217 let cloned = ctx.clone();
218 assert_eq!(
219 cloned.task_id, ctx.task_id,
220 "cloned context should have same task_id"
221 );
222 }
223
224 #[test]
225 fn request_context_is_debug() {
226 let ctx = RequestContext::new(
227 make_message("debug"),
228 TaskId::new("t-9"),
229 "ctx-9".to_owned(),
230 );
231 let debug_str = format!("{ctx:?}");
232 assert!(
233 debug_str.contains("RequestContext"),
234 "Debug output should contain the struct name"
235 );
236 }
237}