protocol_droid/application/client/
handler.rs1use core::future::Future;
19use core::pin::Pin;
20use std::sync::Arc;
21
22use serde_json::{Map, Value};
23
24use crate::domain::CapabilitySet;
25use crate::domain::ir::JsonObject;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct HandlerRefusal {
33 pub code: i64,
35 pub message: String,
37}
38
39impl HandlerRefusal {
40 #[must_use]
42 pub fn unsupported(method: &str) -> Self {
43 Self {
44 code: -32601,
45 message: format!("this client does not handle `{method}`"),
46 }
47 }
48}
49
50pub trait ServerRequestHandler: Send + Sync {
73 fn handle<'a>(
75 &'a self,
76 method: &'a str,
77 params: &'a JsonObject,
78 ) -> Pin<Box<dyn Future<Output = Result<JsonObject, HandlerRefusal>> + Send + 'a>>;
79}
80
81#[derive(Clone, Default)]
83pub struct ConnectOptions {
84 pub(crate) capabilities: CapabilitySet,
85 pub(crate) handler: Option<Arc<dyn ServerRequestHandler>>,
86 pub(crate) auth: Option<Arc<dyn super::bearer::BearerAuth>>,
87}
88
89impl core::fmt::Debug for ConnectOptions {
90 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91 f.debug_struct("ConnectOptions")
92 .field("capabilities", &self.capabilities)
93 .field("handler", &self.handler.is_some())
94 .field("auth", &self.auth.is_some())
95 .finish()
96 }
97}
98
99impl ConnectOptions {
100 #[must_use]
102 pub fn new() -> Self {
103 Self::default()
104 }
105
106 #[must_use]
111 pub const fn declaring(mut self, capabilities: CapabilitySet) -> Self {
112 self.capabilities = capabilities;
113 self
114 }
115
116 #[must_use]
121 pub fn authorizing(mut self, auth: Arc<dyn super::bearer::BearerAuth>) -> Self {
122 self.auth = Some(auth);
123 self
124 }
125
126 #[must_use]
128 pub fn answering(mut self, handler: Arc<dyn ServerRequestHandler>) -> Self {
129 self.handler = Some(handler);
130 self
131 }
132}
133
134#[must_use]
142pub fn apply_elicitation_defaults(request: &JsonObject, answer: &JsonObject) -> JsonObject {
143 let Ok(Value::Object(mut result)) = serde_json::from_str::<Value>(answer.as_str()) else {
144 return answer.clone();
145 };
146 if result.get("action").and_then(Value::as_str) != Some("accept") {
147 return answer.clone();
148 }
149 let Ok(Value::Object(params)) = serde_json::from_str::<Value>(request.as_str()) else {
150 return answer.clone();
151 };
152 let Some(properties) = params
153 .get("requestedSchema")
154 .and_then(|schema| schema.get("properties"))
155 .and_then(Value::as_object)
156 else {
157 return answer.clone();
158 };
159 let content = result
160 .entry("content")
161 .or_insert_with(|| Value::Object(Map::new()));
162 if let Some(content) = content.as_object_mut() {
163 for (name, schema) in properties {
164 if !content.contains_key(name)
165 && let Some(default) = schema.get("default")
166 {
167 content.insert(name.clone(), default.clone());
168 }
169 }
170 }
171 JsonObject::from_value(Value::Object(result)).unwrap_or_else(|_| answer.clone())
172}
173
174#[cfg(test)]
175mod tests {
176 use super::apply_elicitation_defaults;
177 use crate::domain::ir::JsonObject;
178
179 fn object(text: &str) -> JsonObject {
180 JsonObject::new(text).unwrap_or_default()
181 }
182
183 #[test]
184 fn omitted_fields_take_their_default() {
185 let request = object(
186 r#"{"message":"m","requestedSchema":{"type":"object","properties":{"name":{"type":"string","default":"John"},"age":{"type":"integer","default":30},"plain":{"type":"string"}}}}"#,
187 );
188 let filled =
189 apply_elicitation_defaults(&request, &object(r#"{"action":"accept","content":{}}"#));
190 let parsed: serde_json::Value = serde_json::from_str(filled.as_str()).unwrap_or_default();
191 assert_eq!(
192 parsed.pointer("/content/name"),
193 Some(&serde_json::json!("John"))
194 );
195 assert_eq!(parsed.pointer("/content/age"), Some(&serde_json::json!(30)));
196 assert_eq!(
197 parsed.pointer("/content/plain"),
198 None,
199 "no default, nothing invented"
200 );
201 }
202
203 #[test]
204 fn a_value_the_user_gave_is_kept() {
205 let request = object(
206 r#"{"requestedSchema":{"properties":{"name":{"type":"string","default":"John"}}}}"#,
207 );
208 let filled = apply_elicitation_defaults(
209 &request,
210 &object(r#"{"action":"accept","content":{"name":"Ada"}}"#),
211 );
212 assert!(filled.as_str().contains("Ada") && !filled.as_str().contains("John"));
213 }
214
215 #[test]
216 fn a_decline_is_untouched() {
217 let request = object(
218 r#"{"requestedSchema":{"properties":{"name":{"type":"string","default":"John"}}}}"#,
219 );
220 let answer = object(r#"{"action":"decline"}"#);
221 assert_eq!(apply_elicitation_defaults(&request, &answer), answer);
222 }
223}