diff --git a/src/http/query.zig b/src/http/query.zig index 35438f5..1396696 100644 --- a/src/http/query.zig +++ b/src/http/query.zig @@ -1,6 +1,7 @@ const std = @import("std"); +const util = @import("util"); -const QueryIter = @import("util").QueryIter; +const QueryIter = util.QueryIter; /// Parses a set of query parameters described by the struct `T`. /// @@ -84,6 +85,10 @@ pub fn parseQuery(alloc: std.mem.Allocator, comptime T: type, query: []const u8) return (try parse(alloc, T, "", "", fields)) orelse error.NoQuery; } +pub fn parseQueryFree(alloc: std.mem.Allocator, val: anytype) void { + util.deepFree(alloc, val); +} + fn decodeString(alloc: std.mem.Allocator, val: []const u8) ![]u8 { var list = try std.ArrayList(u8).initCapacity(alloc, val.len); errdefer list.deinit(); @@ -142,6 +147,9 @@ fn parse( .Struct => |info| { var result: T = undefined; var fields_specified: usize = 0; + errdefer inline for (info.fields) |field, i| { + if (fields_specified < i) util.deepFree(alloc, @field(result, field.name)); + }; inline for (info.fields) |field| { const F = field.field_type; @@ -151,7 +159,7 @@ fn parse( maybe_value = v; } else if (field.default_value) |default| { if (comptime @sizeOf(F) != 0) { - maybe_value = @ptrCast(*const F, @alignCast(@alignOf(F), default)).*; + maybe_value = try util.deepClone(alloc, @ptrCast(*const F, @alignCast(@alignOf(F), default)).*); } else { maybe_value = std.mem.zeroes(F); } @@ -227,10 +235,38 @@ fn Intermediary(comptime T: type) type { } }); } -fn parseQueryValue(alloc: std.mem.Allocator, comptime T: type, value: ?[]const u8) !T { +fn parseQueryValue(alloc: std.mem.Allocator, comptime T: type, maybe_value: ?[]const u8) !T { const is_optional = comptime std.meta.trait.is(.Optional)(T); - // If param is present, but without an associated value - if (value == null) { + if (maybe_value) |value| { + const Eff = if (is_optional) std.meta.Child(T) else T; + + if (value.len == 0 and is_optional) return null; + + const decoded = try decodeString(alloc, value); + errdefer alloc.free(decoded); + + if (comptime std.meta.trait.isZigString(Eff)) return decoded; + + defer alloc.free(decoded); + + const result = if (comptime std.meta.trait.isIntegral(Eff)) + try std.fmt.parseInt(Eff, decoded, 0) + else if (comptime std.meta.trait.isFloat(Eff)) + try std.fmt.parseFloat(Eff, decoded) + else if (comptime std.meta.trait.is(.Enum)(Eff)) blk: { + _ = std.ascii.lowerString(decoded, decoded); + break :blk std.meta.stringToEnum(Eff, decoded) orelse return error.InvalidEnumValue; + } else if (Eff == bool) blk: { + _ = std.ascii.lowerString(decoded, decoded); + break :blk bool_map.get(decoded) orelse return error.InvalidBool; + } else if (comptime std.meta.trait.hasFn("parse")(Eff)) + try Eff.parse(value) + else + @compileError("Invalid type " ++ @typeName(T)); + + return result; + } else { + // If param is present, but without an associated value return if (is_optional) null else if (T == bool) @@ -238,8 +274,6 @@ fn parseQueryValue(alloc: std.mem.Allocator, comptime T: type, value: ?[]const u else error.InvalidValue; } - - return try parseQueryValueNotNull(alloc, if (is_optional) std.meta.Child(T) else T, value.?); } const bool_map = std.ComptimeStringMap(bool, .{ @@ -256,32 +290,6 @@ const bool_map = std.ComptimeStringMap(bool, .{ .{ "0", false }, }); -fn parseQueryValueNotNull(alloc: std.mem.Allocator, comptime T: type, value: []const u8) !T { - const decoded = try decodeString(alloc, value); - errdefer alloc.free(decoded); - - if (comptime std.meta.trait.isZigString(T)) return decoded; - - defer alloc.free(decoded); - - const result = if (comptime std.meta.trait.isIntegral(T)) - try std.fmt.parseInt(T, decoded, 0) - else if (comptime std.meta.trait.isFloat(T)) - try std.fmt.parseFloat(T, decoded) - else if (comptime std.meta.trait.is(.Enum)(T)) blk: { - _ = std.ascii.lowerString(decoded, decoded); - break :blk std.meta.stringToEnum(T, decoded) orelse return error.InvalidEnumValue; - } else if (T == bool) blk: { - _ = std.ascii.lowerString(decoded, decoded); - break :blk bool_map.get(decoded) orelse return error.InvalidBool; - } else if (comptime std.meta.trait.hasFn("parse")(T)) - try T.parse(value) - else - @compileError("Invalid type " ++ @typeName(T)); - - return result; -} - fn isScalar(comptime T: type) bool { if (comptime std.meta.trait.isZigString(T)) return true; if (comptime std.meta.trait.isIntegral(T)) return true; @@ -359,6 +367,69 @@ fn format(comptime prefix: []const u8, comptime name: []const u8, params: anytyp } test "parseQuery" { + const testCase = struct { + fn case(comptime T: type, expected: T, query_string: []const u8) !void { + const result = try parseQuery(std.testing.allocator, T, query_string); + defer parseQueryFree(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(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="); + + 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()); + + const SubStruct = struct { + sub: struct { + foo: usize = 1, + 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"); + + // TODO: Semantics are ill-defined here + // const SubStruct2 = struct { + // sub: ?struct { + // foo: usize = 1, + // } = null, + // }; + // try testCase(SubStruct2, .{ .sub = null }, ""); + const TestQuery = struct { int: usize = 3, boolean: bool = false, diff --git a/src/http/test.zig b/src/http/test.zig index 1441ec2..c142f68 100644 --- a/src/http/test.zig +++ b/src/http/test.zig @@ -1,3 +1,5 @@ test { _ = @import("./request/test_parser.zig"); + _ = @import("./middleware.zig"); + _ = @import("./query.zig"); } diff --git a/src/util/lib.zig b/src/util/lib.zig index acc29f3..84b2122 100644 --- a/src/util/lib.zig +++ b/src/util/lib.zig @@ -160,6 +160,13 @@ pub fn deepClone(alloc: std.mem.Allocator, val: anytype) !@TypeOf(val) { count += 1; } }, + .Union => { + inline for (comptime std.meta.fieldNames(T)) |f| { + if (std.meta.isTag(val, f)) { + return @unionInit(T, f, try deepClone(alloc, @field(val, f))); + } + } else unreachable; + }, .Array => { var count: usize = 0; errdefer for (result[0..count]) |v| deepFree(alloc, v);