search for standard sites pub-search.waow.tech
search zig blog atproto
at multi-platform-schema 286 lines 8.9 kB view raw
1//! Turso HTTP API client 2//! https://docs.turso.tech/sdk/http/reference 3 4const std = @import("std"); 5const http = std.http; 6const json = std.json; 7const mem = std.mem; 8const Allocator = mem.Allocator; 9 10const result = @import("result.zig"); 11pub const Result = result.Result; 12pub const Row = result.Row; 13pub const BatchResult = result.BatchResult; 14 15const Client = @This(); 16 17// Hrana protocol types (https://github.com/tursodatabase/libsql/blob/main/docs/HRANA_3_SPEC.md) 18const Value = struct { type: []const u8 = "text", value: []const u8 }; 19const Stmt = struct { sql: []const u8, args: ?[]const Value = null }; 20const ExecuteReq = struct { type: []const u8 = "execute", stmt: Stmt }; 21const CloseReq = struct { type: []const u8 = "close" }; 22 23const URL_BUF_SIZE = 512; 24const AUTH_BUF_SIZE = 512; 25 26allocator: Allocator, 27url: []const u8, 28token: []const u8, 29mutex: std.Thread.Mutex = .{}, 30http_client: http.Client, 31 32pub fn init(allocator: Allocator) !Client { 33 const url = std.posix.getenv("TURSO_URL") orelse { 34 std.debug.print("TURSO_URL not set\n", .{}); 35 return error.MissingEnv; 36 }; 37 const token = std.posix.getenv("TURSO_TOKEN") orelse { 38 std.debug.print("TURSO_TOKEN not set\n", .{}); 39 return error.MissingEnv; 40 }; 41 42 const libsql_prefix = "libsql://"; 43 const host = if (mem.startsWith(u8, url, libsql_prefix)) 44 url[libsql_prefix.len..] 45 else 46 url; 47 48 std.debug.print("turso client initialized: {s}\n", .{host}); 49 50 return .{ 51 .allocator = allocator, 52 .url = host, 53 .token = token, 54 .http_client = .{ .allocator = allocator }, 55 }; 56} 57 58pub fn deinit(self: *Client) void { 59 self.http_client.deinit(); 60} 61 62pub fn query(self: *Client, comptime sql: []const u8, args: anytype) !Result { 63 comptime validateArgs(sql, @TypeOf(args)); 64 const args_slice = try self.argsToSlice(args); 65 defer self.allocator.free(args_slice); 66 const response = try self.executeRaw(sql, args_slice); 67 defer self.allocator.free(response); 68 return Result.parse(self.allocator, response); 69} 70 71pub fn exec(self: *Client, comptime sql: []const u8, args: anytype) !void { 72 comptime validateArgs(sql, @TypeOf(args)); 73 const args_slice = try self.argsToSlice(args); 74 defer self.allocator.free(args_slice); 75 const response = try self.executeRaw(sql, args_slice); 76 self.allocator.free(response); 77} 78 79pub const Statement = struct { 80 sql: []const u8, 81 args: []const []const u8 = &.{}, 82}; 83 84pub fn queryBatch(self: *Client, statements: []const Statement) !BatchResult { 85 const response = try self.executeBatchRaw(statements); 86 defer self.allocator.free(response); 87 return BatchResult.parse(self.allocator, response, statements.len); 88} 89 90fn argsToSlice(self: *Client, args: anytype) ![]const []const u8 { 91 const ArgsType = @TypeOf(args); 92 const info = @typeInfo(ArgsType); 93 94 if (info == .pointer) { 95 const child = @typeInfo(info.pointer.child); 96 if (child == .@"struct") { 97 const fields = child.@"struct".fields; 98 const slice = try self.allocator.alloc([]const u8, fields.len); 99 inline for (fields, 0..) |field, i| { 100 slice[i] = @field(args.*, field.name); 101 } 102 return slice; 103 } 104 } 105 106 if (info == .@"struct") { 107 const fields = info.@"struct".fields; 108 const slice = try self.allocator.alloc([]const u8, fields.len); 109 inline for (fields, 0..) |field, i| { 110 slice[i] = @field(args, field.name); 111 } 112 return slice; 113 } 114 115 @compileError("args must be a tuple or pointer to tuple"); 116} 117 118fn executeRaw(self: *Client, sql: []const u8, args: []const []const u8) ![]const u8 { 119 self.mutex.lock(); 120 defer self.mutex.unlock(); 121 122 var url_buf: [URL_BUF_SIZE]u8 = undefined; 123 const url = std.fmt.bufPrint(&url_buf, "https://{s}/v2/pipeline", .{self.url}) catch 124 return error.UrlTooLong; 125 126 const body = try self.buildRequestBody(sql, args); 127 defer self.allocator.free(body); 128 129 var auth_buf: [AUTH_BUF_SIZE]u8 = undefined; 130 const auth = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{self.token}) catch 131 return error.AuthTooLong; 132 133 var response_body: std.Io.Writer.Allocating = .init(self.allocator); 134 errdefer response_body.deinit(); 135 136 const res = self.http_client.fetch(.{ 137 .location = .{ .url = url }, 138 .method = .POST, 139 .headers = .{ 140 .content_type = .{ .override = "application/json" }, 141 .authorization = .{ .override = auth }, 142 }, 143 .payload = body, 144 .response_writer = &response_body.writer, 145 }) catch |err| { 146 std.debug.print("turso request failed: {}\n", .{err}); 147 return error.HttpError; 148 }; 149 150 if (res.status != .ok) { 151 std.debug.print("turso error: {}\n", .{res.status}); 152 return error.TursoError; 153 } 154 155 return try response_body.toOwnedSlice(); 156} 157 158fn executeBatchRaw(self: *Client, statements: []const Statement) ![]const u8 { 159 self.mutex.lock(); 160 defer self.mutex.unlock(); 161 162 var url_buf: [URL_BUF_SIZE]u8 = undefined; 163 const url = std.fmt.bufPrint(&url_buf, "https://{s}/v2/pipeline", .{self.url}) catch 164 return error.UrlTooLong; 165 166 const body = try self.buildBatchRequestBody(statements); 167 defer self.allocator.free(body); 168 169 var auth_buf: [AUTH_BUF_SIZE]u8 = undefined; 170 const auth = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{self.token}) catch 171 return error.AuthTooLong; 172 173 var response_body: std.Io.Writer.Allocating = .init(self.allocator); 174 errdefer response_body.deinit(); 175 176 const res = self.http_client.fetch(.{ 177 .location = .{ .url = url }, 178 .method = .POST, 179 .headers = .{ 180 .content_type = .{ .override = "application/json" }, 181 .authorization = .{ .override = auth }, 182 }, 183 .payload = body, 184 .response_writer = &response_body.writer, 185 }) catch |err| { 186 std.debug.print("turso batch request failed: {}\n", .{err}); 187 return error.HttpError; 188 }; 189 190 if (res.status != .ok) { 191 std.debug.print("turso batch error: {}\n", .{res.status}); 192 return error.TursoError; 193 } 194 195 return try response_body.toOwnedSlice(); 196} 197 198fn buildBatchRequestBody(self: *Client, statements: []const Statement) ![]const u8 { 199 var body: std.Io.Writer.Allocating = .init(self.allocator); 200 errdefer body.deinit(); 201 var jw: json.Stringify = .{ .writer = &body.writer, .options = .{ .emit_null_optional_fields = false } }; 202 203 try jw.beginObject(); 204 try jw.objectField("requests"); 205 try jw.beginArray(); 206 207 for (statements) |stmt| { 208 const values = try self.toValues(stmt.args); 209 defer self.allocator.free(values); 210 try jw.write(ExecuteReq{ 211 .stmt = .{ .sql = stmt.sql, .args = if (values.len > 0) values else null }, 212 }); 213 } 214 215 try jw.write(CloseReq{}); 216 try jw.endArray(); 217 try jw.endObject(); 218 219 return try body.toOwnedSlice(); 220} 221 222fn buildRequestBody(self: *Client, sql: []const u8, args: []const []const u8) ![]const u8 { 223 var body: std.Io.Writer.Allocating = .init(self.allocator); 224 errdefer body.deinit(); 225 var jw: json.Stringify = .{ .writer = &body.writer, .options = .{ .emit_null_optional_fields = false } }; 226 227 const values = try self.toValues(args); 228 defer self.allocator.free(values); 229 230 try jw.beginObject(); 231 try jw.objectField("requests"); 232 try jw.beginArray(); 233 try jw.write(ExecuteReq{ 234 .stmt = .{ .sql = sql, .args = if (values.len > 0) values else null }, 235 }); 236 try jw.write(CloseReq{}); 237 try jw.endArray(); 238 try jw.endObject(); 239 240 return try body.toOwnedSlice(); 241} 242 243fn toValues(self: *Client, args: []const []const u8) ![]const Value { 244 if (args.len == 0) return &.{}; 245 const values = try self.allocator.alloc(Value, args.len); 246 for (args, 0..) |arg, i| { 247 values[i] = .{ .value = arg }; 248 } 249 return values; 250} 251 252fn validateArgs(comptime sql: []const u8, comptime ArgsType: type) void { 253 const expected = countPlaceholders(sql); 254 const provided = countArgsType(ArgsType); 255 if (expected != provided) { 256 @compileError(std.fmt.comptimePrint( 257 "SQL has {} placeholders but {} args provided", 258 .{ expected, provided }, 259 )); 260 } 261} 262 263fn countPlaceholders(comptime sql: []const u8) usize { 264 var count: usize = 0; 265 for (sql) |c| { 266 if (c == '?') count += 1; 267 } 268 return count; 269} 270 271fn countArgsType(comptime ArgsType: type) usize { 272 const info = @typeInfo(ArgsType); 273 274 if (info == .pointer) { 275 const child = @typeInfo(info.pointer.child); 276 if (child == .@"struct") { 277 return child.@"struct".fields.len; 278 } 279 } 280 281 if (info == .@"struct") { 282 return info.@"struct".fields.len; 283 } 284 285 return 0; 286}