const std = @import("std"); const json = std.json; const Allocator = std.mem.Allocator; pub const SourceHutConfig = struct { token: ?[]const u8 = null, repositories: [][]const u8, allocator: Allocator, pub fn deinit(self: *const SourceHutConfig) void { if (self.token) |token| self.allocator.free(token); for (self.repositories) |repo| { self.allocator.free(repo); } self.allocator.free(self.repositories); } }; pub const Config = struct { github_token: ?[]const u8 = null, gitlab_token: ?[]const u8 = null, codeberg_token: ?[]const u8 = null, sourcehut: ?SourceHutConfig = null, allocator: Allocator, pub fn deinit(self: *const Config) void { if (self.github_token) |token| self.allocator.free(token); if (self.gitlab_token) |token| self.allocator.free(token); if (self.codeberg_token) |token| self.allocator.free(token); if (self.sourcehut) |*sh_config| sh_config.deinit(); } }; pub fn loadConfig(allocator: Allocator, path: []const u8) !Config { const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) { error.FileNotFound => { const stderr = std.io.getStdErr().writer(); stderr.print("Config file not found, creating default config at {s}\n", .{path}) catch {}; try createDefaultConfig(path); return Config{ .allocator = allocator }; }, else => return err, }; defer file.close(); const content = try file.readToEndAlloc(allocator, 1024 * 1024); defer allocator.free(content); return parseConfigFromJson(allocator, content); } pub fn parseConfigFromJson(allocator: Allocator, json_content: []const u8) !Config { const parsed = try json.parseFromSlice(json.Value, allocator, json_content, .{}); defer parsed.deinit(); const root = parsed.value.object; var sourcehut_config: ?SourceHutConfig = null; if (root.get("sourcehut")) |sh_obj| { const sh_object = sh_obj.object; const repos_array = sh_object.get("repositories").?.array; var repositories = try allocator.alloc([]const u8, repos_array.items.len); for (repos_array.items, 0..) |repo_item, i| { repositories[i] = try allocator.dupe(u8, repo_item.string); } sourcehut_config = SourceHutConfig{ .token = if (sh_object.get("token")) |v| try allocator.dupe(u8, v.string) else null, .repositories = repositories, .allocator = allocator, }; } return Config{ .github_token = if (root.get("github_token")) |v| switch (v) { .string => |s| if (s.len > 0) try allocator.dupe(u8, s) else null, .null => null, else => null, } else null, .gitlab_token = if (root.get("gitlab_token")) |v| switch (v) { .string => |s| if (s.len > 0) try allocator.dupe(u8, s) else null, .null => null, else => null, } else null, .codeberg_token = if (root.get("codeberg_token")) |v| switch (v) { .string => |s| if (s.len > 0) try allocator.dupe(u8, s) else null, .null => null, else => null, } else null, .sourcehut = sourcehut_config, .allocator = allocator, }; } fn createDefaultConfig(path: []const u8) !void { const file = try std.fs.cwd().createFile(path, .{}); defer file.close(); const default_config = \\{ \\ "github_token": "", \\ "gitlab_token": "", \\ "codeberg_token": "", \\ "sourcehut": { \\ "repositories": [] \\ } \\} ; try file.writeAll(default_config); } test "config loading" { const allocator = std.testing.allocator; // Test with non-existent file const config = loadConfig(allocator, "nonexistent.json") catch |err| { try std.testing.expect(err == error.FileNotFound or err == error.AccessDenied); return; }; defer config.deinit(); }