nika_engine/runtime/builtin/
emit.rs1use super::BuiltinTool;
23use crate::error::NikaError;
24use serde::{Deserialize, Serialize};
25use serde_json::Value;
26use std::future::Future;
27use std::pin::Pin;
28
29#[derive(Debug, Clone, Deserialize)]
32struct EmitParams {
33 name: String,
35 #[serde(default)]
37 payload_json: Option<String>,
38 #[serde(default)]
40 payload: Option<Value>,
41}
42
43impl EmitParams {
44 fn get_payload(&self) -> Result<Value, NikaError> {
46 if let Some(ref json_str) = self.payload_json {
47 serde_json::from_str(json_str).map_err(|e| NikaError::BuiltinInvalidParams {
48 tool: "nika_emit".into(),
49 reason: format!("Invalid payload_json: {}", e),
50 })
51 } else if let Some(ref value) = self.payload {
52 Ok(value.clone())
53 } else {
54 Ok(Value::Null)
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize)]
61struct EmitResponse {
62 emitted: bool,
64 name: String,
66 payload: Value,
68}
69
70pub struct EmitTool;
75
76impl BuiltinTool for EmitTool {
77 fn name(&self) -> &'static str {
78 "emit"
79 }
80
81 fn description(&self) -> &'static str {
82 "Emit custom event to EventLog for workflow observability"
83 }
84
85 fn parameters_schema(&self) -> serde_json::Value {
86 serde_json::json!({
90 "type": "object",
91 "properties": {
92 "name": {
93 "type": "string",
94 "description": "Event name/type identifier"
95 },
96 "payload_json": {
97 "type": "string",
98 "description": "Event payload as JSON string (e.g., '{\"key\": \"value\"}' or '123' or '\"text\"')"
99 }
100 },
101 "required": ["name", "payload_json"],
102 "additionalProperties": false
103 })
104 }
105
106 fn call<'a>(
107 &'a self,
108 args: String,
109 ) -> Pin<Box<dyn Future<Output = Result<String, NikaError>> + Send + 'a>> {
110 Box::pin(async move {
111 let params: EmitParams =
113 serde_json::from_str(&args).map_err(|e| NikaError::BuiltinInvalidParams {
114 tool: "nika:emit".into(),
115 reason: format!("Invalid JSON parameters: {}", e),
116 })?;
117
118 if params.name.is_empty() {
120 return Err(NikaError::BuiltinInvalidParams {
121 tool: "nika:emit".into(),
122 reason: "Event name cannot be empty".into(),
123 });
124 }
125
126 let payload = params.get_payload()?;
128
129 tracing::debug!(
134 target: "nika_emit",
135 name = %params.name,
136 "Custom event emitted"
137 );
138
139 let response = EmitResponse {
141 emitted: true,
142 name: params.name,
143 payload,
144 };
145
146 serde_json::to_string(&response).map_err(|e| NikaError::BuiltinToolError {
147 tool: "nika:emit".into(),
148 reason: format!("Failed to serialize response: {}", e),
149 })
150 })
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_emit_tool_name() {
160 let tool = EmitTool;
161 assert_eq!(tool.name(), "emit");
162 }
163
164 #[test]
165 fn test_emit_tool_description() {
166 let tool = EmitTool;
167 assert!(tool.description().contains("EventLog"));
168 }
169
170 #[test]
171 fn test_emit_tool_schema() {
172 let tool = EmitTool;
173 let schema = tool.parameters_schema();
174 assert_eq!(schema["type"], "object");
175 assert!(schema["properties"]["name"].is_object());
176 assert!(schema["properties"]["payload_json"].is_object());
178 assert_eq!(schema["additionalProperties"], false);
179 assert!(schema["required"]
180 .as_array()
181 .unwrap()
182 .contains(&serde_json::json!("name")));
183 }
184
185 #[tokio::test]
186 async fn test_emit_simple_event() {
187 let tool = EmitTool;
188 let result = tool.call(r#"{"name": "test_event"}"#.to_string()).await;
189
190 assert!(result.is_ok());
191 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
192 assert_eq!(response["emitted"], true);
193 assert_eq!(response["name"], "test_event");
194 assert_eq!(response["payload"], serde_json::json!(null));
195 }
196
197 #[tokio::test]
198 async fn test_emit_with_payload() {
199 let tool = EmitTool;
200 let result = tool
201 .call(
202 r#"{"name": "user_action", "payload": {"action": "click", "target": "button"}}"#
203 .to_string(),
204 )
205 .await;
206
207 assert!(result.is_ok());
208 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
209 assert_eq!(response["emitted"], true);
210 assert_eq!(response["name"], "user_action");
211 assert_eq!(response["payload"]["action"], "click");
212 assert_eq!(response["payload"]["target"], "button");
213 }
214
215 #[tokio::test]
216 async fn test_emit_with_complex_payload() {
217 let tool = EmitTool;
218 let result = tool
219 .call(
220 r#"{
221 "name": "metrics",
222 "payload": {
223 "count": 42,
224 "values": [1, 2, 3],
225 "nested": {"key": "value"}
226 }
227 }"#
228 .to_string(),
229 )
230 .await;
231
232 assert!(result.is_ok());
233 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
234 assert_eq!(response["payload"]["count"], 42);
235 assert_eq!(response["payload"]["values"][1], 2);
236 assert_eq!(response["payload"]["nested"]["key"], "value");
237 }
238
239 #[tokio::test]
240 async fn test_emit_empty_name() {
241 let tool = EmitTool;
242 let result = tool.call(r#"{"name": ""}"#.to_string()).await;
243
244 assert!(result.is_err());
245 let err = result.unwrap_err();
246 assert!(err.to_string().contains("cannot be empty"));
247 }
248
249 #[tokio::test]
250 async fn test_emit_missing_name() {
251 let tool = EmitTool;
252 let result = tool.call(r#"{"payload": {"test": 1}}"#.to_string()).await;
253
254 assert!(result.is_err());
255 let err = result.unwrap_err();
256 assert!(err.to_string().contains("Invalid JSON parameters"));
257 }
258
259 #[tokio::test]
260 async fn test_emit_invalid_json() {
261 let tool = EmitTool;
262 let result = tool.call("not json".to_string()).await;
263
264 assert!(result.is_err());
265 let err = result.unwrap_err();
266 assert!(err.to_string().contains("Invalid JSON parameters"));
267 }
268
269 #[tokio::test]
270 async fn test_emit_null_payload() {
271 let tool = EmitTool;
272 let result = tool
273 .call(r#"{"name": "event", "payload": null}"#.to_string())
274 .await;
275
276 assert!(result.is_ok());
277 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
278 assert_eq!(response["payload"], serde_json::json!(null));
279 }
280
281 #[tokio::test]
282 async fn test_emit_array_payload() {
283 let tool = EmitTool;
284 let result = tool
285 .call(r#"{"name": "items", "payload": [1, 2, 3]}"#.to_string())
286 .await;
287
288 assert!(result.is_ok());
289 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
290 assert_eq!(response["payload"][0], 1);
291 assert_eq!(response["payload"][2], 3);
292 }
293
294 #[tokio::test]
295 async fn test_emit_string_payload() {
296 let tool = EmitTool;
297 let result = tool
298 .call(r#"{"name": "message", "payload": "hello world"}"#.to_string())
299 .await;
300
301 assert!(result.is_ok());
302 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
303 assert_eq!(response["payload"], "hello world");
304 }
305}