aboutsummaryrefslogtreecommitdiffstats
path: root/src/index.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/index.zig')
-rw-r--r--src/index.zig103
1 files changed, 103 insertions, 0 deletions
diff --git a/src/index.zig b/src/index.zig
new file mode 100644
index 0000000..7eb3e4b
--- /dev/null
+++ b/src/index.zig
@@ -0,0 +1,103 @@
+//! Discover Zig versions and their docs URLs from ziglang.org/download/index.json.
+
+const std = @import("std");
+const assert = std.debug.assert;
+const fetch = @import("fetch.zig");
+
+pub const index_url = "https://ziglang.org/download/index.json";
+
+/// Upper bound on versions we will process (NASA Power-of-10: bounded loops).
+pub const max_versions = 64;
+
+pub const Version = struct {
+ /// Release name as it appears in index.json (e.g. "master", "0.15.2").
+ name: []const u8,
+ /// Absolute URL of the single-page language reference.
+ docs_url: []const u8,
+};
+
+/// Owns the slice of versions and all their backing strings.
+pub const Versions = struct {
+ allocator: std.mem.Allocator,
+ items: []Version,
+
+ pub fn deinit(self: *Versions) void {
+ for (self.items) |v| {
+ self.allocator.free(v.name);
+ self.allocator.free(v.docs_url);
+ }
+ self.allocator.free(self.items);
+ }
+};
+
+/// Fetch and parse index.json, collecting every entry that has a `docs` URL.
+pub fn fetchAll(allocator: std.mem.Allocator, client: *std.http.Client) !Versions {
+ const json = try fetch.get(allocator, client, index_url);
+ defer allocator.free(json);
+ return parse(allocator, json);
+}
+
+/// Parse index.json bytes into owned versions. Split out for testability.
+pub fn parse(allocator: std.mem.Allocator, json: []const u8) !Versions {
+ assert(json.len > 0);
+
+ var parsed = try std.json.parseFromSlice(std.json.Value, allocator, json, .{});
+ defer parsed.deinit();
+
+ const root = switch (parsed.value) {
+ .object => |o| o,
+ else => return error.MalformedIndex,
+ };
+
+ var list: std.ArrayList(Version) = .empty;
+ errdefer {
+ for (list.items) |v| {
+ allocator.free(v.name);
+ allocator.free(v.docs_url);
+ }
+ list.deinit(allocator);
+ }
+
+ var it = root.iterator();
+ while (it.next()) |entry| {
+ if (list.items.len >= max_versions) break;
+
+ const obj = switch (entry.value_ptr.*) {
+ .object => |o| o,
+ else => continue,
+ };
+ const docs = switch (obj.get("docs") orelse continue) {
+ .string => |s| s,
+ else => continue,
+ };
+ if (docs.len == 0) continue;
+
+ const name = try allocator.dupe(u8, entry.key_ptr.*);
+ errdefer allocator.free(name);
+ const url = try allocator.dupe(u8, docs);
+ try list.append(allocator, .{ .name = name, .docs_url = url });
+ }
+
+ return .{ .allocator = allocator, .items = try list.toOwnedSlice(allocator) };
+}
+
+test "parses versions with docs urls and skips entries without" {
+ const gpa = std.testing.allocator;
+ const json =
+ \\{
+ \\ "master": { "version": "0.16.0-dev", "docs": "https://ziglang.org/documentation/master/" },
+ \\ "0.15.2": { "docs": "https://ziglang.org/documentation/0.15.2/" },
+ \\ "0.1.0": { "notes": "no docs here" }
+ \\}
+ ;
+ var versions = try parse(gpa, json);
+ defer versions.deinit();
+
+ try std.testing.expectEqual(@as(usize, 2), versions.items.len);
+ var saw_master = false;
+ for (versions.items) |v| {
+ try std.testing.expect(v.docs_url.len > 0);
+ if (std.mem.eql(u8, v.name, "master")) saw_master = true;
+ }
+ try std.testing.expect(saw_master);
+}