diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ef271d2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + test-server: + image: python:3.15-rc-slim + command: > + sh -c "mkdir mock_api && + echo '{\"message\":\"hello\"}' > mock_api/hello && + echo 'hello' > mock_api/hello-raw && + python -m http.server 8080 -d mock_api + " + ports: + - "8080:8080" diff --git a/src/http_utils.zig b/src/http_utils.zig deleted file mode 100644 index 15c54a2..0000000 --- a/src/http_utils.zig +++ /dev/null @@ -1,76 +0,0 @@ -const std = @import("std"); - -const HttpError = error{UnexpectedStatus}; - -pub fn getResponse(comptime T: type, allocator: std.mem.Allocator, url: []const u8) !std.json.Parsed(T) { - // Set up single threaded context - var threaded: std.Io.Threaded = .init_single_threaded; - const io: std.Io = threaded.io(); - - // Create a client - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - //Get the endpoint - const endpoint = try std.Uri.parse(url); - var request = try client.request(.GET, endpoint, .{}); - defer request.deinit(); - - try request.sendBodiless(); - var response = try request.receiveHead(&.{}); - - var transfer_buffer: [64]u8 = undefined; - var decompress: std.http.Decompress = undefined; - var decompress_buffer: [std.compress.flate.max_window_len]u8 = undefined; - const response_reader = response.readerDecompressing( - transfer_buffer[0..], - &decompress, - decompress_buffer[0..], - ); - - var json_reader: std.json.Reader = .init(allocator, response_reader); - defer json_reader.deinit(); - - const parsed: std.json.Parsed(T) = try std.json.parseFromTokenSource( - T, - allocator, - &json_reader, - .{ - .ignore_unknown_fields = true, - .allocate = .alloc_always, - }, - ); - - if (response.head.status.class() != .success) { - return HttpError.UnexpectedStatus; - } - - return parsed; -} - -test "Test getQuoteResponse" { - const QuoteResponse = struct { - quote: struct { - author: struct { - bio: []u8, - description: []u8, - id: []u8, - link: []u8, - name: []u8, - slug: []u8, - }, - content: []u8, - id: []u8, - tags: []struct { - id: []u8, - name: []u8, - }, - }, - }; - const allocator = std.testing.allocator; - const url: []const u8 = "https://api.quotable.kurokeita.dev/api/quotes/random"; - const parsed_quote = try getResponse(QuoteResponse, allocator, url[0..]); - defer parsed_quote.deinit(); - - std.debug.print("{s}", .{parsed_quote.value.quote.id}); -} diff --git a/src/http_utils/http_client_wrapper.zig b/src/http_utils/http_client_wrapper.zig new file mode 100644 index 0000000..cebbd03 --- /dev/null +++ b/src/http_utils/http_client_wrapper.zig @@ -0,0 +1,155 @@ +const std = @import("std"); + +const HttpError = error{UnexpectedStatus}; +const HttpClientWrapper = @This(); + +client: std.http.Client, + +const ResponseOptions = struct { + token: ?[]u8 = null, + extra_headers: []const std.http.Header = &.{}, +}; + +// The caller is in charge of freeing the memory +pub fn makeAuthorizationHeaderFromToken(allocator: std.mem.Allocator, token: []const u8) ![]u8 { + return try std.fmt.allocPrint(allocator, "Authorization: Bearer {s}", .{token}); +} + +// Get raw response +pub fn getRawResponse( + client_wrapper: *HttpClientWrapper, + allocator: std.mem.Allocator, + url: []const u8, + response_options: ResponseOptions, +) ![]u8 { + var auth_header: []u8 = undefined; + + defer allocator.free(auth_header); + if (response_options.token) |token| { + auth_header = try makeAuthorizationHeaderFromToken(allocator, token); + } else { + auth_header = ""; + } + + //Get the endpoint + const endpoint = try std.Uri.parse(url); + var request = try client_wrapper.client.request(.GET, endpoint, .{ + .headers = .{ .authorization = .{ .override = auth_header } }, + .extra_headers = response_options.extra_headers, + }); + + defer request.deinit(); + + try request.sendBodiless(); + var response = try request.receiveHead(&.{}); + + var transfer_buffer: [64]u8 = undefined; + var decompress: std.http.Decompress = undefined; + var decompress_buffer: [std.compress.flate.max_window_len]u8 = undefined; + const response_reader = response.readerDecompressing( + transfer_buffer[0..], + &decompress, + decompress_buffer[0..], + ); + + if (response.head.status.class() != .success) { + return HttpError.UnexpectedStatus; + } + + return response_reader.allocRemaining(allocator, .unlimited); +} + +pub fn getResponse( + client_wrapper: *HttpClientWrapper, + comptime T: type, + allocator: std.mem.Allocator, + url: []const u8, +) !std.json.Parsed(T) { + //Get the endpoint + const endpoint = try std.Uri.parse(url); + var request = try client_wrapper.client.request(.GET, endpoint, .{}); + defer request.deinit(); + + try request.sendBodiless(); + var response = try request.receiveHead(&.{}); + + var transfer_buffer: [64]u8 = undefined; + var decompress: std.http.Decompress = undefined; + var decompress_buffer: [std.compress.flate.max_window_len]u8 = undefined; + const response_reader = response.readerDecompressing( + transfer_buffer[0..], + &decompress, + decompress_buffer[0..], + ); + + if (response.head.status.class() != .success) { + return HttpError.UnexpectedStatus; + } + + var json_reader: std.json.Reader = .init(allocator, response_reader); + defer json_reader.deinit(); + + const parsed: std.json.Parsed(T) = try std.json.parseFromTokenSource( + T, + allocator, + &json_reader, + .{ + .ignore_unknown_fields = true, + .allocate = .alloc_always, + }, + ); + return parsed; +} + +pub fn deinit(client_wrapper: *HttpClientWrapper) void { + client_wrapper.client.deinit(); +} + +// Tests + +test "makeAuthorizationHeader makes the right header" { + const token = "Some Bearer Token"; + const allocator = std.testing.allocator; + + const header = try makeAuthorizationHeaderFromToken(allocator, token); + defer allocator.free(header); + + try std.testing.expect(std.mem.eql(u8, header, "Authorization: Bearer Some Bearer Token")); +} + +test "Test getResponse" { + const io = std.Io.Threaded.global_single_threaded.io(); + const allocator = std.testing.allocator; + + const HelloMessage = struct { message: []u8 }; + + const http_client: std.http.Client = .{ .allocator = allocator, .io = io }; + var test_client: HttpClientWrapper = .{ .client = http_client }; + defer test_client.deinit(); + + const parsed = try test_client.getResponse( + HelloMessage, + allocator, + "http://localhost:8080/hello", + ); + defer parsed.deinit(); + try std.testing.expect(std.mem.eql(u8, parsed.value.message, "hello")); +} + +test "Test getRawResponse" { + const io = std.Io.Threaded.global_single_threaded.io(); + const allocator = std.testing.allocator; + + const http_client: std.http.Client = .{ .allocator = allocator, .io = io }; + var test_client: HttpClientWrapper = .{ .client = http_client }; + defer test_client.deinit(); + + const response = try test_client.getRawResponse( + allocator, + "http://localhost:8080/hello-raw", + .{}, + ); + defer allocator.free(response); + + try std.testing.expect(std.mem.eql(u8, response, "hello\n")); +}