summaryrefslogtreecommitdiff
path: root/src/lua_bridge.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/lua_bridge.zig')
-rw-r--r--src/lua_bridge.zig151
1 files changed, 140 insertions, 11 deletions
diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig
index cb57ade..245b0ec 100644
--- a/src/lua_bridge.zig
+++ b/src/lua_bridge.zig
@@ -36,6 +36,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
+const panto = @import("panto");
pub const c = @cImport({
@cInclude("lua.h");
@@ -260,20 +261,118 @@ pub fn pushJsonAsLua(
pushJsonValue(L, parsed.value) catch return BridgeError.OutOfMemory;
}
-/// Read a Lua value at `idx` and serialize it to a JSON-compatible owned
-/// byte string. Used to convert handler return values into ToolResult
-/// content. For now we only accept string returns; extending to richer
-/// types is straightforward but unnecessary for slice 2.
+/// Read a tool handler's return value at `idx` and convert it into an
+/// owned `[]panto.ResultPart`. Two accepted shapes:
+///
+/// * a plain string -> one `.text` part;
+/// * a table `{ text = "...", attachments = { { media_type = "...",
+/// data = "..." }, ... } }` -> an optional `.text` part followed by
+/// one `.media` part per attachment (`data` is base64-encoded bytes).
+///
+/// Every returned slice/part owns its bytes (allocated with `allocator`);
+/// the caller frees via `panto.freeResultParts`.
pub fn readHandlerResult(
L: *c.lua_State,
idx: c_int,
allocator: Allocator,
-) BridgeError![]u8 {
- if (c.lua_type(L, idx) != T_STRING) return BridgeError.BadHandlerReturn;
+) BridgeError![]panto.ResultPart {
+ const ty = c.lua_type(L, idx);
+ if (ty == T_STRING) {
+ var len: usize = 0;
+ const ptr = c.lua_tolstring(L, idx, &len);
+ if (ptr == null) return BridgeError.BadHandlerReturn;
+ return (panto.textResult(allocator, ptr[0..len]) catch
+ BridgeError.OutOfMemory);
+ }
+ if (ty != T_TABLE) return BridgeError.BadHandlerReturn;
+ return readHandlerResultTable(L, idx, allocator);
+}
+
+/// Read a string field `name` from the table at `tbl_idx`. Returns null
+/// if absent/nil, an error if present-but-not-a-string. The returned
+/// slice borrows from Lua's internal buffer — copy before popping.
+fn optStringField(L: *c.lua_State, tbl_idx: c_int, name: [*:0]const u8) BridgeError!?[]const u8 {
+ const t = c.lua_getfield(L, tbl_idx, name);
+ if (t == T_NIL) {
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return null;
+ }
+ if (t != T_STRING) {
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return BridgeError.BadHandlerReturn;
+ }
var len: usize = 0;
- const ptr = c.lua_tolstring(L, idx, &len);
- if (ptr == null) return BridgeError.BadHandlerReturn;
- return allocator.dupe(u8, ptr[0..len]) catch BridgeError.OutOfMemory;
+ const ptr = c.lua_tolstring(L, -1, &len);
+ if (ptr == null) {
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return BridgeError.BadHandlerReturn;
+ }
+ // Note: value still on stack; caller pops after copying.
+ return ptr[0..len];
+}
+
+fn readHandlerResultTable(
+ L: *c.lua_State,
+ idx: c_int,
+ allocator: Allocator,
+) BridgeError![]panto.ResultPart {
+ var parts: std.ArrayList(panto.ResultPart) = .empty;
+ errdefer {
+ for (parts.items) |p| p.deinit(allocator);
+ parts.deinit(allocator);
+ }
+
+ // Optional `text` field.
+ {
+ const maybe = try optStringField(L, idx, "text");
+ if (maybe) |s| {
+ const owned = allocator.dupe(u8, s) catch return BridgeError.OutOfMemory;
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop field value
+ parts.append(allocator, .{ .text = owned }) catch {
+ allocator.free(owned);
+ return BridgeError.OutOfMemory;
+ };
+ }
+ }
+
+ // Optional `attachments` array.
+ {
+ const t = c.lua_getfield(L, idx, "attachments");
+ defer c.lua_settop(L, c.lua_gettop(L) - 1); // pop attachments
+ if (t == T_TABLE) {
+ const n: usize = @intCast(c.lua_rawlen(L, -1));
+ var i: usize = 1;
+ while (i <= n) : (i += 1) {
+ _ = c.lua_rawgeti(L, -1, @intCast(i)); // push attachment table
+ defer c.lua_settop(L, c.lua_gettop(L) - 1); // pop attachment
+ if (c.lua_type(L, -1) != T_TABLE) return BridgeError.BadHandlerReturn;
+
+ // `media_type` is an optional hint; libpanto detects the
+ // type from the raw bytes when it's absent.
+ const media_type: ?[]const u8 = if (try optStringField(L, -1, "media_type")) |mt_slice| blk: {
+ const owned = allocator.dupe(u8, mt_slice) catch return BridgeError.OutOfMemory;
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop media_type value
+ break :blk owned;
+ } else null;
+ errdefer if (media_type) |mt| allocator.free(mt);
+
+ const data_slice = (try optStringField(L, -1, "data")) orelse
+ return BridgeError.BadHandlerReturn;
+ const data = allocator.dupe(u8, data_slice) catch return BridgeError.OutOfMemory;
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop data value
+ errdefer allocator.free(data);
+
+ parts.append(allocator, .{ .media = .{
+ .media_type = media_type,
+ .data = data,
+ } }) catch return BridgeError.OutOfMemory;
+ }
+ } else if (t != T_NIL) {
+ return BridgeError.BadHandlerReturn;
+ }
+ }
+
+ return parts.toOwnedSlice(allocator) catch BridgeError.OutOfMemory;
}
// ---------------------------------------------------------------------------
@@ -697,8 +796,38 @@ test "handler invocation: input parsed, result captured" {
}
const result = try readHandlerResult(L, -1, std.testing.allocator);
- defer std.testing.allocator.free(result);
- try std.testing.expectEqualStrings("got: hello", result);
+ defer panto.freeResultParts(std.testing.allocator, result);
+ try std.testing.expectEqual(@as(usize, 1), result.len);
+ try std.testing.expectEqualStrings("got: hello", result[0].text);
+}
+
+test "readHandlerResult: table with text and attachments" {
+ const L = c.luaL_newstate() orelse return error.LuaInitFailed;
+ defer c.lua_close(L);
+ c.luaL_openlibs(L);
+ install(L);
+
+ const script =
+ \\return {
+ \\ text = "see image",
+ \\ attachments = {
+ \\ { media_type = "image/png", data = "AAA=" },
+ \\ { media_type = "application/pdf", data = "BBB=" },
+ \\ },
+ \\}
+ ;
+ if (c.luaL_loadstring(L, script) != 0 or c.lua_pcallk(L, 0, 1, 0, 0, null) != 0) {
+ return error.LuaScriptFailed;
+ }
+
+ const result = try readHandlerResult(L, -1, std.testing.allocator);
+ defer panto.freeResultParts(std.testing.allocator, result);
+ try std.testing.expectEqual(@as(usize, 3), result.len);
+ try std.testing.expectEqualStrings("see image", result[0].text);
+ try std.testing.expectEqualStrings("image/png", result[1].media.media_type.?);
+ try std.testing.expectEqualStrings("AAA=", result[1].media.data);
+ try std.testing.expectEqualStrings("application/pdf", result[2].media.media_type.?);
+ try std.testing.expectEqualStrings("BBB=", result[2].media.data);
}
test "handler crash: error message surfaces via xpcall traceback hook" {