2022-10-08 07:51:22 +00:00
|
|
|
const std = @import("std");
|
2022-10-09 09:05:01 +00:00
|
|
|
|
|
|
|
const QueryIter = @import("util").QueryIter;
|
|
|
|
|
|
|
|
/// Parses a set of query parameters described by the struct `T`.
|
|
|
|
///
|
|
|
|
/// To specify query parameters, provide a struct similar to the following:
|
|
|
|
/// ```
|
|
|
|
/// struct {
|
|
|
|
/// foo: bool = false,
|
|
|
|
/// bar: ?[]const u8 = null,
|
|
|
|
/// baz: usize = 10,
|
|
|
|
/// qux: enum { quux, snap } = .quux,
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// This will allow it to parse a query string like the following:
|
|
|
|
/// `?foo&bar=abc&qux=snap`
|
|
|
|
///
|
|
|
|
/// Every parameter must have a default value that will be used when the
|
|
|
|
/// parameter is not provided, and parameter keys.
|
|
|
|
/// Numbers are parsed from their string representations, and a parameter
|
|
|
|
/// provided in the query string without a value is parsed either as a bool
|
|
|
|
/// `true` flag or as `null` depending on the type of its param.
|
|
|
|
///
|
|
|
|
/// Parameter types supported:
|
|
|
|
/// - []const u8
|
|
|
|
/// - numbers (both integer and float)
|
|
|
|
/// + Numbers are parsed in base 10
|
|
|
|
/// - bool
|
|
|
|
/// + See below for detals
|
|
|
|
/// - exhaustive enums
|
|
|
|
/// + Enums are treated as strings with values equal to the enum fields
|
|
|
|
/// - ?F (where isScalar(F) and F != bool)
|
|
|
|
/// - Any type that implements:
|
|
|
|
/// + pub fn parse([]const u8) !F
|
|
|
|
///
|
|
|
|
/// Boolean Parameters:
|
|
|
|
/// The following query strings will all parse a `true` value for the
|
|
|
|
/// parameter `foo: bool = false`:
|
|
|
|
/// - `?foo`
|
|
|
|
/// - `?foo=true`
|
|
|
|
/// - `?foo=t`
|
|
|
|
/// - `?foo=yes`
|
|
|
|
/// - `?foo=y`
|
|
|
|
/// - `?foo=1`
|
|
|
|
/// And the following query strings all parse a `false` value:
|
|
|
|
/// - `?`
|
|
|
|
/// - `?foo=false`
|
|
|
|
/// - `?foo=f`
|
|
|
|
/// - `?foo=no`
|
|
|
|
/// - `?foo=n`
|
|
|
|
/// - `?foo=0`
|
|
|
|
///
|
|
|
|
/// Compound Types:
|
|
|
|
/// Compound (struct) types are also supported, with the parameter key
|
|
|
|
/// for its parameters consisting of the struct's field + '.' + parameter
|
|
|
|
/// field. For example:
|
|
|
|
/// ```
|
|
|
|
/// struct {
|
|
|
|
/// foo: struct {
|
|
|
|
/// baz: usize = 0,
|
|
|
|
/// } = .{},
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
/// Would be used to parse a query string like
|
|
|
|
/// `?foo.baz=12345`
|
|
|
|
///
|
|
|
|
/// Compound types cannot currently be nullable, and must be structs.
|
|
|
|
///
|
|
|
|
/// TODO: values are currently case-sensitive, and are not url-decoded properly.
|
|
|
|
/// This should be fixed.
|
|
|
|
pub fn parseQuery(comptime T: type, query: []const u8) !T {
|
2022-11-14 04:40:13 +00:00
|
|
|
if (comptime !std.meta.trait.isContainer(T)) @compileError("T must be a struct");
|
2022-10-09 09:05:01 +00:00
|
|
|
var iter = QueryIter.from(query);
|
2022-11-14 04:40:13 +00:00
|
|
|
|
|
|
|
var fields = Intermediary(T){};
|
2022-10-09 09:05:01 +00:00
|
|
|
while (iter.next()) |pair| {
|
2022-11-14 04:40:13 +00:00
|
|
|
// TODO: Hash map
|
|
|
|
inline for (std.meta.fields(Intermediary(T))) |field| {
|
|
|
|
if (std.ascii.eqlIgnoreCase(field.name[2..], pair.key)) {
|
|
|
|
@field(fields, field.name) = if (pair.value) |v| .{ .value = v } else .{ .no_value = {} };
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else std.log.debug("unknown param {s}", .{pair.key});
|
2022-10-08 07:51:22 +00:00
|
|
|
}
|
|
|
|
|
2022-11-14 04:40:13 +00:00
|
|
|
return (try parse(T, "", "", fields)).?;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parseScalar(comptime T: type, comptime name: []const u8, fields: anytype) !?T {
|
|
|
|
const param = @field(fields, name);
|
|
|
|
return switch (param) {
|
|
|
|
.not_specified => null,
|
|
|
|
.no_value => try parseQueryValue(T, null),
|
|
|
|
.value => |v| try parseQueryValue(T, v),
|
|
|
|
};
|
2022-10-08 07:51:22 +00:00
|
|
|
}
|
|
|
|
|
2022-11-14 04:40:13 +00:00
|
|
|
fn parse(comptime T: type, comptime prefix: []const u8, comptime name: []const u8, fields: anytype) !?T {
|
|
|
|
if (comptime isScalar(T)) return parseScalar(T, prefix ++ "." ++ name, fields);
|
|
|
|
switch (@typeInfo(T)) {
|
|
|
|
.Union => |info| {
|
|
|
|
var result: ?T = null;
|
|
|
|
inline for (info.fields) |field| {
|
|
|
|
const F = field.field_type;
|
|
|
|
|
|
|
|
const maybe_value = try parse(F, prefix, field.name, fields);
|
|
|
|
if (maybe_value) |value| {
|
|
|
|
if (result != null) return error.DuplicateUnionField;
|
|
|
|
|
|
|
|
result = @unionInit(T, field.name, value);
|
2022-10-09 09:05:01 +00:00
|
|
|
}
|
2022-11-14 04:40:13 +00:00
|
|
|
}
|
|
|
|
std.log.debug("{any}", .{result});
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
|
|
|
.Struct => |info| {
|
|
|
|
var result: T = undefined;
|
|
|
|
var fields_specified: usize = 0;
|
|
|
|
|
|
|
|
inline for (info.fields) |field| {
|
|
|
|
const F = field.field_type;
|
|
|
|
|
|
|
|
var maybe_value: ?F = null;
|
|
|
|
if (try parse(F, prefix ++ "." ++ name, field.name, fields)) |v| {
|
|
|
|
maybe_value = v;
|
|
|
|
} else if (field.default_value) |default| {
|
|
|
|
maybe_value = @ptrCast(*const F, @alignCast(@alignOf(F), default)).*;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (maybe_value) |v| {
|
|
|
|
fields_specified += 1;
|
|
|
|
@field(result, field.name) = v;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fields_specified == 0) {
|
|
|
|
return null;
|
|
|
|
} else if (fields_specified != info.fields.len) {
|
|
|
|
return error.PartiallySpecifiedStruct;
|
2022-10-09 09:05:01 +00:00
|
|
|
} else {
|
2022-11-14 04:40:13 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// Only applies to non-scalar optionals
|
|
|
|
.Optional => |info| return try parse(info.child, prefix, name, fields),
|
|
|
|
|
|
|
|
else => @compileError("tmp"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn recursiveFieldPaths(comptime T: type, comptime prefix: []const u8) []const []const u8 {
|
|
|
|
comptime {
|
|
|
|
if (std.meta.trait.is(.Optional)(T)) return recursiveFieldPaths(std.meta.Child(T), prefix);
|
|
|
|
|
|
|
|
var fields: []const []const u8 = &.{};
|
|
|
|
|
|
|
|
for (std.meta.fields(T)) |f| {
|
|
|
|
const full_name = prefix ++ f.name;
|
|
|
|
|
|
|
|
if (isScalar(f.field_type)) {
|
|
|
|
fields = fields ++ @as([]const []const u8, &.{full_name});
|
|
|
|
} else {
|
|
|
|
const field_prefix = if (@typeInfo(f.field_type) == .Union) prefix else full_name ++ ".";
|
|
|
|
fields = fields ++ recursiveFieldPaths(f.field_type, field_prefix);
|
2022-10-09 09:05:01 +00:00
|
|
|
}
|
|
|
|
}
|
2022-11-14 04:40:13 +00:00
|
|
|
|
|
|
|
return fields;
|
2022-10-09 09:05:01 +00:00
|
|
|
}
|
2022-11-14 04:40:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const QueryParam = union(enum) {
|
|
|
|
not_specified: void,
|
|
|
|
no_value: void,
|
|
|
|
value: []const u8,
|
|
|
|
};
|
|
|
|
|
|
|
|
fn Intermediary(comptime T: type) type {
|
|
|
|
const field_names = recursiveFieldPaths(T, "..");
|
|
|
|
|
|
|
|
var fields: [field_names.len]std.builtin.Type.StructField = undefined;
|
|
|
|
for (field_names) |name, i| fields[i] = .{
|
|
|
|
.name = name,
|
|
|
|
.field_type = QueryParam,
|
|
|
|
.default_value = &QueryParam{ .not_specified = {} },
|
|
|
|
.is_comptime = false,
|
|
|
|
.alignment = @alignOf(QueryParam),
|
|
|
|
};
|
2022-10-08 07:51:22 +00:00
|
|
|
|
2022-11-14 04:40:13 +00:00
|
|
|
return @Type(.{ .Struct = .{
|
|
|
|
.layout = .Auto,
|
|
|
|
.fields = &fields,
|
|
|
|
.decls = &.{},
|
|
|
|
.is_tuple = false,
|
|
|
|
} });
|
2022-10-09 09:05:01 +00:00
|
|
|
}
|
2022-10-08 07:51:22 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
fn parseQueryValue(comptime T: type, 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) {
|
|
|
|
return if (is_optional)
|
|
|
|
null
|
|
|
|
else if (T == bool)
|
|
|
|
true
|
|
|
|
else
|
|
|
|
error.InvalidValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
return try parseQueryValueNotNull(if (is_optional) std.meta.Child(T) else T, value.?);
|
2022-10-08 07:51:22 +00:00
|
|
|
}
|
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
const bool_map = std.ComptimeStringMap(bool, .{
|
|
|
|
.{ "true", true },
|
|
|
|
.{ "t", true },
|
|
|
|
.{ "yes", true },
|
|
|
|
.{ "y", true },
|
|
|
|
.{ "1", true },
|
2022-10-08 07:51:22 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
.{ "false", false },
|
|
|
|
.{ "f", false },
|
|
|
|
.{ "no", false },
|
|
|
|
.{ "n", false },
|
|
|
|
.{ "0", false },
|
|
|
|
});
|
2022-10-08 07:51:22 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
fn parseQueryValueNotNull(comptime T: type, value: []const u8) !T {
|
|
|
|
if (comptime std.meta.trait.isZigString(T)) return value;
|
|
|
|
if (comptime std.meta.trait.isIntegral(T)) return try std.fmt.parseInt(T, value, 0);
|
|
|
|
if (comptime std.meta.trait.isFloat(T)) return try std.fmt.parseFloat(T, value);
|
|
|
|
if (comptime std.meta.trait.is(.Enum)(T)) return std.meta.stringToEnum(T, value) orelse error.InvalidEnumValue;
|
|
|
|
if (T == bool) return bool_map.get(value) orelse error.InvalidBool;
|
|
|
|
if (comptime std.meta.trait.hasFn("parse")(T)) return try T.parse(value);
|
2022-10-08 07:51:22 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
@compileError("Invalid type " ++ @typeName(T));
|
|
|
|
}
|
|
|
|
|
|
|
|
fn isScalar(comptime T: type) bool {
|
|
|
|
if (comptime std.meta.trait.isZigString(T)) return true;
|
|
|
|
if (comptime std.meta.trait.isIntegral(T)) return true;
|
|
|
|
if (comptime std.meta.trait.isFloat(T)) return true;
|
|
|
|
if (comptime std.meta.trait.is(.Enum)(T)) return true;
|
|
|
|
if (T == bool) return true;
|
|
|
|
if (comptime std.meta.trait.hasFn("parse")(T)) return true;
|
|
|
|
|
|
|
|
if (comptime std.meta.trait.is(.Optional)(T) and isScalar(std.meta.Child(T))) return true;
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
test {
|
|
|
|
const TestQuery = struct {
|
|
|
|
int: usize = 3,
|
|
|
|
boolean: bool = false,
|
|
|
|
str_enum: ?enum { foo, bar } = null,
|
|
|
|
};
|
|
|
|
|
|
|
|
try std.testing.expectEqual(TestQuery{
|
|
|
|
.int = 3,
|
|
|
|
.boolean = false,
|
|
|
|
.str_enum = null,
|
|
|
|
}, try parseQuery(TestQuery, ""));
|
|
|
|
|
|
|
|
try std.testing.expectEqual(TestQuery{
|
|
|
|
.int = 5,
|
|
|
|
.boolean = true,
|
|
|
|
.str_enum = .foo,
|
|
|
|
}, try parseQuery(TestQuery, "?int=5&boolean=yes&str_enum=foo"));
|
2022-10-08 07:51:22 +00:00
|
|
|
}
|