Skip to content

Commit 885ad4a

Browse files
committed
parse HTTP version and use it in curl requests
1 parent b7d0118 commit 885ad4a

File tree

5 files changed

+280
-3
lines changed

5 files changed

+280
-3
lines changed

'

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
const std = @import("std");
2+
const http = std.http;
3+
4+
const Allocator = std.mem.Allocator;
5+
const ArrayList = std.ArrayList;
6+
7+
const ParserState = enum { headers, body };
8+
9+
pub const AssertionType = enum {
10+
equal,
11+
not_equal,
12+
contains,
13+
not_contains,
14+
starts_with,
15+
ends_with,
16+
matches_regex,
17+
not_matches_regex,
18+
19+
pub fn fromString(s: []const u8) ?AssertionType {
20+
if (std.ascii.eqlIgnoreCase(s, "==")) return .equal;
21+
if (std.ascii.eqlIgnoreCase(s, "equal")) return .equal;
22+
if (std.ascii.eqlIgnoreCase(s, "!=")) return .not_equal;
23+
if (std.ascii.eqlIgnoreCase(s, "contains")) return .contains;
24+
if (std.ascii.eqlIgnoreCase(s, "not_contains")) return .not_contains;
25+
if (std.ascii.eqlIgnoreCase(s, "starts_with")) return .starts_with;
26+
if (std.ascii.eqlIgnoreCase(s, "ends_with")) return .ends_with;
27+
if (std.ascii.eqlIgnoreCase(s, "matches_regex")) return .matches_regex;
28+
if (std.ascii.eqlIgnoreCase(s, "not_matches_regex")) return .not_matches_regex;
29+
return null;
30+
}
31+
};
32+
33+
pub const Assertion = struct {
34+
key: []const u8,
35+
value: []const u8,
36+
assertion_type: AssertionType,
37+
};
38+
39+
pub const HttpVersion = enum {
40+
@"1.0",
41+
@"1.1",
42+
@"2.0",
43+
@"3.0",
44+
};
45+
46+
pub const HttpRequest = struct {
47+
// TODO: Null HTTP METHOD SHOULD NOT BE CRITICAL TO PROPERLY PARSING
48+
method: ?http.Method,
49+
url: []const u8,
50+
headers: ArrayList(http.Header),
51+
body: ?[]u8,
52+
assertions: ArrayList(Assertion),
53+
version: HttpVersion,
54+
// TODO: Add a name for the request if needed.
55+
56+
pub fn init() HttpRequest {
57+
return .{
58+
.method = null,
59+
.url = "",
60+
.headers = .empty,
61+
.body = null,
62+
.assertions = .empty,
63+
.version = .@"1.0";
64+
};
65+
}
66+
67+
pub fn deinit(self: *HttpRequest, allocator: Allocator) void {
68+
if (self.url.len > 0) allocator.free(self.url);
69+
for (self.assertions.items) |assertion| {
70+
if (assertion.key.len > 0) allocator.free(assertion.key);
71+
if (assertion.value.len > 0) allocator.free(assertion.value);
72+
}
73+
self.assertions.deinit(allocator);
74+
for (self.headers.items) |header| {
75+
if (header.name.len > 0) allocator.free(header.name);
76+
if (header.value.len > 0) allocator.free(header.value);
77+
}
78+
self.headers.deinit(allocator);
79+
if (self.body) |body| {
80+
if (body.len > 0) allocator.free(body);
81+
}
82+
}
83+
};
84+
85+
pub fn parseFile(allocator: Allocator, file_path: []const u8) !ArrayList(HttpRequest) {
86+
const file = try std.fs.cwd().openFile(file_path, .{});
87+
defer file.close();
88+
const file_content = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
89+
defer allocator.free(file_content);
90+
return try parseContent(allocator, file_content);
91+
}
92+
93+
pub fn parseContent(allocator: Allocator, content: []const u8) !ArrayList(HttpRequest) {
94+
var requests: ArrayList(HttpRequest) = .empty;
95+
errdefer {
96+
for (requests.items) |*request| request.deinit(allocator);
97+
requests.deinit(allocator);
98+
}
99+
100+
var current_request = HttpRequest.init();
101+
errdefer current_request.deinit(allocator);
102+
var state: ?ParserState = null;
103+
var lines = std.mem.splitScalar(u8, content, '\n');
104+
var body_buffer: ArrayList(u8) = .empty;
105+
defer body_buffer.deinit(allocator);
106+
107+
while (lines.next()) |line| {
108+
const trimmed_line = std.mem.trim(u8, line, &std.ascii.whitespace);
109+
if (trimmed_line.len == 0) {
110+
if (state == .headers) state = .body;
111+
continue;
112+
}
113+
if (std.mem.startsWith(u8, trimmed_line, "###")) {
114+
if (current_request.method != null) {
115+
if (state == .body and body_buffer.items.len > 0) {
116+
current_request.body = try allocator.dupe(u8, body_buffer.items);
117+
body_buffer.clearRetainingCapacity();
118+
}
119+
try requests.append(allocator, current_request);
120+
current_request = HttpRequest.init();
121+
state = null;
122+
}
123+
continue;
124+
}
125+
if (std.mem.startsWith(u8, trimmed_line, "//#")) {
126+
// Assertion line
127+
var assertion_tokens = std.mem.tokenizeScalar(u8, std.mem.trim(u8, trimmed_line[3..], " "), ' ');
128+
const key = assertion_tokens.next() orelse return error.InvalidAssertionFormat;
129+
const type_str = assertion_tokens.next() orelse return error.InvalidAssertionFormat;
130+
const value = assertion_tokens.next() orelse return error.InvalidAssertionFormat;
131+
const assertion_type = AssertionType.fromString(type_str) orelse return error.InvalidAssertionFormat;
132+
const assertion = Assertion{
133+
.key = try allocator.dupe(u8, key),
134+
.value = try allocator.dupe(u8, value),
135+
.assertion_type = assertion_type,
136+
};
137+
if (assertion.key.len == 0 or assertion.value.len == 0) return error.InvalidAssertionFormat;
138+
try current_request.assertions.append(allocator, assertion);
139+
continue;
140+
}
141+
if (std.mem.startsWith(u8, trimmed_line, "#") or std.mem.startsWith(u8, trimmed_line, "//")) {
142+
continue;
143+
}
144+
if (state == null and current_request.method == null) {
145+
var tokens = std.mem.tokenizeScalar(u8, trimmed_line, ' ');
146+
const method_str = tokens.next() orelse return error.InvalidRequestMissingMethod;
147+
const url = tokens.next() orelse return error.InvalidRequestMissingURL;
148+
current_request.method = std.meta.stringToEnum(http.Method, method_str) orelse null;
149+
current_request.url = try allocator.dupe(u8, url);
150+
state = .headers;
151+
continue;
152+
}
153+
if (state == .headers) {
154+
if (std.mem.indexOf(u8, trimmed_line, ":")) |colon_pos| {
155+
const header_name = std.mem.trim(u8, trimmed_line[0..colon_pos], &std.ascii.whitespace);
156+
const header_value = std.mem.trim(u8, trimmed_line[colon_pos + 1 ..], &std.ascii.whitespace);
157+
try current_request.headers.append(allocator, http.Header{
158+
.name = try allocator.dupe(u8, header_name),
159+
.value = try allocator.dupe(u8, header_value),
160+
});
161+
} else {
162+
return error.InvalidHeaderFormat;
163+
}
164+
continue;
165+
}
166+
if (state == .body) {
167+
try body_buffer.appendSlice(allocator, trimmed_line);
168+
try body_buffer.append(allocator, '\n');
169+
continue;
170+
}
171+
}
172+
if (current_request.method != null) {
173+
if (state == .body and body_buffer.items.len > 0) {
174+
current_request.body = try allocator.dupe(u8, body_buffer.items);
175+
}
176+
try requests.append(allocator, current_request);
177+
}
178+
return requests;
179+
}
180+
181+
test "HttpParser from String Contents" {
182+
const test_http_contents =
183+
\\GET https://api.example.com
184+
\\Accept: */*
185+
\\Authorization: Bearer ABC123
186+
\\
187+
\\###
188+
\\
189+
\\POST https://api.example.com/users
190+
\\Accept: */*
191+
\\Authorization: Bearer ABC123
192+
\\
193+
\\{
194+
\\ "name": "John Doe",
195+
\\ "email": "John@Doe.com",
196+
\\}
197+
;
198+
199+
var requests = try parseContent(std.testing.allocator, test_http_contents);
200+
defer {
201+
for (requests.items) |*request| {
202+
request.deinit(std.testing.allocator);
203+
}
204+
requests.deinit(std.testing.allocator);
205+
}
206+
207+
try std.testing.expectEqual(http.Method.GET, requests.items[0].method);
208+
try std.testing.expectEqual(http.Method.POST, requests.items[1].method);
209+
try std.testing.expectEqualStrings("https://api.example.com", requests.items[0].url);
210+
try std.testing.expectEqualStrings("https://api.example.com/users", requests.items[1].url);
211+
try std.testing.expectEqualStrings("Authorization", requests.items[0].headers.items[1].name);
212+
try std.testing.expectEqualStrings("Bearer ABC123", requests.items[0].headers.items[1].value);
213+
try std.testing.expectEqualStrings("Authorization", requests.items[1].headers.items[1].name);
214+
try std.testing.expectEqualStrings("Bearer ABC123", requests.items[1].headers.items[1].value);
215+
try std.testing.expectEqual(0, (requests.items[0].body orelse "").len);
216+
try std.testing.expect(0 != (requests.items[1].body orelse "").len);
217+
}
218+
219+
test "HttpParser parses assertions" {
220+
const test_http_contents =
221+
\\GET https://api.example.com
222+
\\Accept: */*
223+
\\Authorization: Bearer ABC123
224+
\\
225+
\\//# status equal 200
226+
;
227+
228+
var requests = try parseContent(std.testing.allocator, test_http_contents);
229+
defer {
230+
for (requests.items) |*request| {
231+
request.deinit(std.testing.allocator);
232+
}
233+
requests.deinit(std.testing.allocator);
234+
}
235+
236+
try std.testing.expectEqual(http.Method.GET, requests.items[0].method);
237+
try std.testing.expectEqualStrings("https://api.example.com", requests.items[0].url);
238+
try std.testing.expectEqualStrings("status", requests.items[0].assertions.items[0].key);
239+
try std.testing.expectEqual(AssertionType.equal, requests.items[0].assertions.items[0].assertion_type);
240+
try std.testing.expectEqualStrings("200", requests.items[0].assertions.items[0].value);
241+
try std.testing.expectEqual(0, (requests.items[0].body orelse "").len);
242+
}

build.zig.zon

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
.hash = "regex-0.1.2-axC350bnAQCyrPkH2IEfri8hebJO6IujGY1rhcgZBXpL",
4646
},
4747
.curl = .{
48-
.url = "git+https://github.com/jiacai2050/zig-curl/?ref=HEAD#df369a2925a57446cb3147def4e11e6782452dbd",
49-
.hash = "curl-0.3.1-P4tT4X_LAAB-rzh6tsg_j9OSokOCTt6pK4DxFUYUwC7K",
48+
.url = "git+https://github.com/jiacai2050/zig-curl/?ref=HEAD#1a23d311582c1e72bdb9ec3020f82b8e2a4b8c8a",
49+
.hash = "curl-0.3.1-P4tT4cPNAAAZsLz5AsgitsBp3W0uVpoFT7IeNX2qOQlP",
5050
},
5151
},
5252
.paths = .{

src/httpfile/assertion_checker.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ test "Assertion checker with diagnostics - all pass" {
327327
.headers = .empty,
328328
.assertions = assertions,
329329
.body = null,
330+
.version = .@"HTTP/1.1",
330331
};
331332

332333
var response_headers = std.StringHashMap([]const u8).init(allocator);
@@ -381,6 +382,7 @@ test "Assertion checker with not_equal - all pass" {
381382
.headers = .empty,
382383
.assertions = assertions,
383384
.body = null,
385+
.version = .@"HTTP/1.1",
384386
};
385387

386388
var response_headers = std.StringHashMap([]const u8).init(allocator);
@@ -435,6 +437,7 @@ test "Assertion checker with failures - collects all failures" {
435437
.headers = .empty,
436438
.assertions = assertions,
437439
.body = null,
440+
.version = .@"HTTP/1.1",
438441
};
439442

440443
var response_headers = std.StringHashMap([]const u8).init(allocator);
@@ -493,6 +496,7 @@ test "HttpParser supports starts_with for status, body, and header" {
493496
.headers = .empty,
494497
.assertions = assertions,
495498
.body = null,
499+
.version = .@"HTTP/1.1",
496500
};
497501

498502
var response_headers = std.StringHashMap([]const u8).init(allocator);
@@ -556,6 +560,7 @@ test "HttpParser supports matches_regex and not_matches_regex for status, body,
556560
.headers = .empty,
557561
.assertions = assertions,
558562
.body = null,
563+
.version = .@"HTTP/1.1",
559564
};
560565

561566
var response_headers = std.StringHashMap([]const u8).init(allocator);
@@ -605,6 +610,7 @@ test "HttpParser supports contains and not_contains for headers" {
605610
.headers = .empty,
606611
.assertions = assertions,
607612
.body = null,
613+
.version = .@"HTTP/1.1",
608614
};
609615

610616
var response_headers = std.StringHashMap([]const u8).init(allocator);

src/httpfile/http_client.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ pub const HttpClient = struct {
7070
const easy = try curl.Easy.init(.{
7171
.ca_bundle = ca_bundle,
7272
});
73+
7374
defer easy.deinit();
7475

7576
var writer = std.Io.Writer.Allocating.init(self.allocator);
@@ -90,6 +91,20 @@ pub const HttpClient = struct {
9091

9192
const url = try self.allocator.dupeZ(u8, request.url);
9293
defer self.allocator.free(url);
94+
switch (request.version) {
95+
.@"HTTP/1.0" => {
96+
easy.setHttpVersion(.http1_0);
97+
},
98+
.@"HTTP/1.1" => {
99+
easy.setHttpVersion(.http1_1);
100+
},
101+
.@"HTTP/2" => {
102+
easy.setHttpVersion(.http2);
103+
},
104+
.@"HTTP/3" => {
105+
easy.setHttpVersion(.http3);
106+
},
107+
}
93108
const resp = try easy.fetch(url, .{
94109
.method = try map_method_for_curl(request.method orelse return error.RequestMethodNotSet),
95110
// TODO: Is it possible to remove the ptrCast?

src/httpfile/parser.zig

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,21 @@ pub const Assertion = struct {
3636
assertion_type: AssertionType,
3737
};
3838

39+
pub const HttpVersion = enum {
40+
@"HTTP/1.0",
41+
@"HTTP/1.1",
42+
@"HTTP/2",
43+
@"HTTP/3",
44+
};
45+
3946
pub const HttpRequest = struct {
4047
// TODO: Null HTTP METHOD SHOULD NOT BE CRITICAL TO PROPERLY PARSING
4148
method: ?http.Method,
4249
url: []const u8,
4350
headers: ArrayList(http.Header),
4451
body: ?[]u8,
4552
assertions: ArrayList(Assertion),
53+
version: HttpVersion,
4654
// TODO: Add a name for the request if needed.
4755

4856
pub fn init() HttpRequest {
@@ -52,6 +60,7 @@ pub const HttpRequest = struct {
5260
.headers = .empty,
5361
.body = null,
5462
.assertions = .empty,
63+
.version = .@"HTTP/1.1",
5564
};
5665
}
5766

@@ -136,8 +145,12 @@ pub fn parseContent(allocator: Allocator, content: []const u8) !ArrayList(HttpRe
136145
var tokens = std.mem.tokenizeScalar(u8, trimmed_line, ' ');
137146
const method_str = tokens.next() orelse return error.InvalidRequestMissingMethod;
138147
const url = tokens.next() orelse return error.InvalidRequestMissingURL;
148+
const version = tokens.next();
139149
current_request.method = std.meta.stringToEnum(http.Method, method_str) orelse null;
140150
current_request.url = try allocator.dupe(u8, url);
151+
if (version) |v| {
152+
current_request.version = std.meta.stringToEnum(HttpVersion, v) orelse return error.InvalidHttpVersion;
153+
}
141154
state = .headers;
142155
continue;
143156
}
@@ -171,7 +184,7 @@ pub fn parseContent(allocator: Allocator, content: []const u8) !ArrayList(HttpRe
171184

172185
test "HttpParser from String Contents" {
173186
const test_http_contents =
174-
\\GET https://api.example.com
187+
\\GET https://api.example.com HTTP/3
175188
\\Accept: */*
176189
\\Authorization: Bearer ABC123
177190
\\
@@ -199,6 +212,7 @@ test "HttpParser from String Contents" {
199212
try std.testing.expectEqual(http.Method.POST, requests.items[1].method);
200213
try std.testing.expectEqualStrings("https://api.example.com", requests.items[0].url);
201214
try std.testing.expectEqualStrings("https://api.example.com/users", requests.items[1].url);
215+
try std.testing.expectEqual(HttpVersion.@"HTTP/3", requests.items[0].version);
202216
try std.testing.expectEqualStrings("Authorization", requests.items[0].headers.items[1].name);
203217
try std.testing.expectEqualStrings("Bearer ABC123", requests.items[0].headers.items[1].value);
204218
try std.testing.expectEqualStrings("Authorization", requests.items[1].headers.items[1].name);

0 commit comments

Comments
 (0)