Skip to content

Commit f704015

Browse files
committed
url: implement query parsing
1 parent e42b03a commit f704015

File tree

3 files changed

+226
-0
lines changed

3 files changed

+226
-0
lines changed

src/run_tests.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Window = @import("html/window.zig").Window;
1111
const xhr = @import("xhr/xhr.zig");
1212
const storage = @import("storage/storage.zig");
1313
const url = @import("url/url.zig");
14+
const urlquery = @import("url/query.zig");
1415

1516
const documentTestExecFn = @import("dom/document.zig").testExecFn;
1617
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
@@ -278,6 +279,9 @@ test {
278279

279280
const cssLibdomTest = @import("css/libdom_test.zig");
280281
std.testing.refAllDecls(cssLibdomTest);
282+
283+
const queryTest = @import("url/query.zig");
284+
std.testing.refAllDecls(queryTest);
281285
}
282286

283287
fn testJSRuntime(alloc: std.mem.Allocator) !void {

src/url/query.zig

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const std = @import("std");
2+
3+
const Reader = @import("../str/parser.zig").Reader;
4+
5+
// Values is a map with string key of string values.
6+
pub const Values = struct {
7+
alloc: std.mem.Allocator,
8+
map: std.StringArrayHashMapUnmanaged(List),
9+
10+
const List = std.ArrayListUnmanaged([]const u8);
11+
12+
pub fn init(alloc: std.mem.Allocator) Values {
13+
return .{
14+
.alloc = alloc,
15+
.map = .{},
16+
};
17+
}
18+
19+
pub fn deinit(self: *Values) void {
20+
var it = self.map.iterator();
21+
while (it.next()) |entry| {
22+
for (entry.value_ptr.items) |v| self.alloc.free(v);
23+
entry.value_ptr.deinit(self.alloc);
24+
self.alloc.free(entry.key_ptr.*);
25+
}
26+
self.map.deinit(self.alloc);
27+
}
28+
29+
// add the key value couple to the values.
30+
// the key and the value are duplicated.
31+
pub fn append(self: *Values, k: []const u8, v: []const u8) !void {
32+
const vv = try self.alloc.dupe(u8, v);
33+
34+
if (self.map.getPtr(k)) |list| {
35+
return try list.append(self.alloc, vv);
36+
}
37+
38+
const kk = try self.alloc.dupe(u8, k);
39+
var list = List{};
40+
try list.append(self.alloc, vv);
41+
try self.map.put(self.alloc, kk, list);
42+
}
43+
44+
pub fn get(self: *Values, k: []const u8) [][]const u8 {
45+
if (self.map.get(k)) |list| {
46+
return list.items;
47+
}
48+
49+
return &[_][]const u8{};
50+
}
51+
52+
pub fn first(self: *Values, k: []const u8) []const u8 {
53+
if (self.map.getPtr(k)) |list| {
54+
if (list.items.len == 0) return "";
55+
return list.items[0];
56+
}
57+
58+
return "";
59+
}
60+
61+
pub fn delete(self: *Values, k: []const u8) void {
62+
if (self.map.getPtr(k)) |list| {
63+
list.deinit(self.alloc);
64+
_ = self.map.fetchSwapRemove(k);
65+
}
66+
}
67+
68+
pub fn deleteValue(self: *Values, k: []const u8, v: []const u8) void {
69+
const list = self.map.getPtr(k) orelse return;
70+
71+
var i: usize = 0;
72+
while (i < list.items.len) {
73+
if (std.mem.eql(u8, v, list.items[i])) {
74+
_ = list.swapRemove(i);
75+
return;
76+
}
77+
i += 1;
78+
}
79+
}
80+
81+
pub fn count(self: *Values) usize {
82+
return self.map.count();
83+
}
84+
};
85+
86+
// Parse the given query.
87+
pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values {
88+
var values = Values.init(alloc);
89+
errdefer values.deinit();
90+
91+
const ln = s.len;
92+
if (ln == 0) return values;
93+
94+
var r = Reader{ .s = s };
95+
while (true) {
96+
const param = r.until('&');
97+
if (param.len == 0) break;
98+
99+
var rr = Reader{ .s = param };
100+
const k = rr.until('=');
101+
if (k.len == 0) continue;
102+
103+
_ = rr.skip();
104+
const v = rr.tail();
105+
106+
// TODO decode k and v
107+
108+
try values.append(k, v);
109+
110+
if (!r.skip()) break;
111+
}
112+
113+
return values;
114+
}
115+
116+
test "parse empty query" {
117+
var values = try parseQuery(std.testing.allocator, "");
118+
defer values.deinit();
119+
120+
try std.testing.expect(values.count() == 0);
121+
}
122+
123+
test "parse empty query &" {
124+
var values = try parseQuery(std.testing.allocator, "&");
125+
defer values.deinit();
126+
127+
try std.testing.expect(values.count() == 0);
128+
}
129+
130+
test "parse query" {
131+
var values = try parseQuery(std.testing.allocator, "a=b&b=c");
132+
defer values.deinit();
133+
134+
try std.testing.expect(values.count() == 2);
135+
try std.testing.expect(values.get("a").len == 1);
136+
try std.testing.expect(std.mem.eql(u8, values.get("a")[0], "b"));
137+
try std.testing.expect(std.mem.eql(u8, values.first("a"), "b"));
138+
139+
try std.testing.expect(values.get("b").len == 1);
140+
try std.testing.expect(std.mem.eql(u8, values.get("b")[0], "c"));
141+
try std.testing.expect(std.mem.eql(u8, values.first("b"), "c"));
142+
}
143+
144+
test "parse query no value" {
145+
var values = try parseQuery(std.testing.allocator, "a");
146+
defer values.deinit();
147+
148+
try std.testing.expect(values.count() == 1);
149+
try std.testing.expect(std.mem.eql(u8, values.first("a"), ""));
150+
}
151+
152+
test "parse query dup" {
153+
var values = try parseQuery(std.testing.allocator, "a=b&a=c");
154+
defer values.deinit();
155+
156+
try std.testing.expect(values.count() == 1);
157+
try std.testing.expect(std.mem.eql(u8, values.first("a"), "b"));
158+
try std.testing.expect(values.get("a").len == 2);
159+
}

src/url/url.zig

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const Case = jsruntime.test_utils.Case;
55
const checkCases = jsruntime.test_utils.checkCases;
66
const generate = @import("../generate.zig");
77

8+
const query = @import("query.zig");
9+
810
pub const Interfaces = generate.Tuple(.{
911
URL,
1012
URLSearchParams,
@@ -27,6 +29,7 @@ pub const Interfaces = generate.Tuple(.{
2729
pub const URL = struct {
2830
rawuri: []const u8,
2931
uri: std.Uri,
32+
search_params: URLSearchParams,
3033

3134
pub const mem_guarantied = true;
3235

@@ -41,10 +44,12 @@ pub const URL = struct {
4144
return .{
4245
.rawuri = raw,
4346
.uri = uri,
47+
.search_params = try URLSearchParams.constructor(alloc, uri.query),
4448
};
4549
}
4650

4751
pub fn deinit(self: *URL, alloc: std.mem.Allocator) void {
52+
self.search_params.deinit();
4853
alloc.free(self.rawuri);
4954
}
5055

@@ -125,14 +130,57 @@ pub const URL = struct {
125130
return try std.mem.concat(alloc, u8, &[_][]const u8{ "#", self.uri.fragment.? });
126131
}
127132

133+
pub fn get_searchParams(self: *URL) *URLSearchParams {
134+
return &self.search_params;
135+
}
136+
128137
pub fn _toJSON(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
129138
return try self.get_href(alloc);
130139
}
131140
};
132141

133142
// https://url.spec.whatwg.org/#interface-urlsearchparams
143+
// TODO array like
134144
pub const URLSearchParams = struct {
145+
values: query.Values,
146+
135147
pub const mem_guarantied = true;
148+
149+
pub fn constructor(alloc: std.mem.Allocator, init: ?[]const u8) !URLSearchParams {
150+
return .{
151+
.values = try query.parseQuery(alloc, init orelse ""),
152+
};
153+
}
154+
155+
pub fn deinit(self: *URLSearchParams, _: std.mem.Allocator) void {
156+
self.values.deinit();
157+
}
158+
159+
pub fn get_size(self: *URLSearchParams) u32 {
160+
return @intCast(self.values.count());
161+
}
162+
163+
pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8) !void {
164+
try self.values.append(name, value);
165+
}
166+
167+
pub fn _delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) !void {
168+
if (value) |v| return self.values.deleteValue(name, v);
169+
170+
self.values.delete(name);
171+
}
172+
173+
pub fn _get(self: *URLSearchParams, name: []const u8) ?[]const u8 {
174+
return self.values.first(name);
175+
}
176+
177+
// TODO return generates an error: caught unexpected error 'TypeLookup'
178+
// pub fn _getAll(self: *URLSearchParams, name: []const u8) [][]const u8 {
179+
// try self.values.get(name);
180+
// }
181+
182+
// TODO
183+
pub fn _sort(_: *URLSearchParams) void {}
136184
};
137185

138186
// Tests
@@ -154,6 +202,21 @@ pub fn testExecFn(
154202
.{ .src = "url.pathname", .ex = "/path" },
155203
.{ .src = "url.search", .ex = "?query" },
156204
.{ .src = "url.hash", .ex = "#fragment" },
205+
.{ .src = "url.searchParams.get('query')", .ex = "" },
157206
};
158207
try checkCases(js_env, &url);
208+
209+
var qs = [_]Case{
210+
.{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E')", .ex = "undefined" },
211+
.{ .src = "url.searchParams.get('a')", .ex = "~" },
212+
.{ .src = "url.searchParams.get('b')", .ex = "~" },
213+
.{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" },
214+
.{ .src = "url.searchParams.get('c')", .ex = "foo" },
215+
.{ .src = "url.searchParams.size", .ex = "3" },
216+
.{ .src = "url.searchParams.delete('c', 'foo')", .ex = "undefined" },
217+
.{ .src = "url.searchParams.get('c')", .ex = "" },
218+
.{ .src = "url.searchParams.delete('a')", .ex = "undefined" },
219+
.{ .src = "url.searchParams.get('a')", .ex = "" },
220+
};
221+
try checkCases(js_env, &qs);
159222
}

0 commit comments

Comments
 (0)