Skip to content

Custom Providers

Create fully custom providers for any Git hosting solution.

Creating a Custom Provider

zig
const myProvider = updater.providers.custom(
    "my-git-server",       // Provider name
    buildMyUrl,            // URL builder function
    parseMyResponse,       // Response parser function
    &myHeaders,            // HTTP headers
);

Required Functions

URL Builder

zig
fn buildMyUrl(
    allocator: std.mem.Allocator,
    owner: []const u8,
    repo: []const u8,
) std.mem.Allocator.Error![]const u8 {
    return std.fmt.allocPrint(
        allocator,
        "https://git.example.com/api/repos/{s}/{s}/latest",
        .{ owner, repo },
    );
}

Response Parser

zig
fn parseMyResponse(
    allocator: std.mem.Allocator,
    body: []const u8,
) updater.GitProvider.ParseError!updater.ReleaseInfo {
    const parsed = std.json.parseFromSlice(
        std.json.Value,
        allocator,
        body,
        .{},
    ) catch return error.InvalidJson;
    defer parsed.deinit();

    // Extract fields from JSON
    const root = parsed.value;
    if (root != .object) return error.InvalidFormat;

    const tag = extractString(root, "version") orelse return error.MissingField;
    const url = extractString(root, "url") orelse return error.MissingField;

    return updater.ReleaseInfo{
        .tag = try allocator.dupe(u8, tag),
        .url = try allocator.dupe(u8, url),
    };
}

fn extractString(value: std.json.Value, key: []const u8) ?[]const u8 {
    if (value != .object) return null;
    const field = value.object.get(key) orelse return null;
    if (field != .string) return null;
    return field.string;
}

Complete Example

zig
const std = @import("std");
const updater = @import("updater");

// Custom headers
const myHeaders = [_]updater.HttpHeader{
    .{ .name = "Accept", .value = "application/json" },
    .{ .name = "User-Agent", .value = "my-app/1.0" },
    .{ .name = "X-Api-Key", .value = "secret-key" },
};

fn buildMyUrl(
    allocator: std.mem.Allocator,
    owner: []const u8,
    repo: []const u8,
) std.mem.Allocator.Error![]const u8 {
    return std.fmt.allocPrint(
        allocator,
        "https://releases.example.com/api/v2/{s}/{s}/current",
        .{ owner, repo },
    );
}

fn parseMyResponse(
    allocator: std.mem.Allocator,
    body: []const u8,
) updater.GitProvider.ParseError!updater.ReleaseInfo {
    // Parse custom JSON format:
    // {"version": "1.2.3", "download": "https://...", "notes": "..."}

    const parsed = std.json.parseFromSlice(
        std.json.Value,
        allocator,
        body,
        .{},
    ) catch return error.InvalidJson;
    defer parsed.deinit();

    const root = parsed.value;
    if (root != .object) return error.InvalidFormat;

    // Required: version
    const version_field = root.object.get("version") orelse return error.MissingField;
    if (version_field != .string) return error.InvalidFormat;
    const tag = allocator.dupe(u8, version_field.string) catch return error.OutOfMemory;
    errdefer allocator.free(tag);

    // Required: download URL
    const url_field = root.object.get("download") orelse return error.MissingField;
    if (url_field != .string) return error.InvalidFormat;
    const url = allocator.dupe(u8, url_field.string) catch return error.OutOfMemory;
    errdefer allocator.free(url);

    // Optional: notes
    var body_text: ?[]const u8 = null;
    if (root.object.get("notes")) |notes| {
        if (notes == .string) {
            body_text = allocator.dupe(u8, notes.string) catch return error.OutOfMemory;
        }
    }

    return updater.ReleaseInfo{
        .tag = tag,
        .url = url,
        .body = body_text,
    };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const myProvider = updater.providers.custom(
        "my-releases",
        buildMyUrl,
        parseMyResponse,
        &myHeaders,
    );

    const result = try updater.checkForUpdates(allocator, .{
        .provider = myProvider,
        .owner = "company",
        .repo = "product",
        .current_version = "1.0.0",
    });

    if (result.has_update) {
        std.debug.print("Update: {s}\n", .{result.latest_version.?});
    }
}

Error Handling

Your parser should return appropriate errors:

zig
pub const ParseError = error{
    InvalidJson,    // JSON parsing failed
    MissingField,   // Required field not present
    InvalidFormat,  // Field has wrong type
    OutOfMemory,    // Allocation failed
};

Memory Management

Important: All strings returned in ReleaseInfo must be allocated using the provided allocator. The caller will free them using ReleaseInfo.deinit().

zig
// Good: allocate copies
const tag = allocator.dupe(u8, raw_tag) catch return error.OutOfMemory;

// Bad: returning slice from parsed JSON (will be freed)
const tag = raw_tag;  // DON'T DO THIS

Testing

Test your provider with mock responses:

zig
test "custom provider parses response" {
    const allocator = std.testing.allocator;

    const mock_response =
        \\{"version": "2.0.0", "download": "https://example.com/v2.0.0"}
    ;

    var info = try parseMyResponse(allocator, mock_response);
    defer info.deinit(allocator);

    try std.testing.expectEqualStrings("2.0.0", info.tag);
}

Released under the MIT License.