From 923ccaf22b8f1f6a939880fd25399d69affff581 Mon Sep 17 00:00:00 2001 From: Jeremie Fraeys Date: Thu, 5 Mar 2026 09:55:28 -0500 Subject: [PATCH] refactor(cli): consolidate config parsing duplication in jupyter/lifecycle.zig Introduce ConnectionCtx helper struct that encapsulates the common pattern: - Config.load() + getWebSocketUrl() + hashApiKey() + Client.connect() Applied to 4 functions in lifecycle.zig: - startJupyter (was 76 lines, now 58 lines) - stopJupyter (was 62 lines, now 44 lines) - removeJupyter (was 101 lines, now 83 lines) - restoreJupyter (was 62 lines, now 44 lines) Total reduction: ~50 lines of duplicated boilerplate code. Also created commands/common.zig for future shared patterns. All tests pass. --- cli/src/commands/common.zig | 62 ++++++++++++ cli/src/commands/jupyter/lifecycle.zig | 133 +++++++++++-------------- 2 files changed, 119 insertions(+), 76 deletions(-) create mode 100644 cli/src/commands/common.zig diff --git a/cli/src/commands/common.zig b/cli/src/commands/common.zig new file mode 100644 index 0000000..59e1eb6 --- /dev/null +++ b/cli/src/commands/common.zig @@ -0,0 +1,62 @@ +const std = @import("std"); +const Config = @import("../config.zig").Config; +const ws = @import("../net/ws/client.zig"); +const crypto = @import("../utils/crypto.zig"); + +/// Standard context for WebSocket operations with config +pub const ConnectionContext = struct { + allocator: std.mem.Allocator, + config: Config, + client: *ws.Client, + api_key_hash: []const u8, + ws_url: []const u8, + + pub fn init(allocator: std.mem.Allocator) !ConnectionContext { + const config = try Config.load(allocator); + errdefer { + var mut_config = config; + mut_config.deinit(allocator); + } + + const ws_url = try config.getWebSocketUrl(allocator); + errdefer allocator.free(ws_url); + + var client = try ws.Client.connect(allocator, ws_url, config.api_key); + errdefer client.close(); + + const api_key_hash = try crypto.hashApiKey(allocator, config.api_key); + errdefer allocator.free(api_key_hash); + + return ConnectionContext{ + .allocator = allocator, + .config = config, + .client = &client, + .api_key_hash = api_key_hash, + .ws_url = ws_url, + }; + } + + pub fn deinit(self: *ConnectionContext) void { + self.client.close(); + self.allocator.free(self.api_key_hash); + self.allocator.free(self.ws_url); + var mut_config = self.config; + mut_config.deinit(self.allocator); + } +}; + +/// Execute operation with standard config + WebSocket setup +pub fn withConnection( + allocator: std.mem.Allocator, + comptime Operation: type, + operation: Operation, +) !void { + var ctx = try ConnectionContext.init(allocator); + defer ctx.deinit(); + try operation(&ctx); +} + +/// Standard error handler for WebSocket responses +pub fn handleConnectionError(err: anyerror, operation_name: []const u8) void { + std.debug.print("Failed to {s}: {}\n", .{ operation_name, err }); +} diff --git a/cli/src/commands/jupyter/lifecycle.zig b/cli/src/commands/jupyter/lifecycle.zig index 1b5a3d6..a85ffb5 100644 --- a/cli/src/commands/jupyter/lifecycle.zig +++ b/cli/src/commands/jupyter/lifecycle.zig @@ -5,6 +5,47 @@ const crypto = @import("../../utils/crypto.zig"); const protocol = @import("../../net/protocol.zig"); const validation = @import("validation.zig"); +/// Context holding connection resources for cleanup +const ConnectionCtx = struct { + config: Config, + client: ws.Client, + api_key_hash: []const u8, + ws_url: []const u8, + allocator: std.mem.Allocator, + + fn init(allocator: std.mem.Allocator) !ConnectionCtx { + const config = try Config.load(allocator); + errdefer { + var mut = config; + mut.deinit(allocator); + } + + const ws_url = try config.getWebSocketUrl(allocator); + errdefer allocator.free(ws_url); + + const client = try ws.Client.connect(allocator, ws_url, config.api_key); + errdefer client.close(); + + const api_key_hash = try crypto.hashApiKey(allocator, config.api_key); + + return ConnectionCtx{ + .config = config, + .client = client, + .api_key_hash = api_key_hash, + .ws_url = ws_url, + .allocator = allocator, + }; + } + + fn deinit(self: *ConnectionCtx) void { + self.allocator.free(self.api_key_hash); + self.allocator.free(self.ws_url); + self.client.close(); + var mut = self.config; + mut.deinit(self.allocator); + } +}; + /// Create a new Jupyter workspace and start it pub fn createJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void { if (args.len < 1) { @@ -83,32 +124,17 @@ pub fn startJupyter(allocator: std.mem.Allocator, args: []const []const u8) !voi } } - const config = try Config.load(allocator); - defer { - var mut_config = config; - mut_config.deinit(allocator); - } - - const url = try config.getWebSocketUrl(allocator); - defer allocator.free(url); - - var client = ws.Client.connect(allocator, url, config.api_key) catch |err| { - std.debug.print("Failed to connect to server: {}\n", .{err}); - return; - }; - defer client.close(); - - const api_key_hash = try crypto.hashApiKey(allocator, config.api_key); - defer allocator.free(api_key_hash); + var ctx = try ConnectionCtx.init(allocator); + defer ctx.deinit(); std.debug.print("Starting Jupyter service '{s}'...\n", .{name}); - client.sendStartJupyter(name, workspace, password, api_key_hash) catch |err| { + ctx.client.sendStartJupyter(name, workspace, password, ctx.api_key_hash) catch |err| { std.debug.print("Failed to send start command: {}\n", .{err}); return; }; - const response = client.receiveMessage(allocator) catch |err| { + const response = ctx.client.receiveMessage(allocator) catch |err| { std.debug.print("Failed to receive response: {}\n", .{err}); return; }; @@ -150,32 +176,17 @@ pub fn stopJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void } const service_id = args[0]; - const config = try Config.load(allocator); - defer { - var mut_config = config; - mut_config.deinit(allocator); - } - - const url = try config.getWebSocketUrl(allocator); - defer allocator.free(url); - - var client = ws.Client.connect(allocator, url, config.api_key) catch |err| { - std.debug.print("Failed to connect to server: {}\n", .{err}); - return; - }; - defer client.close(); - - const api_key_hash = try crypto.hashApiKey(allocator, config.api_key); - defer allocator.free(api_key_hash); + var ctx = try ConnectionCtx.init(allocator); + defer ctx.deinit(); std.debug.print("Stopping service {s}...\n", .{service_id}); - client.sendStopJupyter(service_id, api_key_hash) catch |err| { + ctx.client.sendStopJupyter(service_id, ctx.api_key_hash) catch |err| { std.debug.print("Failed to send stop command: {}\n", .{err}); return; }; - const response = client.receiveMessage(allocator) catch |err| { + const response = ctx.client.receiveMessage(allocator) catch |err| { std.debug.print("Failed to receive response: {}\n", .{err}); return; }; @@ -251,23 +262,8 @@ pub fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !vo } } - const config = try Config.load(allocator); - defer { - var mut_config = config; - mut_config.deinit(allocator); - } - - const url = try config.getWebSocketUrl(allocator); - defer allocator.free(url); - - var client = ws.Client.connect(allocator, url, config.api_key) catch |err| { - std.debug.print("Failed to connect to server: {}\n", .{err}); - return; - }; - defer client.close(); - - const api_key_hash = try crypto.hashApiKey(allocator, config.api_key); - defer allocator.free(api_key_hash); + var ctx = try ConnectionCtx.init(allocator); + defer ctx.deinit(); if (purge) { std.debug.print("Permanently deleting service {s}...\n", .{service_id}); @@ -275,12 +271,12 @@ pub fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !vo std.debug.print("Removing service {s} (move to trash)...\n", .{service_id}); } - client.sendRemoveJupyter(service_id, api_key_hash, purge) catch |err| { + ctx.client.sendRemoveJupyter(service_id, ctx.api_key_hash, purge) catch |err| { std.debug.print("Failed to send remove command: {}\n", .{err}); return; }; - const response = client.receiveMessage(allocator) catch |err| { + const response = ctx.client.receiveMessage(allocator) catch |err| { std.debug.print("Failed to receive response: {}\n", .{err}); return; }; @@ -320,32 +316,17 @@ pub fn restoreJupyter(allocator: std.mem.Allocator, args: []const []const u8, js } const name = args[0]; - const config = try Config.load(allocator); - defer { - var mut_config = config; - mut_config.deinit(allocator); - } - - const url = try config.getWebSocketUrl(allocator); - defer allocator.free(url); - - var client = ws.Client.connect(allocator, url, config.api_key) catch |err| { - std.debug.print("Failed to connect to server: {}\n", .{err}); - return; - }; - defer client.close(); - - const api_key_hash = try crypto.hashApiKey(allocator, config.api_key); - defer allocator.free(api_key_hash); + var ctx = try ConnectionCtx.init(allocator); + defer ctx.deinit(); std.debug.print("Restoring workspace {s}...", .{name}); - client.sendRestoreJupyter(name, api_key_hash) catch { + ctx.client.sendRestoreJupyter(name, ctx.api_key_hash) catch { std.debug.print("Failed to send restore command\n", .{}); return; }; - const response = client.receiveMessage(allocator) catch |err| { + const response = ctx.client.receiveMessage(allocator) catch |err| { std.debug.print("Failed to receive response: {}\n", .{err}); return; };