From 621d7fee0ace729f8d44126032d2c6e13f72ee7f Mon Sep 17 00:00:00 2001 From: t Date: Tue, 7 Jul 2026 11:29:23 -0600 Subject: Move libpanto projects to libpantograph dependency Remove the in-repo libpanto sources and binding projects from pantograph. Consume libpantograph through the Zig package URL at code.tjp.lol/libpantograph.git, including the Lua module artifact used by CLI extensions. --- libpanto/src/http_helper.zig | 160 ------------------------------------------- 1 file changed, 160 deletions(-) delete mode 100644 libpanto/src/http_helper.zig (limited to 'libpanto/src/http_helper.zig') diff --git a/libpanto/src/http_helper.zig b/libpanto/src/http_helper.zig deleted file mode 100644 index a075d72..0000000 --- a/libpanto/src/http_helper.zig +++ /dev/null @@ -1,160 +0,0 @@ -//! Small non-streaming HTTP request/response helper over the process-global -//! `std.http.Client`. The streaming providers deliberately avoid `fetch()` -//! (it buffers); auth flows want the opposite — request a URL, read the whole -//! response body into one buffer, inspect status + JSON. This is that. -//! -//! Used by `auth.zig` for OAuth device flows and token exchanges. Not part of -//! the streaming hot path, so simplicity wins: one request, full-body read, -//! owned result. - -const std = @import("std"); -const config_mod = @import("config.zig"); - -pub const Header = config_mod.Header; -pub const Method = std.http.Method; - -/// A fully-read HTTP response. `body` is owned by `allocator`. -pub const Response = struct { - allocator: std.mem.Allocator, - status: u16, - body: []u8, - - pub fn deinit(self: Response) void { - self.allocator.free(self.body); - } - - /// True for a 2xx status. - pub fn ok(self: Response) bool { - return self.status >= 200 and self.status < 300; - } -}; - -pub const Options = struct { - /// Caller headers (auth bearer, identity headers, …). `accept` and - /// `content-type` are added separately from the fields below. - headers: []const Header = &.{}, - /// Request body, or null for a bodiless request (e.g. a GET). - body: ?[]const u8 = null, - /// `content-type` header value when `body` is present. - content_type: ?[]const u8 = null, - /// `accept` header value. Defaults to JSON. - accept: ?[]const u8 = "application/json", - /// Upper bound on the response body read into memory. - max_body_bytes: usize = 1 << 20, -}; - -/// Perform one request and return the fully-read response. The caller owns -/// `Response.body` and must `deinit` it. Redirects are surfaced as their 3xx -/// status (not followed) so auth credentials never leak across a redirect. -pub fn request( - allocator: std.mem.Allocator, - client: *std.http.Client, - method: Method, - url: []const u8, - opts: Options, -) !Response { - const uri = try std.Uri.parse(url); - - var hdrs: std.ArrayList(std.http.Header) = .empty; - defer hdrs.deinit(allocator); - if (opts.accept) |a| try hdrs.append(allocator, .{ .name = "accept", .value = a }); - if (opts.content_type) |ct| { - if (opts.body != null) try hdrs.append(allocator, .{ .name = "content-type", .value = ct }); - } - for (opts.headers) |h| try hdrs.append(allocator, .{ .name = h.name, .value = h.value }); - - var req = try client.request(method, uri, .{ - .extra_headers = hdrs.items, - // Disable compression so we read the body without a decompressor. - .headers = .{ .accept_encoding = .{ .override = "identity" } }, - .keep_alive = false, - // Surface 3xx to us rather than following with auth headers attached. - .redirect_behavior = .unhandled, - }); - defer req.deinit(); - - if (opts.body) |body| { - req.transfer_encoding = .{ .content_length = body.len }; - var send_buf: [4096]u8 = undefined; - var bw = try req.sendBodyUnflushed(&send_buf); - try bw.writer.writeAll(body); - try bw.end(); - try req.connection.?.flush(); - } else { - try req.sendBodiless(); - } - - var redirect_buf: [2048]u8 = undefined; - var response = try req.receiveHead(&redirect_buf); - const status: u16 = @intFromEnum(response.head.status); - - var transfer_buf: [4096]u8 = undefined; - const body_reader = response.reader(&transfer_buf); - var out: std.ArrayList(u8) = .empty; - errdefer out.deinit(allocator); - var tmp: [4096]u8 = undefined; - while (true) { - const n = body_reader.readSliceShort(&tmp) catch break; - if (n == 0) break; - try out.appendSlice(allocator, tmp[0..n]); - if (out.items.len > opts.max_body_bytes) break; - } - - return .{ .allocator = allocator, .status = status, .body = try out.toOwnedSlice(allocator) }; -} - -// =========================================================================== -// JSON helpers (used by auth flows to read fields out of a response body) -// =========================================================================== - -/// Read a string at a dotted JSON path (e.g. `endpoints.api`) from a parsed -/// JSON value. Returns null if any segment is missing or the leaf is not a -/// string. Borrows from `root`. -pub fn jsonStringAtPath(root: std.json.Value, path: []const u8) ?[]const u8 { - const leaf = jsonAtPath(root, path) orelse return null; - return switch (leaf) { - .string => |s| s, - else => null, - }; -} - -/// Read an integer (unix-seconds expiry, etc.) at a dotted JSON path. Accepts -/// JSON integers and integer-valued floats. Returns null otherwise. -pub fn jsonIntAtPath(root: std.json.Value, path: []const u8) ?i64 { - const leaf = jsonAtPath(root, path) orelse return null; - return switch (leaf) { - .integer => |i| i, - .float => |f| @intFromFloat(f), - .number_string => |s| std.fmt.parseInt(i64, s, 10) catch null, - else => null, - }; -} - -/// Walk a dotted path through nested JSON objects. Returns the leaf value or -/// null if any object segment is missing. -pub fn jsonAtPath(root: std.json.Value, path: []const u8) ?std.json.Value { - var cur = root; - var it = std.mem.splitScalar(u8, path, '.'); - while (it.next()) |seg| { - switch (cur) { - .object => |o| cur = o.get(seg) orelse return null, - else => return null, - } - } - return cur; -} - -const t = std.testing; - -test "jsonAtPath: nested object string + int" { - const src = - \\{"token":"abc","expires_at":1700000000,"endpoints":{"api":"https://x"}} - ; - var parsed = try std.json.parseFromSlice(std.json.Value, t.allocator, src, .{}); - defer parsed.deinit(); - try t.expectEqualStrings("abc", jsonStringAtPath(parsed.value, "token").?); - try t.expectEqual(@as(i64, 1700000000), jsonIntAtPath(parsed.value, "expires_at").?); - try t.expectEqualStrings("https://x", jsonStringAtPath(parsed.value, "endpoints.api").?); - try t.expect(jsonStringAtPath(parsed.value, "endpoints.missing") == null); - try t.expect(jsonStringAtPath(parsed.value, "nope.deep") == null); -} -- cgit v1.3