provider cleanup

This commit is contained in:
Emil Lerch 2025-07-12 18:27:36 -07:00
parent 7490ff3bc5
commit fd8242784d
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 233 additions and 219 deletions

View file

@ -4,10 +4,10 @@ const ArrayList = std.ArrayList;
const atom = @import("atom.zig"); const atom = @import("atom.zig");
const Release = @import("main.zig").Release; const Release = @import("main.zig").Release;
const github = @import("providers/github.zig"); const GitHub = @import("providers/GitHub.zig");
const gitlab = @import("providers/gitlab.zig"); const GitLab = @import("providers/GitLab.zig");
const codeberg = @import("providers/codeberg.zig"); const Codeberg = @import("providers/Codeberg.zig");
const sourcehut = @import("providers/sourcehut.zig"); const SourceHut = @import("providers/SourceHut.zig");
const config = @import("config.zig"); const config = @import("config.zig");
test "Atom feed validates against W3C validator" { test "Atom feed validates against W3C validator" {
@ -60,8 +60,8 @@ test "GitHub provider integration" {
return; return;
} }
var provider = github.GitHubProvider{}; var provider = GitHub.init(app_config.github_token.?);
const releases = provider.fetchReleases(allocator, app_config.github_token.?) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("GitHub provider error: {}\n", .{err}); std.debug.print("GitHub provider error: {}\n", .{err});
return; return;
}; };
@ -98,8 +98,8 @@ test "GitLab provider integration" {
return; return;
} }
var provider = gitlab.GitLabProvider{}; var provider = GitLab.init(app_config.gitlab_token.?);
const releases = provider.fetchReleases(allocator, app_config.gitlab_token.?) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("GitLab provider error: {}\n", .{err}); std.debug.print("GitLab provider error: {}\n", .{err});
return; // Skip test if provider fails return; // Skip test if provider fails
}; };
@ -139,8 +139,8 @@ test "Codeberg provider integration" {
return; return;
} }
var provider = codeberg.CodebergProvider{}; var provider = Codeberg.init(app_config.codeberg_token.?);
const releases = provider.fetchReleases(allocator, app_config.codeberg_token.?) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("Codeberg provider error: {}\n", .{err}); std.debug.print("Codeberg provider error: {}\n", .{err});
return; // Skip test if provider fails return; // Skip test if provider fails
}; };
@ -177,8 +177,8 @@ test "SourceHut provider integration" {
return; return;
} }
var provider = sourcehut.SourceHutProvider{}; var provider = SourceHut.init(app_config.sourcehut.?.token.?, app_config.sourcehut.?.repositories);
const releases = provider.fetchReleasesForRepos(allocator, app_config.sourcehut.?.repositories, app_config.sourcehut.?.token) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("SourceHut provider error: {}\n", .{err}); std.debug.print("SourceHut provider error: {}\n", .{err});
return; // Skip test if provider fails return; // Skip test if provider fails
}; };

View file

