aboutsummaryrefslogtreecommitdiffstats
path: root/src/index.zig
blob: 7eb3e4be67acbe37f3144004d8cabf1495851d91 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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);
}