diff --git a/src/http/middleware.zig b/src/http/middleware.zig index 190a60d..541ca97 100644 --- a/src/http/middleware.zig +++ b/src/http/middleware.zig @@ -163,27 +163,12 @@ test "InjectContextValue" { .handle(.{}, .{}, .{ .efgh = @as(usize, 10) }, ExpectContext(.{ .abcd = 5, .efgh = 10 }){}); } -fn expectDeepEquals(expected: anytype, actual: anytype) !void { - const E = @TypeOf(expected); - const A = @TypeOf(actual); - if (E == void) return std.testing.expect(A == void); - try std.testing.expect(std.meta.fields(E).len == std.meta.fields(A).len); - inline for (std.meta.fields(E)) |f| { - const e = @field(expected, f.name); - const a = @field(actual, f.name); - if (comptime std.meta.trait.isZigString(f.field_type)) { - try std.testing.expectEqualStrings(a, e); - } else { - try std.testing.expectEqual(a, e); - } - } -} - // Helper for testing purposes fn ExpectContext(comptime val: anytype) type { return struct { pub fn handle(_: @This(), _: anytype, _: anytype, ctx: anytype, _: void) !void { - try expectDeepEquals(val, ctx); + if (@TypeOf(val) == void) return error.TestUnexpectedResult; + try util.testing.expectDeepEqual(@as(@TypeOf(ctx), val), ctx); } }; } @@ -485,7 +470,15 @@ pub fn mount(comptime route: []const u8) Mount(route) { test "mount" { const testCase = struct { fn func(comptime base: []const u8, request: []const u8, comptime expected: ?[]const u8) !void { - const result = mount(base).handle(.{}, .{}, addField(.{}, "path", request), expectContext(.{ .path = expected orelse "" })); + const result = mount(base).handle( + .{}, + .{}, + addField(.{}, "path", request), + expectContext(.{ + .path = expected orelse "", + .mounted_at = std.mem.trim(u8, base, "/"), + }), + ); try if (expected != null) result else std.testing.expectError(error.RouteMismatch, result); } }.func; @@ -590,7 +583,8 @@ test "ParsePathArgs" { expected: @TypeOf(expected), path: []const u8, fn handle(self: @This(), _: anytype, _: anytype, ctx: anytype, _: void) !void { - try expectDeepEquals(self.expected, ctx.args); + if (@TypeOf(expected) == @TypeOf(null)) return error.TestUnexpectedResult; + try util.testing.expectDeepEqual(@as(@TypeOf(ctx.args), self.expected), ctx.args); try std.testing.expectEqualStrings(self.path, ctx.path); } }{ .expected = expected, .path = path }; @@ -619,10 +613,10 @@ test "ParsePathArgs" { try testCase("/:foo*", struct { foo: []const u8 }, "/", .{ .foo = "/" }); try testCase("/:foo*", struct { foo: []const u8 }, "", .{ .foo = "" }); - try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/", .{})); - try std.testing.expectError(error.RouteMismatch, testCase("/abcd/:id", struct { id: usize }, "/123", .{})); - try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/3/id/blahblah", .{ .id = 3 })); - try std.testing.expectError(error.InvalidCharacter, testCase("/:id", struct { id: usize }, "/xyz", .{})); + try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/", null)); + try std.testing.expectError(error.RouteMismatch, testCase("/abcd/:id", struct { id: usize }, "/123", null)); + try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/3/id/blahblah", null)); + try std.testing.expectError(error.InvalidCharacter, testCase("/:id", struct { id: usize }, "/xyz", null)); } const BaseContentType = enum { @@ -715,7 +709,7 @@ pub fn ParseBody(comptime Body: type, comptime options: ParseBodyOptions) type { } }; } -pub fn parseBody(comptime Body: type) ParseBody(Body) { +pub fn parseBody(comptime Body: type, comptime options: ParseBodyOptions) ParseBody(Body, options) { return .{}; } @@ -752,7 +746,7 @@ test "parseBody" { var headers = http.Fields.init(std.testing.allocator); defer headers.deinit(); - try parseBody(Struct).handle( + try parseBody(Struct, .{}).handle( .{ .body = @as(?std.io.StreamSource, stream), .headers = headers }, .{}, .{ .allocator = std.testing.allocator }, diff --git a/src/http/multipart.zig b/src/http/multipart.zig index 854b7c3..e4c3880 100644 --- a/src/http/multipart.zig +++ b/src/http/multipart.zig @@ -357,6 +357,6 @@ test "parseFormData" { var src = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) }; const val = try parseFormData(struct { foo: []const u8, - }, "abcd", src.reader(), std.testing.allocator); + }, false, "abcd", src.reader(), std.testing.allocator); util.deepFree(std.testing.allocator, val); } diff --git a/src/http/urlencode.zig b/src/http/urlencode.zig index fdfb680..85cc7b9 100644 --- a/src/http/urlencode.zig +++ b/src/http/urlencode.zig @@ -252,49 +252,49 @@ fn formatQuery(comptime prefix: []const u8, params: anytype, writer: anytype) !v test "parse" { const testCase = struct { - fn case(comptime T: type, expected: T, query_string: []const u8) !void { - const result = try parse(std.testing.allocator, T, query_string); + fn case(allow_unknown_fields: bool, comptime T: type, expected: T, query_string: []const u8) !void { + const result = try parse(std.testing.allocator, allow_unknown_fields, T, query_string); defer parseFree(std.testing.allocator, result); try util.testing.expectDeepEqual(expected, result); } }.case; - try testCase(struct { int: usize = 3 }, .{ .int = 3 }, ""); - try testCase(struct { int: usize = 3 }, .{ .int = 2 }, "int=2"); - try testCase(struct { int: usize = 3 }, .{ .int = 2 }, "int=2&"); - try testCase(struct { boolean: bool = false }, .{ .boolean = false }, ""); - try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean"); - try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean=true"); - try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean=y"); - try testCase(struct { boolean: bool = false }, .{ .boolean = false }, "boolean=f"); - try testCase(struct { boolean: bool = false }, .{ .boolean = false }, "boolean=no"); - try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = null }, ""); - try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .foo }, "str_enum=foo"); - try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .bar }, "str_enum=bar"); - try testCase(struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = .foo }, ""); - try testCase(struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = null }, "str_enum"); - try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2"); - try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2&"); - try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&&n2=2&"); + try testCase(false, struct { int: usize = 3 }, .{ .int = 3 }, ""); + try testCase(false, struct { int: usize = 3 }, .{ .int = 2 }, "int=2"); + try testCase(false, struct { int: usize = 3 }, .{ .int = 2 }, "int=2&"); + try testCase(false, struct { boolean: bool = false }, .{ .boolean = false }, ""); + try testCase(false, struct { boolean: bool = false }, .{ .boolean = true }, "boolean"); + try testCase(false, struct { boolean: bool = false }, .{ .boolean = true }, "boolean=true"); + try testCase(false, struct { boolean: bool = false }, .{ .boolean = true }, "boolean=y"); + try testCase(false, struct { boolean: bool = false }, .{ .boolean = false }, "boolean=f"); + try testCase(false, struct { boolean: bool = false }, .{ .boolean = false }, "boolean=no"); + try testCase(false, struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = null }, ""); + try testCase(false, struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .foo }, "str_enum=foo"); + try testCase(false, struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .bar }, "str_enum=bar"); + try testCase(false, struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = .foo }, ""); + try testCase(false, struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = null }, "str_enum"); + try testCase(false, struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2"); + try testCase(false, struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2&"); + try testCase(false, struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&&n2=2&"); - try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, ""); - try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, "str"); - try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, "str="); - try testCase(struct { str: ?[]const u8 = null }, .{ .str = "foo" }, "str=foo"); - try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, "str=foo"); - try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, ""); - try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str"); - try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str="); + try testCase(false, struct { str: ?[]const u8 = null }, .{ .str = null }, ""); + try testCase(false, struct { str: ?[]const u8 = null }, .{ .str = null }, "str"); + try testCase(false, struct { str: ?[]const u8 = null }, .{ .str = null }, "str="); + try testCase(false, struct { str: ?[]const u8 = null }, .{ .str = "foo" }, "str=foo"); + try testCase(false, struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, "str=foo"); + try testCase(false, struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, ""); + try testCase(false, struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str"); + try testCase(false, struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str="); const rand_uuid = comptime util.Uuid.parse("c1fb6578-4d0c-4eb9-9f67-d56da3ae6f5d") catch unreachable; - try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, ""); - try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, "id="); - try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, "id"); - try testCase(struct { id: ?util.Uuid = null }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray()); - try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, ""); - try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id="); - try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id"); - try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray()); + try testCase(false, struct { id: ?util.Uuid = null }, .{ .id = null }, ""); + try testCase(false, struct { id: ?util.Uuid = null }, .{ .id = null }, "id="); + try testCase(false, struct { id: ?util.Uuid = null }, .{ .id = null }, "id"); + try testCase(false, struct { id: ?util.Uuid = null }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray()); + try testCase(false, struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, ""); + try testCase(false, struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id="); + try testCase(false, struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id"); + try testCase(false, struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray()); const SubStruct = struct { sub: struct { @@ -302,9 +302,9 @@ test "parse" { bar: usize = 2, } = .{}, }; - try testCase(SubStruct, .{ .sub = .{ .foo = 1, .bar = 2 } }, ""); - try testCase(SubStruct, .{ .sub = .{ .foo = 3, .bar = 3 } }, "sub.foo=3&sub.bar=3"); - try testCase(SubStruct, .{ .sub = .{ .foo = 3, .bar = 2 } }, "sub.foo=3"); + try testCase(false, SubStruct, .{ .sub = .{ .foo = 1, .bar = 2 } }, ""); + try testCase(false, SubStruct, .{ .sub = .{ .foo = 3, .bar = 3 } }, "sub.foo=3&sub.bar=3"); + try testCase(false, SubStruct, .{ .sub = .{ .foo = 3, .bar = 2 } }, "sub.foo=3"); // TODO: Semantics are ill-defined here. What happens if the substruct doesn't have // default values? @@ -313,8 +313,8 @@ test "parse" { // foo: usize = 1, // } = null, // }; - // try testCase(SubStruct2, .{ .sub = null }, ""); - // try testCase(SubStruct2, .{ .sub = null }, "sub="); + // try testCase(false, SubStruct2, .{ .sub = null }, ""); + // try testCase(false, SubStruct2, .{ .sub = null }, "sub="); // TODO: also here (semantics are well defined it just breaks tests) // const SubUnion = struct { @@ -323,21 +323,24 @@ test "parse" { // bar: usize, // } = null, // }; - // try testCase(SubUnion, .{ .sub = null }, ""); - // try testCase(SubUnion, .{ .sub = null }, "sub="); + // try testCase(false, SubUnion, .{ .sub = null }, ""); + // try testCase(false, SubUnion, .{ .sub = null }, "sub="); const SubUnion2 = struct { - sub: ?struct { - foo: usize, - val: union(enum) { + sub: ?union(enum) { + bar: struct { + foo: usize, bar: []const u8, + }, + baz: struct { + foo: usize, baz: []const u8, }, } = null, }; - try testCase(SubUnion2, .{ .sub = null }, ""); - try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .bar = "abc" } } }, "sub.foo=1&sub.bar=abc"); - try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .baz = "abc" } } }, "sub.foo=1&sub.baz=abc"); + try testCase(false, SubUnion2, .{ .sub = null }, ""); + try testCase(false, SubUnion2, .{ .sub = .{ .bar = .{ .foo = 1, .bar = "abc" } } }, "sub.foo=1&sub.bar=abc"); + try testCase(false, SubUnion2, .{ .sub = .{ .baz = .{ .foo = 1, .baz = "abc" } } }, "sub.foo=1&sub.baz=abc"); } test "encodeStruct" { diff --git a/src/template/lib.zig b/src/template/lib.zig index 4698cab..2b98606 100644 --- a/src/template/lib.zig +++ b/src/template/lib.zig @@ -1349,7 +1349,7 @@ test "template" { try testCase("", .{}, ""); try testCase("abcd", .{}, "abcd"); - try testCase("{.val}", .{ .val = 3 }, "3"); + try testCase("{.val}", .{ .val = @as(usize, 3) }, "3"); try testCase("{#if .val}1{/if}", .{ .val = true }, "1"); try testCase("{#for .vals |$v|=} {$v} {=/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); try testCase("{#for .vals |$val|}{$val}{/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); diff --git a/src/util/serialize.zig b/src/util/serialize.zig index 120ca48..4098688 100644 --- a/src/util/serialize.zig +++ b/src/util/serialize.zig @@ -266,7 +266,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime } pub fn finish(self: *@This(), allocator: std.mem.Allocator) !Result { - return (try self.deserialize(allocator, Result, self.data, &.{})) orelse + return (try self.deserialize(allocator, Result, self.data, &.{}, true)) orelse if (std.meta.fields(Result).len == 0) return .{} else @@ -290,6 +290,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime comptime T: type, intermediary: anytype, comptime field_ref: FieldRef, + allow_default: bool, ) DeserializeError!?T { if (comptime Context.options.isScalar(T)) { const val = @field(intermediary.static, util.comptimeJoin(".", field_ref)); @@ -304,7 +305,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime var partial_match_found: bool = false; inline for (info.fields) |field| { const F = field.field_type; - const maybe_value = self.deserialize(allocator, F, intermediary, field_ref) catch |err| switch (err) { + const maybe_value = self.deserialize(allocator, F, intermediary, field_ref, false) catch |err| switch (err) { error.MissingField => blk: { partial_match_found = true; break :blk @as(?F, null); @@ -333,7 +334,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime inline for (info.fields) |field, i| { const F = field.field_type; const new_field_ref = field_ref ++ &[_][]const u8{field.name}; - const maybe_value = try self.deserialize(allocator, F, intermediary, new_field_ref); + const maybe_value = try self.deserialize(allocator, F, intermediary, new_field_ref, false); if (maybe_value) |v| { @field(result, field.name) = v; fields_alloced[i] = true; @@ -350,7 +351,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime } if (any_missing and any_explicit) return error.MissingField; - if (!any_explicit) { + if (!any_explicit and !allow_default) { inline for (info.fields) |field, i| { if (fields_alloced[i]) self.deserializeFree(allocator, @field(result, field.name)); } @@ -369,7 +370,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime var count: usize = 0; errdefer for (result[0..count]) |res| util.deepFree(allocator, res); for (data.items) |sub, i| { - result[i] = (try self.deserialize(allocator, info.child, sub, &.{})) orelse return error.SparseSlice; + result[i] = (try self.deserialize(allocator, info.child, sub, &.{}, false)) orelse return error.SparseSlice; } return result; @@ -378,7 +379,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime }, // Specifically non-scalar optionals - .Optional => |info| return try self.deserialize(allocator, info.child, intermediary, field_ref), + .Optional => |info| return try self.deserialize(allocator, info.child, intermediary, field_ref, allow_default), else => @compileError("Unsupported type"), } @@ -406,7 +407,8 @@ test "Deserializer" { { const T = struct { foo: []const u8, bar: bool }; - var ds = Deserializer(T){}; + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); try ds.setSerializedField("foo", "123"); try ds.setSerializedField("bar", "true"); @@ -419,7 +421,8 @@ test "Deserializer" { { const T = struct { foo: []const u8, bar: bool }; - var ds = Deserializer(T){}; + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); try std.testing.expectError(error.UnknownField, ds.setSerializedField("baz", "123")); } @@ -429,7 +432,8 @@ test "Deserializer" { foo: struct { bar: bool, baz: bool }, }; - var ds = Deserializer(T){}; + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); try ds.setSerializedField("foo.bar", "true"); try ds.setSerializedField("foo.baz", "true"); @@ -438,29 +442,45 @@ test "Deserializer" { try util.testing.expectDeepEqual(T{ .foo = .{ .bar = true, .baz = true } }, val); } - // Union embedding + // Union behavior { const T = struct { - foo: union(enum) { bar: bool, baz: bool }, + foo: union(enum) { + bar: struct { + bar: bool, + }, + baz: struct { + baz: bool, + }, + }, }; - var ds = Deserializer(T){}; - try ds.setSerializedField("bar", "true"); + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); + try ds.setSerializedField("foo.bar", "true"); const val = try ds.finish(std.testing.allocator); defer ds.finishFree(std.testing.allocator, val); - try util.testing.expectDeepEqual(T{ .foo = .{ .bar = true } }, val); + try util.testing.expectDeepEqual(T{ .foo = .{ .bar = .{ .bar = true } } }, val); } // Returns error if multiple union fields specified { const T = struct { - foo: union(enum) { bar: bool, baz: bool }, + foo: union(enum) { + bar: struct { + bar: bool, + }, + baz: struct { + baz: bool, + }, + }, }; - var ds = Deserializer(T){}; - try ds.setSerializedField("bar", "true"); - try ds.setSerializedField("baz", "true"); + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); + try ds.setSerializedField("foo.bar", "true"); + try ds.setSerializedField("foo.baz", "true"); try std.testing.expectError(error.DuplicateUnionMember, ds.finish(std.testing.allocator)); } @@ -469,7 +489,8 @@ test "Deserializer" { { const T = struct { foo: []const u8 = "123", bar: bool = true }; - var ds = Deserializer(T){}; + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); const val = try ds.finish(std.testing.allocator); defer ds.finishFree(std.testing.allocator, val); @@ -480,7 +501,8 @@ test "Deserializer" { { const T = struct { foo: []const u8, bar: bool }; - var ds = Deserializer(T){}; + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); try ds.setSerializedField("foo", "123"); try std.testing.expectError(error.MissingField, ds.finish(std.testing.allocator)); @@ -493,7 +515,8 @@ test "Deserializer" { qux: ?union(enum) { quux: usize } = null, }; - var ds = Deserializer(T){}; + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); const val = try ds.finish(std.testing.allocator); defer ds.finishFree(std.testing.allocator, val); @@ -506,9 +529,10 @@ test "Deserializer" { qux: ?union(enum) { quux: usize } = null, }; - var ds = Deserializer(T){}; + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); try ds.setSerializedField("foo.baz", "3"); - try ds.setSerializedField("quux", "3"); + try ds.setSerializedField("qux", "3"); const val = try ds.finish(std.testing.allocator); defer ds.finishFree(std.testing.allocator, val); @@ -521,9 +545,10 @@ test "Deserializer" { qux: ?union(enum) { quux: usize } = null, }; - var ds = Deserializer(T){}; + var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) }; + defer ds.deinit(); try ds.setSerializedField("foo.bar", "3"); - try ds.setSerializedField("quux", "3"); + try ds.setSerializedField("qux", "3"); try std.testing.expectError(error.MissingField, ds.finish(std.testing.allocator)); }