Revamp QueryString parser test
This commit is contained in:
parent
f7f84f0516
commit
ce40448dc8
3 changed files with 113 additions and 33 deletions
|
@ -1,6 +1,7 @@
|
||||||
const std = @import("std");
|
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`.
|
/// 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;
|
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 {
|
fn decodeString(alloc: std.mem.Allocator, val: []const u8) ![]u8 {
|
||||||
var list = try std.ArrayList(u8).initCapacity(alloc, val.len);
|
var list = try std.ArrayList(u8).initCapacity(alloc, val.len);
|
||||||
errdefer list.deinit();
|
errdefer list.deinit();
|
||||||
|
@ -142,6 +147,9 @@ fn parse(
|
||||||
.Struct => |info| {
|
.Struct => |info| {
|
||||||
var result: T = undefined;
|
var result: T = undefined;
|
||||||
var fields_specified: usize = 0;
|
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| {
|
inline for (info.fields) |field| {
|
||||||
const F = field.field_type;
|
const F = field.field_type;
|
||||||
|
@ -151,7 +159,7 @@ fn parse(
|
||||||
maybe_value = v;
|
maybe_value = v;
|
||||||
} else if (field.default_value) |default| {
|
} else if (field.default_value) |default| {
|
||||||
if (comptime @sizeOf(F) != 0) {
|
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 {
|
} else {
|
||||||
maybe_value = std.mem.zeroes(F);
|
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);
|
const is_optional = comptime std.meta.trait.is(.Optional)(T);
|
||||||
// If param is present, but without an associated value
|
if (maybe_value) |value| {
|
||||||
if (value == null) {
|
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)
|
return if (is_optional)
|
||||||
null
|
null
|
||||||
else if (T == bool)
|
else if (T == bool)
|
||||||
|
@ -238,8 +274,6 @@ fn parseQueryValue(alloc: std.mem.Allocator, comptime T: type, value: ?[]const u
|
||||||
else
|
else
|
||||||
error.InvalidValue;
|
error.InvalidValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return try parseQueryValueNotNull(alloc, if (is_optional) std.meta.Child(T) else T, value.?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool_map = std.ComptimeStringMap(bool, .{
|
const bool_map = std.ComptimeStringMap(bool, .{
|
||||||
|
@ -256,32 +290,6 @@ const bool_map = std.ComptimeStringMap(bool, .{
|
||||||
.{ "0", false },
|
.{ "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 {
|
fn isScalar(comptime T: type) bool {
|
||||||
if (comptime std.meta.trait.isZigString(T)) return true;
|
if (comptime std.meta.trait.isZigString(T)) return true;
|
||||||
if (comptime std.meta.trait.isIntegral(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" {
|
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 {
|
const TestQuery = struct {
|
||||||
int: usize = 3,
|
int: usize = 3,
|
||||||
boolean: bool = false,
|
boolean: bool = false,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
test {
|
test {
|
||||||
_ = @import("./request/test_parser.zig");
|
_ = @import("./request/test_parser.zig");
|
||||||
|
_ = @import("./middleware.zig");
|
||||||
|
_ = @import("./query.zig");
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,6 +160,13 @@ pub fn deepClone(alloc: std.mem.Allocator, val: anytype) !@TypeOf(val) {
|
||||||
count += 1;
|
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 => {
|
.Array => {
|
||||||
var count: usize = 0;
|
var count: usize = 0;
|
||||||
errdefer for (result[0..count]) |v| deepFree(alloc, v);
|
errdefer for (result[0..count]) |v| deepFree(alloc, v);
|
||||||
|
|
Loading…
Reference in a new issue