@ -5,10 +5,10 @@ const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Thread = std.Thread; const Thread = std.Thread;
const github = @import("providers/github.zig"); const GitHub = @import("providers/GitHub.zig");
const gitlab = @import("providers/gitlab.zig"); const GitLab = @import("providers/GitLab.zig");
const codeberg = @import("providers/codeberg.zig"); const Codeberg = @import("providers/Codeberg.zig");
const sourcehut = @import("providers/sourcehut.zig"); const SourceHut = @import("providers/SourceHut.zig");
const atom = @import("atom.zig"); const atom = @import("atom.zig");
const config = @import("config.zig"); const config = @import("config.zig");
const zeit = @import("zeit"); const zeit = @import("zeit");
@ -102,33 +102,27 @@ pub fn main() !u8 {
defer providers.deinit(); defer providers.deinit();
// Initialize providers with their tokens (need to persist for the lifetime of the program) // Initialize providers with their tokens (need to persist for the lifetime of the program)
var github_provider: ?github.GitHubProvider = null; var github_provider: ?GitHub = null;
var gitlab_provider: ?gitlab.GitLabProvider = null; var gitlab_provider: ?GitLab = null;
var codeberg_provider: ?codeberg.CodebergProvider = null; var codeberg_provider: ?Codeberg = null;
var sourcehut_provider: ?sourcehut.SourceHutProvider = null; var sourcehut_provider: ?SourceHut = null;
if (app_config.github_token) |token| { if (app_config.github_token) |token| {
github_provider = github.GitHubProvider.init(token); github_provider = GitHub.init(token);
try providers.append(Provider.init(&github_provider.?)); try providers.append(github_provider.?.provider());
} }
if (app_config.gitlab_token) |token| { if (app_config.gitlab_token) |token| {
gitlab_provider = gitlab.GitLabProvider.init(token); gitlab_provider = GitLab.init(token);
try providers.append(Provider.init(&gitlab_provider.?)); try providers.append(gitlab_provider.?.provider());
} }
if (app_config.codeberg_token) |token| { if (app_config.codeberg_token) |token| {
codeberg_provider = codeberg.CodebergProvider.init(token); codeberg_provider = Codeberg.init(token);
try providers.append(Provider.init(&codeberg_provider.?)); try providers.append(codeberg_provider.?.provider());
}
// Configure SourceHut provider with repositories if available
if (app_config.sourcehut) |sh_config| {
if (sh_config.repositories.len > 0 and sh_config.token != null) {
sourcehut_provider = sourcehut.SourceHutProvider.init(sh_config.token.?, sh_config.repositories);
try providers.append(Provider.init(&sourcehut_provider.?));
}
} }
if (app_config.sourcehut) |sh_config| if (sh_config.repositories.len > 0 and sh_config.token != null) {
sourcehut_provider = SourceHut.init(sh_config.token.?, sh_config.repositories);
try providers.append(sourcehut_provider.?.provider());
};
// Fetch releases from all providers concurrently using thread pool // Fetch releases from all providers concurrently using thread pool
const provider_results = try fetchReleasesFromAllProviders(allocator, providers.items, existing_releases.items); const provider_results = try fetchReleasesFromAllProviders(allocator, providers.items, existing_releases.items);

View file

@ -6,51 +6,56 @@ const ArrayList = std.ArrayList;
const zeit = @import("zeit"); const zeit = @import("zeit");
const Release = @import("../main.zig").Release; const Release = @import("../main.zig").Release;
const Provider = @import("../Provider.zig");
pub const CodebergProvider = struct { token: []const u8,
token: []const u8,
pub fn init(token: []const u8) CodebergProvider { const Self = @This();
return CodebergProvider{ .token = token };
}
pub fn fetchReleases(self: *@This(), allocator: Allocator) !ArrayList(Release) { pub fn init(token: []const u8) Self {
var client = http.Client{ .allocator = allocator }; return Self{ .token = token };
defer client.deinit(); }
var releases = ArrayList(Release).init(allocator); pub fn provider(self: *Self) Provider {
return Provider.init(self);
}
// Get starred repositories (Codeberg uses Gitea API) pub fn fetchReleases(self: *Self, allocator: Allocator) !ArrayList(Release) {
const starred_repos = try getStarredRepos(allocator, &client, self.token); var client = http.Client{ .allocator = allocator };
defer { defer client.deinit();
for (starred_repos.items) |repo| {
allocator.free(repo);
}
starred_repos.deinit();
}
// Get releases for each repo var releases = ArrayList(Release).init(allocator);
// Get starred repositories (Codeberg uses Gitea API)
const starred_repos = try getStarredRepos(allocator, &client, self.token);
defer {
for (starred_repos.items) |repo| { for (starred_repos.items) |repo| {
const repo_releases = getRepoReleases(allocator, &client, self.token, repo) catch |err| { allocator.free(repo);
std.debug.print("Error fetching Codeberg releases for {s}: {}\n", .{ repo, err });
continue;
};
defer repo_releases.deinit();
// Transfer ownership of the releases to the main list
for (repo_releases.items) |release| {
try releases.append(release);
}
} }
starred_repos.deinit();
return releases;
} }
pub fn getName(self: *@This()) []const u8 { // Get releases for each repo
_ = self; for (starred_repos.items) |repo| {
return "codeberg"; const repo_releases = getRepoReleases(allocator, &client, self.token, repo) catch |err| {
std.debug.print("Error fetching Codeberg releases for {s}: {}\n", .{ repo, err });
continue;
};
defer repo_releases.deinit();
// Transfer ownership of the releases to the main list
for (repo_releases.items) |release| {
try releases.append(release);
}
} }
};
return releases;
}
pub fn getName(self: *Self) []const u8 {
_ = self;
return "codeberg";
}
fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8) !ArrayList([]const u8) { fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8) !ArrayList([]const u8) {
var repos = ArrayList([]const u8).init(allocator); var repos = ArrayList([]const u8).init(allocator);
@ -262,10 +267,10 @@ fn parseTimestamp(date_str: []const u8) !i64 {
test "codeberg provider" { test "codeberg provider" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
var provider = CodebergProvider.init(""); var codeberg_provider = init("");
// Test with empty token (should fail gracefully) // Test with empty token (should fail gracefully)
const releases = provider.fetchReleases(allocator) catch |err| { const releases = codeberg_provider.fetchReleases(allocator) catch |err| {
try std.testing.expect(err == error.Unauthorized or err == error.HttpRequestFailed); try std.testing.expect(err == error.Unauthorized or err == error.HttpRequestFailed);
return; return;
}; };
@ -276,7 +281,7 @@ test "codeberg provider" {
releases.deinit(); releases.deinit();
} }
try std.testing.expectEqualStrings("codeberg", provider.getName()); try std.testing.expectEqualStrings("codeberg", codeberg_provider.getName());
} }
test "codeberg release parsing with live data snapshot" { test "codeberg release parsing with live data snapshot" {

View file

@ -6,48 +6,53 @@ const ArrayList = std.ArrayList;
const zeit = @import("zeit"); const zeit = @import("zeit");
const Release = @import("../main.zig").Release; const Release = @import("../main.zig").Release;
const Provider = @import("../Provider.zig");
pub const GitHubProvider = struct { token: []const u8,
token: []const u8,
pub fn init(token: []const u8) GitHubProvider { const Self = @This();
return GitHubProvider{ .token = token };
}
pub fn fetchReleases(self: *@This(), allocator: Allocator) !ArrayList(Release) { pub fn init(token: []const u8) Self {
var client = http.Client{ .allocator = allocator }; return Self{ .token = token };
defer client.deinit(); }
var releases = ArrayList(Release).init(allocator); pub fn provider(self: *Self) Provider {
return Provider.init(self);
}
// First, get starred repositories pub fn fetchReleases(self: *Self, allocator: Allocator) !ArrayList(Release) {
const starred_repos = try getStarredRepos(allocator, &client, self.token); var client = http.Client{ .allocator = allocator };
defer { defer client.deinit();
for (starred_repos.items) |repo| {
allocator.free(repo);
}
starred_repos.deinit();
}
// Then get releases for each repo var releases = ArrayList(Release).init(allocator);
// First, get starred repositories
const starred_repos = try getStarredRepos(allocator, &client, self.token);
defer {
for (starred_repos.items) |repo| { for (starred_repos.items) |repo| {
const repo_releases = getRepoReleases(allocator, &client, self.token, repo) catch |err| { allocator.free(repo);
std.debug.print("Error fetching releases for {s}: {}\n", .{ repo, err });
continue;
};
defer repo_releases.deinit();
try releases.appendSlice(repo_releases.items);
} }
starred_repos.deinit();
return releases;
} }
pub fn getName(self: *@This()) []const u8 { // Then get releases for each repo
_ = self; for (starred_repos.items) |repo| {
return "github"; const repo_releases = getRepoReleases(allocator, &client, self.token, repo) catch |err| {
std.debug.print("Error fetching releases for {s}: {}\n", .{ repo, err });
continue;
};
defer repo_releases.deinit();
try releases.appendSlice(repo_releases.items);
} }
};
return releases;
}
pub fn getName(self: *Self) []const u8 {
_ = self;
return "github";
}
fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8) !ArrayList([]const u8) { fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8) !ArrayList([]const u8) {
var repos = ArrayList([]const u8).init(allocator); var repos = ArrayList([]const u8).init(allocator);
@ -174,10 +179,10 @@ fn parseTimestamp(date_str: []const u8) !i64 {
test "github provider" { test "github provider" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
var provider = GitHubProvider.init(""); var github_provider = init("");
// Test with empty token (should fail gracefully) // Test with empty token (should fail gracefully)
const releases = provider.fetchReleases(allocator) catch |err| { const releases = github_provider.fetchReleases(allocator) catch |err| {
try std.testing.expect(err == error.HttpRequestFailed); try std.testing.expect(err == error.HttpRequestFailed);
return; return;
}; };
@ -188,7 +193,7 @@ test "github provider" {
releases.deinit(); releases.deinit();
} }
try std.testing.expectEqualStrings("github", provider.getName()); try std.testing.expectEqualStrings("github", github_provider.getName());
} }
test "github release parsing with live data snapshot" { test "github release parsing with live data snapshot" {

View file

@ -6,51 +6,56 @@ const ArrayList = std.ArrayList;
const zeit = @import("zeit"); const zeit = @import("zeit");
const Release = @import("../main.zig").Release; const Release = @import("../main.zig").Release;
const Provider = @import("../Provider.zig");
pub const GitLabProvider = struct { token: []const u8,
token: []const u8,
pub fn init(token: []const u8) GitLabProvider { const Self = @This();
return GitLabProvider{ .token = token };
}
pub fn fetchReleases(self: *@This(), allocator: Allocator) !ArrayList(Release) { pub fn init(token: []const u8) Self {
var client = http.Client{ .allocator = allocator }; return Self{ .token = token };
defer client.deinit(); }
var releases = ArrayList(Release).init(allocator); pub fn provider(self: *Self) Provider {
return Provider.init(self);
}
// Get starred projects pub fn fetchReleases(self: *Self, allocator: Allocator) !ArrayList(Release) {
const starred_projects = try getStarredProjects(allocator, &client, self.token); var client = http.Client{ .allocator = allocator };
defer { defer client.deinit();
for (starred_projects.items) |project| {
allocator.free(project); var releases = ArrayList(Release).init(allocator);
}
starred_projects.deinit(); // Get starred projects
const starred_projects = try getStarredProjects(allocator, &client, self.token);
defer {
for (starred_projects.items) |project| {
allocator.free(project);
} }
starred_projects.deinit();
}
// Get releases for each project // Get releases for each project
for (starred_projects.items) |project_id| { for (starred_projects.items) |project_id| {
const project_releases = getProjectReleases(allocator, &client, self.token, project_id) catch |err| { const project_releases = getProjectReleases(allocator, &client, self.token, project_id) catch |err| {
std.debug.print("Error fetching GitLab releases for project {s}: {}\n", .{ project_id, err }); std.debug.print("Error fetching GitLab releases for project {s}: {}\n", .{ project_id, err });
continue; continue;
}; };
defer project_releases.deinit(); defer project_releases.deinit();
// Transfer ownership of the releases to the main list // Transfer ownership of the releases to the main list
for (project_releases.items) |release| { for (project_releases.items) |release| {
try releases.append(release); try releases.append(release);
}
} }
return releases;
} }
pub fn getName(self: *@This()) []const u8 { return releases;
_ = self; }
return "gitlab";
} pub fn getName(self: *Self) []const u8 {
}; _ = self;
return "gitlab";
}
fn getStarredProjects(allocator: Allocator, client: *http.Client, token: []const u8) !ArrayList([]const u8) { fn getStarredProjects(allocator: Allocator, client: *http.Client, token: []const u8) !ArrayList([]const u8) {
var projects = ArrayList([]const u8).init(allocator); var projects = ArrayList([]const u8).init(allocator);
@ -258,10 +263,10 @@ fn parseTimestamp(date_str: []const u8) !i64 {
test "gitlab provider" { test "gitlab provider" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
var provider = GitLabProvider.init(""); var gitlab_provider = init("");
// Test with empty token (should fail gracefully) // Test with empty token (should fail gracefully)
const releases = provider.fetchReleases(allocator) catch |err| { const releases = gitlab_provider.fetchReleases(allocator) catch |err| {
try std.testing.expect(err == error.HttpRequestFailed); try std.testing.expect(err == error.HttpRequestFailed);
return; return;
}; };
@ -272,7 +277,7 @@ test "gitlab provider" {
releases.deinit(); releases.deinit();
} }
try std.testing.expectEqualStrings("gitlab", provider.getName()); try std.testing.expectEqualStrings("gitlab", gitlab_provider.getName());
} }
test "gitlab release parsing with live data snapshot" { test "gitlab release parsing with live data snapshot" {

View file

@ -6,90 +6,95 @@ const ArrayList = std.ArrayList;
const zeit = @import("zeit"); const zeit = @import("zeit");
const Release = @import("../main.zig").Release; const Release = @import("../main.zig").Release;
const Provider = @import("../Provider.zig");
pub const SourceHutProvider = struct { repositories: [][]const u8,
repositories: [][]const u8, token: []const u8,
token: []const u8,
pub fn init(token: []const u8, repositories: [][]const u8) SourceHutProvider { const Self = @This();
return SourceHutProvider{ .token = token, .repositories = repositories };
pub fn init(token: []const u8, repositories: [][]const u8) Self {
return Self{ .token = token, .repositories = repositories };
}
pub fn provider(self: *Self) Provider {
return Provider.init(self);
}
pub fn fetchReleases(self: *Self, allocator: Allocator) !ArrayList(Release) {
return self.fetchReleasesForRepos(allocator, self.repositories, self.token);
}
pub fn fetchReleasesForRepos(self: *Self, allocator: Allocator, repositories: [][]const u8, token: ?[]const u8) !ArrayList(Release) {
_ = self;
var client = http.Client{ .allocator = allocator };
defer client.deinit();
var releases = ArrayList(Release).init(allocator);
errdefer {
for (releases.items) |release| {
release.deinit(allocator);
}
releases.deinit();
} }
pub fn fetchReleases(self: *@This(), allocator: Allocator) !ArrayList(Release) { for (repositories) |repo| {
return self.fetchReleasesForRepos(allocator, self.repositories, self.token); const repo_tags = getRepoTags(allocator, &client, token, repo) catch |err| {
} std.debug.print("Error fetching SourceHut tags for {s}: {}\n", .{ repo, err });
continue;
pub fn fetchReleasesForRepos(self: *@This(), allocator: Allocator, repositories: [][]const u8, token: ?[]const u8) !ArrayList(Release) { };
_ = self;
var client = http.Client{ .allocator = allocator };
defer client.deinit();
var releases = ArrayList(Release).init(allocator);
errdefer {
for (releases.items) |release| {
release.deinit(allocator);
}
releases.deinit();
}
for (repositories) |repo| {
const repo_tags = getRepoTags(allocator, &client, token, repo) catch |err| {
std.debug.print("Error fetching SourceHut tags for {s}: {}\n", .{ repo, err });
continue;
};
defer {
for (repo_tags.items) |release| {
release.deinit(allocator);
}
repo_tags.deinit();
}
for (repo_tags.items) |release| {
const duplicated_release = Release{
.repo_name = try allocator.dupe(u8, release.repo_name),
.tag_name = try allocator.dupe(u8, release.tag_name),
.published_at = try allocator.dupe(u8, release.published_at),
.html_url = try allocator.dupe(u8, release.html_url),
.description = try allocator.dupe(u8, release.description),
.provider = try allocator.dupe(u8, release.provider),
};
releases.append(duplicated_release) catch |err| {
duplicated_release.deinit(allocator);
return err;
};
}
}
return releases;
}
pub fn fetchReleasesForReposFiltered(self: *@This(), allocator: Allocator, repositories: [][]const u8, token: ?[]const u8, existing_releases: []const Release) !ArrayList(Release) {
var latest_date: i64 = 0;
for (existing_releases) |release| {
if (std.mem.eql(u8, release.provider, "sourcehut")) {
const release_time = parseReleaseTimestamp(release.published_at) catch 0;
if (release_time > latest_date) {
latest_date = release_time;
}
}
}
const all_releases = try self.fetchReleasesForRepos(allocator, repositories, token);
defer { defer {
for (all_releases.items) |release| { for (repo_tags.items) |release| {
release.deinit(allocator); release.deinit(allocator);
} }
all_releases.deinit(); repo_tags.deinit();
} }
return filterNewReleases(allocator, all_releases.items, latest_date); for (repo_tags.items) |release| {
const duplicated_release = Release{
.repo_name = try allocator.dupe(u8, release.repo_name),
.tag_name = try allocator.dupe(u8, release.tag_name),
.published_at = try allocator.dupe(u8, release.published_at),
.html_url = try allocator.dupe(u8, release.html_url),
.description = try allocator.dupe(u8, release.description),
.provider = try allocator.dupe(u8, release.provider),
};
releases.append(duplicated_release) catch |err| {
duplicated_release.deinit(allocator);
return err;
};
}
} }
pub fn getName(self: *@This()) []const u8 { return releases;
_ = self; }
return "sourcehut";
pub fn fetchReleasesForReposFiltered(self: *Self, allocator: Allocator, repositories: [][]const u8, token: ?[]const u8, existing_releases: []const Release) !ArrayList(Release) {
var latest_date: i64 = 0;
for (existing_releases) |release| {
if (std.mem.eql(u8, release.provider, "sourcehut")) {
const release_time = parseReleaseTimestamp(release.published_at) catch 0;
if (release_time > latest_date) {
latest_date = release_time;
}
}
} }
};
const all_releases = try self.fetchReleasesForRepos(allocator, repositories, token);
defer {
for (all_releases.items) |release| {
release.deinit(allocator);
}
all_releases.deinit();
}
return filterNewReleases(allocator, all_releases.items, latest_date);
}
pub fn getName(self: *Self) []const u8 {
_ = self;
return "sourcehut";
}
fn getRepoTags(allocator: Allocator, client: *http.Client, token: ?[]const u8, repo: []const u8) !ArrayList(Release) { fn getRepoTags(allocator: Allocator, client: *http.Client, token: ?[]const u8, repo: []const u8) !ArrayList(Release) {
var releases = ArrayList(Release).init(allocator); var releases = ArrayList(Release).init(allocator);
@ -343,10 +348,10 @@ test "sourcehut provider" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const repos = [_][]const u8{}; const repos = [_][]const u8{};
var provider = SourceHutProvider.init("", &repos); var sourcehut_provider = init("", &repos);
// Test with empty token (should fail gracefully) // Test with empty token (should fail gracefully)
const releases = provider.fetchReleases(allocator) catch |err| { const releases = sourcehut_provider.fetchReleases(allocator) catch |err| {
try std.testing.expect(err == error.HttpRequestFailed); try std.testing.expect(err == error.HttpRequestFailed);
return; return;
}; };
@ -357,7 +362,7 @@ test "sourcehut provider" {
releases.deinit(); releases.deinit();
} }
try std.testing.expectEqualStrings("sourcehut", provider.getName()); try std.testing.expectEqualStrings("sourcehut", sourcehut_provider.getName());
} }
test "sourcehut release parsing with live data snapshot" { test "sourcehut release parsing with live data snapshot" {