1use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use crate::generated::api_types::CanvasAction;
15
16pub type CanvasJsonSchema = serde_json::Map<String, Value>;
25
26#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37#[non_exhaustive]
38pub struct CanvasDeclaration {
39 pub id: String,
41 pub display_name: String,
43 pub description: String,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub input_schema: Option<Value>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub actions: Option<Vec<CanvasAction>>,
51}
52
53impl CanvasDeclaration {
54 pub fn new(
56 id: impl Into<String>,
57 display_name: impl Into<String>,
58 description: impl Into<String>,
59 ) -> Self {
60 Self {
61 id: id.into(),
62 display_name: display_name.into(),
63 description: description.into(),
64 input_schema: None,
65 actions: None,
66 }
67 }
68
69 pub fn with_description(mut self, description: impl Into<String>) -> Self {
71 self.description = description.into();
72 self
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85#[serde(rename_all = "camelCase")]
86pub struct CanvasError {
87 pub code: String,
89 pub message: String,
91}
92
93impl std::fmt::Display for CanvasError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 write!(f, "{}: {}", self.code, self.message)
96 }
97}
98
99impl std::error::Error for CanvasError {}
100
101impl CanvasError {
102 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
104 Self {
105 code: code.into(),
106 message: message.into(),
107 }
108 }
109
110 pub fn no_handler() -> Self {
112 Self::new(
113 "canvas_action_no_handler",
114 "No handler implemented for this canvas action",
115 )
116 }
117}
118
119pub type CanvasResult<T> = Result<T, CanvasError>;
128
129#[async_trait]
149pub trait CanvasHandler: Send + Sync {
150 async fn on_open(
152 &self,
153 ctx: crate::generated::api_types::CanvasProviderOpenRequest,
154 ) -> CanvasResult<crate::generated::api_types::CanvasProviderOpenResult>;
155
156 async fn on_action(
158 &self,
159 _ctx: crate::generated::api_types::CanvasProviderInvokeActionRequest,
160 ) -> CanvasResult<Value> {
161 Err(CanvasError::no_handler())
162 }
163
164 async fn on_close(
166 &self,
167 _ctx: crate::generated::api_types::CanvasProviderCloseRequest,
168 ) -> CanvasResult<()> {
169 Ok(())
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use serde_json::json;
176
177 use super::*;
178 use crate::generated::api_types::{
179 CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult,
180 };
181 use crate::types::SessionId;
182
183 struct EchoHandler;
184
185 #[async_trait]
186 impl CanvasHandler for EchoHandler {
187 async fn on_open(
188 &self,
189 ctx: CanvasProviderOpenRequest,
190 ) -> CanvasResult<CanvasProviderOpenResult> {
191 Ok(CanvasProviderOpenResult {
192 url: Some(format!("https://example.test/{}", ctx.canvas_id)),
193 title: Some("Echo".to_string()),
194 status: Some("ready".to_string()),
195 })
196 }
197
198 async fn on_action(&self, ctx: CanvasProviderInvokeActionRequest) -> CanvasResult<Value> {
199 Ok(json!({ "echoed": ctx.action_name, "input": ctx.input }))
200 }
201 }
202
203 #[test]
204 fn declaration_serializes_camel_case_and_skips_none() {
205 let decl = CanvasDeclaration {
206 id: "counter".to_string(),
207 display_name: "Counter".to_string(),
208 description: "Count things".to_string(),
209 input_schema: None,
210 actions: Some(vec![CanvasAction {
211 name: "increment".to_string(),
212 description: Some("bump".to_string()),
213 input_schema: None,
214 }]),
215 };
216
217 let value = serde_json::to_value(&decl).unwrap();
218
219 assert_eq!(value["id"], "counter");
220 assert_eq!(value["displayName"], "Counter");
221 assert_eq!(value["description"], "Count things");
222 assert_eq!(value["actions"][0]["name"], "increment");
223 }
224
225 #[tokio::test]
226 async fn handler_on_open_returns_response() {
227 let handler = EchoHandler;
228 let response = handler
229 .on_open(CanvasProviderOpenRequest {
230 session_id: SessionId::from("s1"),
231 extension_id: "project:echo".to_string(),
232 canvas_id: "echo".to_string(),
233 instance_id: "echo-1".to_string(),
234 input: Some(json!({ "x": 1 })),
235 host: None,
236 session: None,
237 })
238 .await
239 .unwrap();
240
241 assert_eq!(response.url.as_deref(), Some("https://example.test/echo"));
242 assert_eq!(response.title.as_deref(), Some("Echo"));
243 assert_eq!(response.status.as_deref(), Some("ready"));
244 }
245
246 #[tokio::test]
247 async fn handler_on_action_returns_value() {
248 let handler = EchoHandler;
249 let result = handler
250 .on_action(CanvasProviderInvokeActionRequest {
251 session_id: SessionId::from("s1"),
252 extension_id: "project:echo".to_string(),
253 canvas_id: "echo".to_string(),
254 instance_id: "inst-1".to_string(),
255 action_name: "shout".to_string(),
256 input: Some(json!("hi")),
257 host: None,
258 session: None,
259 })
260 .await
261 .unwrap();
262
263 assert_eq!(result["echoed"], "shout");
264 assert_eq!(result["input"], "hi");
265 }
266
267 #[tokio::test]
268 async fn default_on_action_returns_no_handler_error() {
269 struct OpenOnly;
270 #[async_trait]
271 impl CanvasHandler for OpenOnly {
272 async fn on_open(
273 &self,
274 _ctx: CanvasProviderOpenRequest,
275 ) -> CanvasResult<CanvasProviderOpenResult> {
276 Ok(CanvasProviderOpenResult {
277 url: None,
278 title: None,
279 status: None,
280 })
281 }
282 }
283
284 let err = OpenOnly
285 .on_action(CanvasProviderInvokeActionRequest {
286 session_id: SessionId::from("s1"),
287 extension_id: "project:open-only".to_string(),
288 canvas_id: "x".to_string(),
289 instance_id: "x-1".to_string(),
290 action_name: "anything".to_string(),
291 input: Some(Value::Null),
292 host: None,
293 session: None,
294 })
295 .await
296 .unwrap_err();
297
298 assert_eq!(err.code, "canvas_action_no_handler");
299 }
300}