Skip to content

Instantly share code, notes, and snippets.

@paulosuzart
Last active January 3, 2025 15:12
Show Gist options
  • Save paulosuzart/37aaeb8ab1de70e68404259bf928e371 to your computer and use it in GitHub Desktop.
Save paulosuzart/37aaeb8ab1de70e68404259bf928e371 to your computer and use it in GitHub Desktop.
Simple fetching starred items from github
//! A simple test that creates a http client, fetches the first page of starred items for a given user
//! and prints each repo with their topics, as well as each language and the amount of repos for that language.
//! Sample output:
//! Repo name is gluesql/gluesql ({ database, nosql, rust, schemaless, sql, storage, webassembly, websql })
//! Repo name is efugier/smartcat ({ ai, chatgpt, cli, command-line, command-line-tool, copilot, llm, mistral-ai, unix })
//! Repo name is regolith-labs/steel ({ solana })
//! Language Rust: 13 repos
//! Language Zig: 2 repos
//! Language Jupyter Notebook: 1 repos
//! Language HTML: 1 repos
//! ...
const std = @import("std");
const lib = @import("root.zig");
const Location = std.http.Client.FetchOptions.Location;
const URI = "https://api.github.com/user/starred";
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// gets the token from the env
const token = std.posix.getenv("TOKEN") orelse {
std.log.err("Please provide a TOKEN env", .{});
return;
};
// creates the bearer
const bearer = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token});
// allocates the http client
var client = std.http.Client{
.allocator = allocator,
};
const uri = try std.Uri.parse(URI);
// We allocate the space where the resultg body will be placed
var respStorage = std.ArrayList(u8).init(allocator);
const opts = std.http.Client.FetchOptions{
.location = Location{ .uri = uri },
.method = std.http.Method.GET,
.headers = std.http.Client.Request.Headers{ .authorization = .{ .override = bearer } },
.response_storage = .{ .dynamic = &respStorage },
};
const res = try client.fetch(opts);
if (res.status != std.http.Status.ok) {
std.debug.print("Error general: {s}", .{respStorage.items});
return;
}
const repos = try std.json.parseFromSlice([]lib.Repo, allocator, respStorage.items, .{
.ignore_unknown_fields = true,
});
for (repos.value) |r| {
std.debug.print("Repo name is {s}/{s} ({s})\n", .{ r.owner.login, r.name, r.topics });
}
const x = &repos.value;
var groupped = std.StringHashMap([]*lib.Repo).init(allocator);
try lib.byLanguage(allocator, x, &groupped);
var langIter = groupped.iterator();
while (langIter.next()) |g| {
std.debug.print("Language {s}: {d} repos\n", .{ g.key_ptr.*, g.value_ptr.*.len });
}
// careful ordered deinit and free is needed in order for everything to work. Otherwise we get runtime panics.
// There's so much stuff to deinit and free that the would need to get a LanguageGroup that takes the allocator and
// does the dirty work by itself. Same for the client.
defer {
var iter = groupped.iterator();
while (iter.next()) |o| {
allocator.free(o.value_ptr.*);
}
groupped.deinit();
repos.deinit();
respStorage.deinit();
allocator.free(bearer);
client.deinit();
_ = gpa.deinit();
}
}
//! Aggregates the struct and the by_language stuff.
const std = @import("std");
const testing = std.testing;
pub const Owner = struct {
login: []u8,
};
pub const Repo = struct {
name: []u8,
owner: Owner,
description: ?[]u8,
topics: [][]u8,
language: ?[]u8,
};
/// Group the repositories by language. Takes the target HashMap as a parameter because I couldn't make it
/// work in a way that the hashmap was returned. (At least not without breaking here and there).
/// The calling code is responsible for cleaning up the hashmap INCLUDING THE allocated slices for pointers to Repo.
pub fn byLanguage(allocator: std.mem.Allocator, repos: *const []Repo, target: *std.StringHashMap([]*Repo)) !void {
for (repos.*) |*repo| {
const language = repo.language orelse "not-set";
const gop = try target.getOrPut(language);
if (!gop.found_existing) {
// Allocate a slice of one repo pointer for new languages
gop.value_ptr.* = try allocator.alloc(*Repo, 1);
gop.value_ptr.*[0] = repo; // repo is now a pointer
} else {
// Extend the existing slice of repo pointers
const current_slice = gop.value_ptr.*;
var new_slice = try allocator.realloc(current_slice, current_slice.len + 1);
new_slice[current_slice.len] = repo; // repo is now a pointer
gop.value_ptr.* = new_slice;
}
}
}
test "byLanguage works" {
const allocator = std.testing.allocator;
const repos = [_]Repo{
.{
.name = @constCast("repo1"),
.language = @constCast("zig"),
.owner = Owner{ .login = @constCast("ps") },
.topics = undefined,
.description = @constCast("sample"),
},
.{
.name = @constCast("repo2"),
.language = @constCast("rust"),
.owner = Owner{ .login = @constCast("ps") },
.topics = undefined,
.description = @constCast("sample"),
},
.{
.name = @constCast("repo3"),
.language = @constCast("rust"),
.owner = Owner{ .login = @constCast("ps") },
.topics = undefined,
.description = @constCast("sample"),
},
};
const repo_slice = try allocator.alloc(Repo, repos.len);
defer allocator.free(repo_slice);
@memcpy(repo_slice, &repos);
var groupped = std.StringHashMap([]*Repo).init(allocator);
try byLanguage(allocator, @constCast(&repo_slice), &groupped);
defer {
var iter = groupped.iterator();
while (iter.next()) |e| {
allocator.free(e.value_ptr.*);
}
groupped.deinit();
}
try std.testing.expectEqual(@as(usize, 2), groupped.count());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment