Skip to content

Commit eff1341

Browse files
authored
Merge pull request #647 from lightpanda-io/form_data
add FormData web API
2 parents ddd35e3 + 265272b commit eff1341

File tree

2 files changed

+248
-0
lines changed

2 files changed

+248
-0
lines changed

src/browser/env.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const WebApis = struct {
3535
@import("storage/storage.zig").Interfaces,
3636
@import("url/url.zig").Interfaces,
3737
@import("xhr/xhr.zig").Interfaces,
38+
@import("xhr/form_data.zig").Interfaces,
3839
@import("xmlserializer/xmlserializer.zig").Interfaces,
3940
});
4041
};

src/browser/xhr/form_data.zig

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
21+
const Allocator = std.mem.Allocator;
22+
23+
const iterator = @import("../iterator/iterator.zig");
24+
const SessionState = @import("../env.zig").SessionState;
25+
26+
pub const Interfaces = .{
27+
FormData,
28+
KeyIterable,
29+
ValueIterable,
30+
EntryIterable,
31+
};
32+
33+
// We store the values in an ArrayList rather than a an
34+
// StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(),
35+
// values() and entries()) work. The FormData can contain duplicate keys, and
36+
// each iteration yields 1 key=>value pair. So, given:
37+
//
38+
// let f = new FormData();
39+
// f.append('a', '1');
40+
// f.append('a', '2');
41+
//
42+
// Then we'd expect f.keys(), f.values() and f.entries() to yield 2 results:
43+
// ['a', '1']
44+
// ['a', '2']
45+
//
46+
// This is much easier to do with an ArrayList than a HashMap, especially given
47+
// that the FormData could be mutated while iterating.
48+
// The downside is that most of the normal operations are O(N).
49+
50+
// https://xhr.spec.whatwg.org/#interface-formdata
51+
pub const FormData = struct {
52+
entries: std.ArrayListUnmanaged(Entry),
53+
54+
pub fn constructor() FormData {
55+
return .{
56+
.entries = .empty,
57+
};
58+
}
59+
60+
pub fn _get(self: *const FormData, key: []const u8) ?[]const u8 {
61+
const result = self.find(key) orelse return null;
62+
return result.entry.value;
63+
}
64+
65+
pub fn _getAll(self: *const FormData, key: []const u8, state: *SessionState) ![][]const u8 {
66+
const arena = state.call_arena;
67+
var arr: std.ArrayListUnmanaged([]const u8) = .empty;
68+
for (self.entries.items) |entry| {
69+
if (std.mem.eql(u8, key, entry.key)) {
70+
try arr.append(arena, entry.value);
71+
}
72+
}
73+
return arr.items;
74+
}
75+
76+
pub fn _has(self: *const FormData, key: []const u8) bool {
77+
return self.find(key) != null;
78+
}
79+
80+
// TODO: value should be a string or blog
81+
// TODO: another optional parameter for the filename
82+
pub fn _set(self: *FormData, key: []const u8, value: []const u8, state: *SessionState) !void {
83+
self._delete(key);
84+
return self._append(key, value, state);
85+
}
86+
87+
// TODO: value should be a string or blog
88+
// TODO: another optional parameter for the filename
89+
pub fn _append(self: *FormData, key: []const u8, value: []const u8, state: *SessionState) !void {
90+
const arena = state.arena;
91+
return self.entries.append(arena, .{ .key = try arena.dupe(u8, key), .value = try arena.dupe(u8, value) });
92+
}
93+
94+
pub fn _delete(self: *FormData, key: []const u8) void {
95+
var i: usize = 0;
96+
while (i < self.entries.items.len) {
97+
const entry = self.entries.items[i];
98+
if (std.mem.eql(u8, key, entry.key)) {
99+
_ = self.entries.swapRemove(i);
100+
} else {
101+
i += 1;
102+
}
103+
}
104+
}
105+
106+
pub fn _keys(self: *const FormData) KeyIterable {
107+
return .{ .inner = .{ .entries = &self.entries } };
108+
}
109+
110+
pub fn _values(self: *const FormData) ValueIterable {
111+
return .{ .inner = .{ .entries = &self.entries } };
112+
}
113+
114+
pub fn _entries(self: *const FormData) EntryIterable {
115+
return .{ .inner = .{ .entries = &self.entries } };
116+
}
117+
118+
pub fn _symbol_iterator(self: *const FormData) EntryIterable {
119+
return self._entries();
120+
}
121+
122+
const FindResult = struct {
123+
index: usize,
124+
entry: Entry,
125+
};
126+
127+
fn find(self: *const FormData, key: []const u8) ?FindResult {
128+
for (self.entries.items, 0..) |entry, i| {
129+
if (std.mem.eql(u8, key, entry.key)) {
130+
return .{ .index = i, .entry = entry };
131+
}
132+
}
133+
return null;
134+
}
135+
};
136+
137+
const Entry = struct {
138+
key: []const u8,
139+
value: []const u8,
140+
};
141+
142+
const KeyIterable = iterator.Iterable(KeyIterator, "FormDataKeyIterator");
143+
const ValueIterable = iterator.Iterable(ValueIterator, "FormDataValueIterator");
144+
const EntryIterable = iterator.Iterable(EntryIterator, "FormDataEntryIterator");
145+
146+
const KeyIterator = struct {
147+
index: usize = 0,
148+
entries: *const std.ArrayListUnmanaged(Entry),
149+
150+
pub fn _next(self: *KeyIterator) ?[]const u8 {
151+
const index = self.index;
152+
if (index == self.entries.items.len) {
153+
return null;
154+
}
155+
self.index += 1;
156+
return self.entries.items[index].key;
157+
}
158+
};
159+
160+
const ValueIterator = struct {
161+
index: usize = 0,
162+
entries: *const std.ArrayListUnmanaged(Entry),
163+
164+
pub fn _next(self: *ValueIterator) ?[]const u8 {
165+
const index = self.index;
166+
if (index == self.entries.items.len) {
167+
return null;
168+
}
169+
self.index += 1;
170+
return self.entries.items[index].value;
171+
}
172+
};
173+
174+
const EntryIterator = struct {
175+
index: usize = 0,
176+
entries: *const std.ArrayListUnmanaged(Entry),
177+
178+
pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } {
179+
const index = self.index;
180+
if (index == self.entries.items.len) {
181+
return null;
182+
}
183+
self.index += 1;
184+
const entry = self.entries.items[index];
185+
return .{ entry.key, entry.value };
186+
}
187+
};
188+
189+
const testing = @import("../../testing.zig");
190+
test "FormData" {
191+
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
192+
defer runner.deinit();
193+
194+
try runner.testCases(&.{
195+
.{ "let f = new FormData()", null },
196+
.{ "f.get('a')", "null" },
197+
.{ "f.has('a')", "false" },
198+
.{ "f.getAll('a')", "" },
199+
.{ "f.delete('a')", "undefined" },
200+
201+
.{ "f.set('a', 1)", "undefined" },
202+
.{ "f.has('a')", "true" },
203+
.{ "f.get('a')", "1" },
204+
.{ "f.getAll('a')", "1" },
205+
206+
.{ "f.append('a', 2)", "undefined" },
207+
.{ "f.has('a')", "true" },
208+
.{ "f.get('a')", "1" },
209+
.{ "f.getAll('a')", "1,2" },
210+
211+
.{ "f.append('b', '3')", "undefined" },
212+
.{ "f.has('a')", "true" },
213+
.{ "f.get('a')", "1" },
214+
.{ "f.getAll('a')", "1,2" },
215+
.{ "f.has('b')", "true" },
216+
.{ "f.get('b')", "3" },
217+
.{ "f.getAll('b')", "3" },
218+
219+
.{ "let acc = [];", null },
220+
.{ "for (const key of f.keys()) { acc.push(key) }; acc;", "a,a,b" },
221+
222+
.{ "acc = [];", null },
223+
.{ "for (const value of f.values()) { acc.push(value) }; acc;", "1,2,3" },
224+
225+
.{ "acc = [];", null },
226+
.{ "for (const entry of f.entries()) { acc.push(entry) }; acc;", "a,1,a,2,b,3" },
227+
228+
.{ "acc = [];", null },
229+
.{ "for (const entry of f) { acc.push(entry) }; acc;", "a,1,a,2,b,3" },
230+
231+
.{ "f.delete('a')", "undefined" },
232+
.{ "f.has('a')", "false" },
233+
.{ "f.has('b')", "true" },
234+
235+
.{ "acc = [];", null },
236+
.{ "for (const key of f.keys()) { acc.push(key) }; acc;", "b" },
237+
238+
.{ "acc = [];", null },
239+
.{ "for (const value of f.values()) { acc.push(value) }; acc;", "3" },
240+
241+
.{ "acc = [];", null },
242+
.{ "for (const entry of f.entries()) { acc.push(entry) }; acc;", "b,3" },
243+
244+
.{ "acc = [];", null },
245+
.{ "for (const entry of f) { acc.push(entry) }; acc;", "b,3" },
246+
}, .{});
247+
}

0 commit comments

Comments
 (0)