A wrapper around std.http.Client to make it easier to create requests and consume responses.

// The client is thread-safe
var client = zul.http.Client.init(allocator);
defer client.deinit();

// Not thread safe, method defaults to .GET
var req = try client.request("https://api.github.com/search/topics");
defer req.deinit();

// Set the querystring, can also be set in the URL passed to client.request
// or a mix of setting in client.request and programmatically via req.query
try req.query("q", "zig");

try req.header("Authorization", "Your Token");

// The lifetime of res is tied to req
var res = try req.getResponse(.{});
if (res.status != 200) {
	// TODO: handle error
	return;
}

// On success, this is a zul.Managed(SearchResult), its lifetime is detached
// from the req, allowing it to outlive req.
const managed = try res.json(SearchResult, allocator, .{});

// Parsing the JSON and creating SearchResult [probably] required some allocations.
// Internally an arena was created to manage this from the allocator passed to
// res.json.
defer managed.deinit();

const search_result = managed.value;

zul.http.Client is wrapper around std.http.Client. Is is thread-safe and its only purpose is to create zul.http.Request objects.

client: std.http.Client

Should only be used if tweaks to the underlying std.http.Client are needed.

Releases all memory associated with the client. The client should not be used after this is called.

Creates a Request object, using the provided url. The Request will use the Client's allocator. If a querystring is provided as part of the url, it must be properly encoded. Use query(name, value) to add or append a querystring which the library will encode.

fn allocRequest(
  self: *Client,

  // With the plain request() method, the client's allocator is used. A different
  // allocator can be used with this variant.
  allocator: Allocator,

  url: []const u8,

) !Request

Creates a Request object, using the provided url. The Request will use the provided Allocator.

zul.http.Request is used to build the request (querystring, headers, body) and issue the request to get a Response. A Request is not thread-safe. To get a request, use the client.request method.

headers: std.http.Headers

Gives direct access to the request headers. To add headers, prefer using the header method.

method: std.http.Method

Defaults to .GET.

url: zul.StringBuilder

Gives direct access to the URL. For manipulating the querystring, prefer using the query method.

Releases all memory associated with the request as well as any generated response.

Sets the request body to the given value.

Builds a URL encoded body. Can be called multiple times. The first call will se the Content-Typeheader to application/x-www-form-urlencoded.

Adds the given name-value pair to the request headers.

Appends the given name-value pair to the request's querystring. Both the name and value will be automatically encoded if needed. The querystring can also be set when first creating the request as part of the specified URL. It is allowed to set some querystring values via the original URLs (which must be encoded by the caller) and others via this method.

Will send the contents of the file as the body of the request. file_path can be absolute or relative.

fn getResponse(
	req: *Request,

	opts: .{
		// whether or not to parse the respons headers
		.response_headers: bool = true,

		.write_progress_state: *anyopaque = undefined,

		.write_progress: ?*const fn(total: usize, written: usize, state: *anyopaque) void = null,
	}
) !Response

Issues the requests and, on success, returns the Response.

The write_progress option field is a callback that will be called as the file body is uploaded. An optional state can be specified via the write_progress_state the option field which is passed into the callback.

var res = try req.getResponse(.{
	.write_progress = uploadProgress
});

// ...

fn uploadProgress(total: usize, written: usize, state: *anyopaque) void {
	// It is an undefined behavior to try to access the state
	// when `write_progress_state` was not specified.
	_ = state;

	std.fmt.print("Written {d} of {d}", {written, total});
}

Or, with state:

// ProgressTracker can be anything, it's specific to your app
var tracker = ProgressTracker{};

var res = try req.getResponse(.{
	.write_progress = uploadProgress
	.write_progress_state = &tracker,
});

// ...

fn uploadProgress(total: usize, written: usize, state: *anyopaque) void {
	var tracker: *ProgressTracker = @alignCast(@ptrCast(state));
	// use tracker however you want, it's your class!
}

zul.http.Response lifetime is tied to the initiating request. Therefore, it has no deinit method. When request.deinit is called, the response is no longer valid. Note however that the methods for reading the body detach the body from this lifetime.

headers: std.StringHashMap([]const u8)

The response headrs. Only populated if the response_headers option is specified in getResponse (this option defaults to true).

req: *std.http.Client.Request

The underlying request.

res: *std.http.Client.Response

The underlying response.

status: u16

The response's HTTP status code.

Returns the value associated with the given header name, if any. The name is case-insensitive. Only populated if the response_headers option is specified in getResponse (this option defaults to true).

fn json(
	self: Response,

	// The type to parse into
	comptime T: type,

	// An arena allocator will be created from this allocator for any memory needed
	// to parse the JSON and create T
	allocator: std.mem.Allocator,

	// Consider setting ignore_unknown_fields = true
	// and the max_value_len
	opts: std.json.ParseOptions

) !zul.Managed(T)

Attempts to parse the body as JSON. On success, the returned object has its own lifetime, independent of the initiating request or response.

zul.Manage is a renamed std.json.Parsed(T) (I dislike the name std.json.Parsed(T) because it represents data and behavior that has nothing to with with JSON or parsing).

fn allocBody (
  self: *Response,

  // Allocator will be used to create the []const u8 that will hold the body
  // If the response has a content-length, then exactly $content_length bytes will
  // be allocated
  allocator: std.mem.Allocator,

  // {.max_size = usize, .buffer_size: usize}
  opts: zul.StringBuilder.FromReaderOpts,

) !zul.StringBuilder

Reads the body into a zul.StringBuilder. Consider setting the max_size field of opts to a reasonable value.

This method returns a zul.StringBuilder to support chunked-encoding responses where the length isn't known ahead of time and a growable buffer is needed. In such cases, a correctly sized []const u8 cannot be returned without doing an additional copy. zul.StringBuilder is preferred over std.ArrayList(u8) because of its more efficient ability to read from a std.io.Reader.