Revamp QueryString parser test

This commit is contained in:
jaina heartles 2022-11-27 00:58:56 -08:00
parent f7f84f0516
commit ce40448dc8
3 changed files with 113 additions and 33 deletions

View File

@ -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,

View File

@ -1,3 +1,5 @@
test {
_ = @import("./request/test_parser.zig");
_ = @import("./middleware.zig");
_ = @import("./query.zig");
}

View File

@ -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);