1use std::process::{Command, Stdio};
2
3use async_trait::async_trait;
4use imp_core::storage;
5use imp_core::tools::lua::{parameter_schema_from_lua, tool_output_from_lua_result};
6use imp_core::tools::{Tool, ToolContext, ToolOutput, ToolRegistry};
7use imp_core::ui::{ComponentSpec, SelectOption};
8use imp_core::Error as CoreError;
9use imp_llm::auth::AuthStore;
10use mlua::{Function, Lua, MultiValue, Table, Value};
11use serde_json::{json, Value as JsonValue};
12use std::sync::{Arc, Mutex};
13
14use crate::sandbox::{
15 LuaCallContext, LuaCommandHandle, LuaError, LuaHookHandle, LuaRuntime, LuaToolHandle,
16};
17
18pub struct LuaTool {
21 name: String,
22 label: String,
23 description: String,
24 readonly: bool,
25 params: serde_json::Value,
26 runtime: Arc<Mutex<LuaRuntime>>,
27 handle_index: usize,
28}
29
30#[async_trait]
31impl Tool for LuaTool {
32 fn name(&self) -> &str {
33 &self.name
34 }
35
36 fn label(&self) -> &str {
37 &self.label
38 }
39
40 fn description(&self) -> &str {
41 &self.description
42 }
43
44 fn parameters(&self) -> serde_json::Value {
45 parameter_schema_from_lua(&self.params)
46 }
47
48 fn is_readonly(&self) -> bool {
49 self.readonly
50 }
51
52 async fn execute(
53 &self,
54 call_id: &str,
55 params: serde_json::Value,
56 ctx: ToolContext,
57 ) -> imp_core::Result<ToolOutput> {
58 let runtime = Arc::clone(&self.runtime);
59 let handle_index = self.handle_index;
60 let call_id = call_id.to_string();
61 let ctx_json = json!({
62 "cwd": ctx.cwd.display().to_string(),
63 "cancelled": ctx.is_cancelled(),
64 });
65 let call_ctx = LuaCallContext::from(ctx);
66
67 tokio::task::spawn_blocking(move || {
68 let runtime_guard = runtime
69 .lock()
70 .map_err(|_| CoreError::Tool("Lua runtime lock poisoned".into()))?;
71
72 runtime_guard.set_call_context(call_ctx);
74
75 let result = (|| {
76 let tools = runtime_guard.tools();
77 let handles = tools
78 .lock()
79 .map_err(|_| CoreError::Tool("Lua tool registry lock poisoned".into()))?;
80 let handle = handles.get(handle_index).ok_or_else(|| {
81 CoreError::Tool(format!("Lua tool handle {handle_index} not found"))
82 })?;
83
84 let execute_fn: Function = runtime_guard
85 .lua()
86 .registry_value(&handle.execute_key)
87 .map_err(lua_tool_error)?;
88 let lua_params =
89 json_to_lua_value(runtime_guard.lua(), ¶ms).map_err(lua_tool_error)?;
90 let lua_ctx =
91 json_to_lua_value(runtime_guard.lua(), &ctx_json).map_err(lua_tool_error)?;
92 let result: Value = execute_fn
93 .call((call_id.as_str(), lua_params, lua_ctx))
94 .map_err(lua_tool_error)?;
95
96 tool_output_from_lua_result(lua_value_to_json(result))
97 })();
98
99 runtime_guard.clear_call_context();
100 result
101 })
102 .await
103 .map_err(|error| CoreError::Tool(format!("Lua tool task failed: {error}")))?
104 }
105}
106
107pub fn load_lua_tools(runtime: Arc<Mutex<LuaRuntime>>, registry: &mut ToolRegistry) {
109 let handles = {
110 let runtime_guard = runtime
111 .lock()
112 .expect("Lua runtime lock poisoned while loading tools");
113 let tools = runtime_guard.tools();
114 let handles = tools
115 .lock()
116 .expect("Lua tool registry lock poisoned while loading tools");
117
118 handles
119 .iter()
120 .enumerate()
121 .map(|(index, handle)| LuaTool {
122 name: handle.name.clone(),
123 label: handle.label.clone(),
124 description: handle.description.clone(),
125 readonly: handle.readonly,
126 params: handle.params.clone(),
127 runtime: Arc::clone(&runtime),
128 handle_index: index,
129 })
130 .collect::<Vec<_>>()
131 };
132
133 for tool in handles {
134 registry.register(Arc::new(tool));
135 }
136}
137
138fn lua_tool_error(error: mlua::Error) -> CoreError {
139 CoreError::Tool(format!("Lua tool error: {error}"))
140}
141
142fn extract_header_pairs(headers: Option<Table>) -> mlua::Result<Vec<(String, String)>> {
144 let mut pairs = Vec::new();
145 if let Some(tbl) = headers {
146 for pair in tbl.pairs::<String, String>() {
147 let (k, v) = pair?;
148 pairs.push((k, v));
149 }
150 }
151 Ok(pairs)
152}
153
154fn ui_unavailable_result(lua: &Lua) -> mlua::Result<Value> {
155 ui_error_result(lua, "unavailable")
156}
157
158fn ui_cancelled_result(lua: &Lua) -> mlua::Result<Value> {
159 ui_error_result(lua, "cancelled")
160}
161
162fn ui_invalid_result(lua: &Lua, message: impl AsRef<str>) -> mlua::Result<Value> {
163 let result = lua.create_table()?;
164 result.set("ok", false)?;
165 result.set("reason", "invalid")?;
166 result.set("message", message.as_ref())?;
167 Ok(Value::Table(result))
168}
169
170fn ui_error_result(lua: &Lua, reason: &str) -> mlua::Result<Value> {
171 let result = lua.create_table()?;
172 result.set("ok", false)?;
173 result.set("reason", reason)?;
174 Ok(Value::Table(result))
175}
176
177fn ui_ok_result(lua: &Lua, value: JsonValue) -> mlua::Result<Value> {
178 let result = lua.create_table()?;
179 result.set("ok", true)?;
180 result.set("value", json_to_lua_value(lua, &value)?)?;
181 Ok(Value::Table(result))
182}
183
184fn option_from_json(value: &JsonValue) -> Option<SelectOption> {
185 match value {
186 JsonValue::String(label) => Some(SelectOption {
187 label: label.clone(),
188 description: None,
189 }),
190 JsonValue::Object(object) => Some(SelectOption {
191 label: object.get("label")?.as_str()?.to_string(),
192 description: object
193 .get("description")
194 .and_then(JsonValue::as_str)
195 .map(str::to_string),
196 }),
197 _ => None,
198 }
199}
200
201fn options_from_spec(spec: &JsonValue) -> Result<Vec<SelectOption>, String> {
202 let values = spec
203 .get("options")
204 .and_then(JsonValue::as_array)
205 .ok_or_else(|| "ui request requires an options array".to_string())?;
206 values
207 .iter()
208 .map(|value| option_from_json(value).ok_or_else(|| "invalid ui option".to_string()))
209 .collect()
210}
211
212fn component_from_spec(spec: &JsonValue) -> Result<ComponentSpec, String> {
213 let component = spec.get("component").unwrap_or(spec);
214 serde_json::from_value(component.clone()).map_err(|error| error.to_string())
215}
216
217fn execute_ui_request(
218 lua: &Lua,
219 spec: JsonValue,
220 call_ctx: &Arc<Mutex<Option<LuaCallContext>>>,
221) -> mlua::Result<Value> {
222 let ctx = {
223 let ctx_guard = call_ctx
224 .lock()
225 .map_err(|_| mlua::Error::external("call context lock poisoned"))?;
226 match ctx_guard.as_ref() {
227 Some(ctx) => ctx.to_tool_context(),
228 None => return ui_unavailable_result(lua),
229 }
230 };
231
232 if !ctx.ui.has_ui() {
233 return ui_unavailable_result(lua);
234 }
235
236 let kind = spec
237 .get("kind")
238 .and_then(JsonValue::as_str)
239 .unwrap_or("custom");
240 let title = spec.get("title").and_then(JsonValue::as_str).unwrap_or("");
241 let message = spec
242 .get("message")
243 .or_else(|| spec.get("context"))
244 .and_then(JsonValue::as_str)
245 .unwrap_or("");
246
247 let handle = tokio::runtime::Handle::try_current()
248 .map_err(|_| mlua::Error::external("imp.ui requires a tokio runtime"))?;
249
250 match kind {
251 "confirm" => match handle.block_on(ctx.ui.confirm(title, message)) {
252 Some(value) => ui_ok_result(lua, JsonValue::Bool(value)),
253 None => ui_cancelled_result(lua),
254 },
255 "select" => {
256 let options = match options_from_spec(&spec) {
257 Ok(options) => options,
258 Err(message) => return ui_invalid_result(lua, message),
259 };
260 match handle.block_on(ctx.ui.select_with_context(title, message, &options)) {
261 Some(index) => ui_ok_result(
262 lua,
263 json!({
264 "index": index + 1,
265 "label": options.get(index).map(|option| option.label.clone()),
266 }),
267 ),
268 None => ui_cancelled_result(lua),
269 }
270 }
271 "multi_select" | "multi-select" => {
272 let options = match options_from_spec(&spec) {
273 Ok(options) => options,
274 Err(message) => return ui_invalid_result(lua, message),
275 };
276 match handle.block_on(ctx.ui.multi_select_with_context(title, message, &options)) {
277 Some(indices) => {
278 let selected: Vec<JsonValue> = indices
279 .into_iter()
280 .map(|index| {
281 json!({
282 "index": index + 1,
283 "label": options.get(index).map(|option| option.label.clone()),
284 })
285 })
286 .collect();
287 ui_ok_result(lua, JsonValue::Array(selected))
288 }
289 None => ui_cancelled_result(lua),
290 }
291 }
292 "input" => {
293 let placeholder = spec
294 .get("placeholder")
295 .and_then(JsonValue::as_str)
296 .or_else(|| spec.get("default").and_then(JsonValue::as_str))
297 .unwrap_or("");
298 match handle.block_on(ctx.ui.input_with_context(title, message, placeholder)) {
299 Some(value) => ui_ok_result(lua, JsonValue::String(value)),
300 None => ui_cancelled_result(lua),
301 }
302 }
303 "custom" => {
304 let component = match component_from_spec(&spec) {
305 Ok(component) => component,
306 Err(message) => return ui_invalid_result(lua, message),
307 };
308 match handle.block_on(ctx.ui.custom(component)) {
309 Some(value) => ui_ok_result(lua, value),
310 None => ui_cancelled_result(lua),
311 }
312 }
313 other => ui_invalid_result(lua, format!("unknown ui request kind '{other}'")),
314 }
315}
316
317pub fn setup_host_api(runtime: &LuaRuntime) -> Result<(), LuaError> {
334 let lua = runtime.lua();
335
336 let imp = lua.create_table()?;
337
338 let hooks = runtime.hooks();
340 let on_fn = lua.create_function(move |lua_inner, (event, handler): (String, Function)| {
341 let key = lua_inner.create_registry_value(handler)?;
342 let handle = LuaHookHandle {
343 event,
344 handler_key: key,
345 };
346 hooks.lock().unwrap().push(handle);
347 Ok(())
348 })?;
349 imp.set("on", on_fn)?;
350
351 let tools = runtime.tools();
353 let register_tool_fn = lua.create_function(move |lua_inner, def: Table| {
354 let name: String = def.get("name")?;
355 let label: String = def
356 .get::<Option<String>>("label")?
357 .unwrap_or_else(|| name.clone());
358 let description: String = def
359 .get::<Option<String>>("description")?
360 .unwrap_or_default();
361 let readonly: bool = def.get::<Option<bool>>("readonly")?.unwrap_or(false);
362
363 let params_val: Value = def.get("params")?;
364 let params = lua_value_to_json(params_val);
365
366 let execute_fn: Function = def.get("execute")?;
367 let key = lua_inner.create_registry_value(execute_fn)?;
368
369 let handle = LuaToolHandle {
370 name,
371 label,
372 description,
373 readonly,
374 params,
375 execute_key: key,
376 };
377 tools.lock().unwrap().push(handle);
378 Ok(())
379 })?;
380 imp.set("register_tool", register_tool_fn)?;
381
382 let allow_shell_exec = runtime.allow_shell_exec();
384 let exec_fn = lua.create_function(
385 move |lua_inner, (cmd, args, opts): (String, Option<Table>, Option<Table>)| {
386 if !allow_shell_exec.load(std::sync::atomic::Ordering::Relaxed) {
387 return Err(mlua::Error::external(
388 "imp.exec() is disabled for this runtime",
389 ));
390 }
391 let output = if let Some(args_table) = args {
392 let mut command = Command::new(&cmd);
393 for pair in args_table.sequence_values::<String>() {
394 command.arg(pair?);
395 }
396
397 if let Some(opts_table) = &opts {
398 if let Ok(Some(cwd)) = opts_table.get::<Option<String>>("cwd") {
399 command.current_dir(cwd);
400 }
401 if let Ok(Some(env_table)) = opts_table.get::<Option<Table>>("env") {
402 for pair in env_table.pairs::<String, String>() {
403 let (name, value) = pair?;
404 command.env(name, value);
405 }
406 }
407 }
408
409 command.stdin(Stdio::null()).output()
410 } else {
411 let mut command = Command::new("sh");
412 command.arg("-c").arg(&cmd);
413
414 if let Some(opts_table) = &opts {
415 if let Ok(Some(cwd)) = opts_table.get::<Option<String>>("cwd") {
416 command.current_dir(cwd);
417 }
418 if let Ok(Some(env_table)) = opts_table.get::<Option<Table>>("env") {
419 for pair in env_table.pairs::<String, String>() {
420 let (name, value) = pair?;
421 command.env(name, value);
422 }
423 }
424 }
425
426 command.stdin(Stdio::null()).output()
427 }
428 .map_err(mlua::Error::external)?;
429
430 let result = lua_inner.create_table()?;
431 result.set(
432 "stdout",
433 String::from_utf8_lossy(&output.stdout).to_string(),
434 )?;
435 result.set(
436 "stderr",
437 String::from_utf8_lossy(&output.stderr).to_string(),
438 )?;
439 result.set("exit_code", output.status.code().unwrap_or(-1))?;
440
441 Ok(result)
442 },
443 )?;
444 imp.set("exec", exec_fn)?;
445
446 let commands = runtime.commands();
448 let register_command_fn =
449 lua.create_function(move |lua_inner, (name, def): (String, Table)| {
450 let description: String = def
451 .get::<Option<String>>("description")?
452 .unwrap_or_default();
453 let handler: Function = def.get("handler")?;
454 let key = lua_inner.create_registry_value(handler)?;
455
456 let handle = LuaCommandHandle {
457 name,
458 description,
459 handler_key: key,
460 };
461 commands.lock().unwrap().push(handle);
462 Ok(())
463 })?;
464 imp.set("register_command", register_command_fn)?;
465
466 let events = lua.create_table()?;
468
469 let handlers_table = lua.create_table()?;
471 lua.set_named_registry_value("__imp_event_handlers", handlers_table)?;
472
473 let events_on = lua.create_function(|lua_inner, (name, handler): (String, Function)| {
474 let handlers: Table = lua_inner.named_registry_value("__imp_event_handlers")?;
475 let list: Table = match handlers.get::<Option<Table>>(name.as_str())? {
476 Some(t) => t,
477 None => {
478 let t = lua_inner.create_table()?;
479 handlers.set(name.as_str(), t.clone())?;
480 t
481 }
482 };
483 let len = list.raw_len();
484 list.set(len + 1, handler)?;
485 Ok(())
486 })?;
487 events.set("on", events_on)?;
488
489 let events_emit = lua.create_function(|lua_inner, (name, data): (String, Value)| {
490 let handlers: Table = lua_inner.named_registry_value("__imp_event_handlers")?;
491 if let Some(list) = handlers.get::<Option<Table>>(name.as_str())? {
492 for pair in list.sequence_values::<Function>() {
493 let handler = pair?;
494 let _ = handler.call::<()>(data.clone());
497 }
498 }
499 Ok(())
500 })?;
501 events.set("emit", events_emit)?;
502
503 imp.set("events", events)?;
504
505 let native_tools = runtime.native_tools();
507 let tool_call_ctx = runtime.call_context();
508 let allow_native_tool_calls = runtime.allow_native_tool_calls();
509 let imp_tool_fn = lua.create_function(
510 move |lua_inner, (name, params): (String, Value)| -> mlua::Result<MultiValue> {
511 if !allow_native_tool_calls.load(std::sync::atomic::Ordering::Relaxed) {
512 return Err(mlua::Error::external(
513 "imp.tool() is disabled for this runtime",
514 ));
515 }
516
517 let tool = {
519 let tools_guard = native_tools
520 .lock()
521 .map_err(|_| mlua::Error::external("native tools lock poisoned"))?;
522 tools_guard
523 .get(&name)
524 .cloned()
525 .ok_or_else(|| mlua::Error::external(format!("tool '{name}' not found")))?
526 };
527
528 let ctx = {
530 let ctx_guard = tool_call_ctx
531 .lock()
532 .map_err(|_| mlua::Error::external("call context lock poisoned"))?;
533 ctx_guard
534 .as_ref()
535 .ok_or_else(|| {
536 mlua::Error::external("imp.tool() called outside of tool execution context")
537 })?
538 .to_tool_context()
539 };
540
541 let params_json = lua_value_to_json(params);
542
543 let handle = tokio::runtime::Handle::try_current()
545 .map_err(|_| mlua::Error::external("imp.tool() requires a tokio runtime"))?;
546
547 let output = handle
548 .block_on(tool.execute("lua-call", params_json, ctx))
549 .map_err(|e| mlua::Error::external(format!("tool error: {e}")))?;
550
551 let mut mv = MultiValue::new();
553 if output.is_error {
554 let err_text = output
555 .text_content()
556 .unwrap_or("tool execution failed")
557 .to_string();
558 mv.push_back(Value::Nil);
559 mv.push_back(Value::String(lua_inner.create_string(&err_text)?));
560 } else if let Some(text) = output.text_content() {
561 mv.push_back(Value::String(lua_inner.create_string(text)?));
562 } else {
563 mv.push_back(Value::Nil);
564 }
565 Ok(mv)
566 },
567 )?;
568 imp.set("tool", imp_tool_fn)?;
569
570 let ui = lua.create_table()?;
572 let ui_call_ctx = runtime.call_context();
573 let ui_request_fn = lua.create_function(move |lua_inner, spec: Value| {
574 execute_ui_request(lua_inner, lua_value_to_json(spec), &ui_call_ctx)
575 })?;
576 ui.set("request", ui_request_fn)?;
577
578 let confirm_call_ctx = runtime.call_context();
579 let ui_confirm_fn = lua.create_function(
580 move |lua_inner, (title, message): (String, Option<String>)| -> mlua::Result<Value> {
581 let result = execute_ui_request(
582 lua_inner,
583 json!({
584 "kind": "confirm",
585 "title": title,
586 "message": message.unwrap_or_default(),
587 }),
588 &confirm_call_ctx,
589 )?;
590 let json = lua_value_to_json(result);
591 if json.get("ok").and_then(JsonValue::as_bool) == Some(true) {
592 Ok(match json.get("value").and_then(JsonValue::as_bool) {
593 Some(value) => Value::Boolean(value),
594 None => Value::Nil,
595 })
596 } else {
597 Ok(Value::Nil)
598 }
599 },
600 )?;
601 ui.set("confirm", ui_confirm_fn)?;
602
603 imp.set("ui", ui)?;
604
605 let update_call_ctx = runtime.call_context();
607 let imp_update_fn = lua.create_function(move |_lua, text: String| {
608 let ctx_guard = update_call_ctx
609 .lock()
610 .map_err(|_| mlua::Error::external("call context lock poisoned"))?;
611 if let Some(ref ctx) = *ctx_guard {
612 let _ = ctx.update_tx.try_send(imp_core::tools::ToolUpdate {
613 content: vec![imp_core::imp_llm::ContentBlock::Text { text }],
614 details: serde_json::Value::Null,
615 });
616 }
617 Ok(())
618 })?;
619 imp.set("update", imp_update_fn)?;
620
621 let allow_secrets = runtime.allow_secrets();
623 let secret_fn = lua.create_function(
624 move |lua_inner, (provider, field): (String, Option<String>)| -> mlua::Result<Value> {
625 if !allow_secrets.load(std::sync::atomic::Ordering::Relaxed) {
626 return Err(mlua::Error::external(
627 "imp.secret() is disabled for this runtime",
628 ));
629 }
630 let auth_path =
631 storage::existing_global_auth_path().unwrap_or_else(storage::global_auth_path);
632 let auth_store =
633 AuthStore::load(&auth_path).unwrap_or_else(|_| AuthStore::new(auth_path.clone()));
634 let field = field.unwrap_or_else(|| "api_key".to_string());
635 match auth_store.resolve_secret_field(&provider, &field) {
636 Ok(value) => Ok(Value::String(lua_inner.create_string(&value)?)),
637 Err(error) => Err(mlua::Error::external(error.to_string())),
638 }
639 },
640 )?;
641 imp.set("secret", secret_fn)?;
642
643 let allow_secrets = runtime.allow_secrets();
645 let secret_fields_fn =
646 lua.create_function(move |lua_inner, provider: String| -> mlua::Result<Value> {
647 if !allow_secrets.load(std::sync::atomic::Ordering::Relaxed) {
648 return Err(mlua::Error::external(
649 "imp.secret_fields() is disabled for this runtime",
650 ));
651 }
652 let auth_path =
653 storage::existing_global_auth_path().unwrap_or_else(storage::global_auth_path);
654 let auth_store =
655 AuthStore::load(&auth_path).unwrap_or_else(|_| AuthStore::new(auth_path.clone()));
656 match auth_store.resolve_secret_fields(&provider) {
657 Ok(fields) => {
658 let table = lua_inner.create_table()?;
659 for (field, value) in fields {
660 table.set(field, value)?;
661 }
662 Ok(Value::Table(table))
663 }
664 Err(error) => Err(mlua::Error::external(error.to_string())),
665 }
666 })?;
667 imp.set("secret_fields", secret_fields_fn)?;
668
669 let allowed_env = runtime.allowed_env();
671 let env_fn = lua.create_function(move |lua_inner, name: String| {
672 let allowed = allowed_env
673 .lock()
674 .map_err(|_| mlua::Error::external("allowed_env lock poisoned"))?;
675 if !allowed.contains(&name) {
677 return Ok(Value::Nil);
678 }
679 match std::env::var(&name) {
680 Ok(val) => Ok(Value::String(lua_inner.create_string(&val)?)),
681 Err(_) => Ok(Value::Nil),
682 }
683 })?;
684 imp.set("env", env_fn)?;
685
686 let http = lua.create_table()?;
688 let allow_http = runtime.allow_http();
689
690 let http_get_fn =
691 lua.create_function(move |lua_inner, (url, headers): (String, Option<Table>)| {
692 if !allow_http.load(std::sync::atomic::Ordering::Relaxed) {
693 return Err(mlua::Error::external(
694 "imp.http.get() is disabled for this runtime",
695 ));
696 }
697 let header_pairs = extract_header_pairs(headers)?;
698
699 let handle = tokio::runtime::Handle::try_current()
700 .map_err(|_| mlua::Error::external("imp.http requires a tokio runtime"))?;
701
702 let (status, body) = handle
703 .block_on(async {
704 let client = reqwest::Client::new();
705 let mut builder = client.get(&url);
706 for (k, v) in &header_pairs {
707 builder = builder.header(k.as_str(), v.as_str());
708 }
709 let resp = builder.send().await.map_err(|e| e.to_string())?;
710 let status = resp.status().as_u16();
711 let body = resp.text().await.map_err(|e| e.to_string())?;
712 Ok::<_, String>((status, body))
713 })
714 .map_err(mlua::Error::external)?;
715
716 let result = lua_inner.create_table()?;
717 result.set("status", status)?;
718 result.set("body", body)?;
719 Ok(result)
720 })?;
721 http.set("get", http_get_fn)?;
722
723 let allow_http = runtime.allow_http();
724 let http_post_fn = lua.create_function(
725 move |lua_inner, (url, body, headers): (String, String, Option<Table>)| {
726 if !allow_http.load(std::sync::atomic::Ordering::Relaxed) {
727 return Err(mlua::Error::external(
728 "imp.http.post() is disabled for this runtime",
729 ));
730 }
731 let header_pairs = extract_header_pairs(headers)?;
732
733 let handle = tokio::runtime::Handle::try_current()
734 .map_err(|_| mlua::Error::external("imp.http requires a tokio runtime"))?;
735
736 let (status, resp_body) = handle
737 .block_on(async {
738 let client = reqwest::Client::new();
739 let mut builder = client.post(&url).body(body);
740 for (k, v) in &header_pairs {
741 builder = builder.header(k.as_str(), v.as_str());
742 }
743 let resp = builder.send().await.map_err(|e| e.to_string())?;
744 let status = resp.status().as_u16();
745 let resp_body = resp.text().await.map_err(|e| e.to_string())?;
746 Ok::<_, String>((status, resp_body))
747 })
748 .map_err(mlua::Error::external)?;
749
750 let result = lua_inner.create_table()?;
751 result.set("status", status)?;
752 result.set("body", resp_body)?;
753 Ok(result)
754 },
755 )?;
756 http.set("post", http_post_fn)?;
757
758 imp.set("http", http)?;
759
760 lua.globals().set("imp", imp)?;
762
763 Ok(())
764}
765
766pub fn lua_value_to_json(value: Value) -> serde_json::Value {
768 match value {
769 Value::Nil => serde_json::Value::Null,
770 Value::Boolean(b) => serde_json::Value::Bool(b),
771 Value::Integer(i) => serde_json::Value::Number(serde_json::Number::from(i)),
772 Value::Number(n) => serde_json::Number::from_f64(n)
773 .map(serde_json::Value::Number)
774 .unwrap_or(serde_json::Value::Null),
775 Value::String(s) => {
776 serde_json::Value::String(s.to_str().map(|s| s.to_string()).unwrap_or_default())
777 }
778 Value::Table(t) => {
779 let len = t.raw_len();
781 if len > 0 {
782 let is_array = (1..=len).all(|i| {
784 t.get::<Value>(i)
785 .ok()
786 .map(|v| !matches!(v, Value::Nil))
787 .unwrap_or(false)
788 });
789 if is_array {
790 let arr: Vec<serde_json::Value> = (1..=len)
791 .filter_map(|i| t.get::<Value>(i).ok().map(lua_value_to_json))
792 .collect();
793 return serde_json::Value::Array(arr);
794 }
795 }
796
797 let mut map = serde_json::Map::new();
799 if let Ok(pairs) = t.pairs::<String, Value>().collect::<Result<Vec<_>, _>>() {
800 for (k, v) in pairs {
801 map.insert(k, lua_value_to_json(v));
802 }
803 }
804 serde_json::Value::Object(map)
805 }
806 _ => serde_json::Value::Null,
807 }
808}
809
810pub fn json_to_lua_value(lua: &Lua, value: &serde_json::Value) -> mlua::Result<Value> {
812 match value {
813 serde_json::Value::Null => Ok(Value::Nil),
814 serde_json::Value::Bool(b) => Ok(Value::Boolean(*b)),
815 serde_json::Value::Number(n) => {
816 if let Some(i) = n.as_i64() {
817 Ok(Value::Integer(i))
818 } else if let Some(f) = n.as_f64() {
819 Ok(Value::Number(f))
820 } else {
821 Ok(Value::Nil)
822 }
823 }
824 serde_json::Value::String(s) => Ok(Value::String(lua.create_string(s)?)),
825 serde_json::Value::Array(arr) => {
826 let table = lua.create_table()?;
827 for (i, v) in arr.iter().enumerate() {
828 table.set(i + 1, json_to_lua_value(lua, v)?)?;
829 }
830 Ok(Value::Table(table))
831 }
832 serde_json::Value::Object(map) => {
833 let table = lua.create_table()?;
834 for (k, v) in map {
835 table.set(k.as_str(), json_to_lua_value(lua, v)?)?;
836 }
837 Ok(Value::Table(table))
838 }
839 }
840}