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.
This commit is contained in:
Jeremie Fraeys 2026-03-05 09:55:28 -05:00
parent 729394b7d5
commit 923ccaf22b
No known key found for this signature in database
2 changed files with 119 additions and 76 deletions

View file

@ -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 });
}

View file

@ -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;
};