//! 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); }