fediglam/src/main/query.zig

317 lines
10 KiB
Zig

const std = @import("std");
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 {
if (comptime !std.meta.trait.isContainer(T)) @compileError("T must be a struct");
var iter = QueryIter.from(query);
var fields = Intermediary(T){};
while (iter.next()) |pair| {
// 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});
}
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),
};
}
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);
}
}
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;
} else {
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);
}
}
return fields;
}
}
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),
};
return @Type(.{ .Struct = .{
.layout = .Auto,
.fields = &fields,
.decls = &.{},
.is_tuple = false,
} });
}
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.?);
}
const bool_map = std.ComptimeStringMap(bool, .{
.{ "true", true },
.{ "t", true },
.{ "yes", true },
.{ "y", true },
.{ "1", true },
.{ "false", false },
.{ "f", false },
.{ "no", false },
.{ "n", false },
.{ "0", false },
});
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);
@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;
}
pub fn formatQuery(params: anytype, writer: anytype) !void {
try format("", "", params, writer);
}
fn formatScalar(comptime name: []const u8, val: anytype, writer: anytype) !void {
const T = @TypeOf(val);
if (comptime std.meta.trait.isZigString(T)) return std.fmt.format(writer, "{s}={s}&", .{ name, val });
_ = try switch (@typeInfo(T)) {
.Enum => std.fmt.format(writer, "{s}={s}&", .{ name, @tagName(val) }),
.Optional => if (val) |v| formatScalar(name, v, writer),
else => std.fmt.format(writer, "{s}={}&", .{ name, val }),
};
}
fn format(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void {
const T = @TypeOf(params);
const eff_prefix = if (prefix.len == 0) "" else prefix ++ ".";
if (comptime isScalar(T)) return formatScalar(eff_prefix ++ name, params, writer);
switch (@typeInfo(T)) {
.Struct => {
inline for (std.meta.fields(T)) |field| {
const val = @field(params, field.name);
try format(eff_prefix ++ name, field.name, val, writer);
}
},
.Union => {
inline for (std.meta.fields(T)) |field| {
const tag = @field(std.meta.Tag(T), field.name);
const tag_name = field.name;
if (@as(std.meta.Tag(T), params) == tag) {
const val = @field(params, tag_name);
try format(prefix, tag_name, val, writer);
}
}
},
.Optional => {
if (params) |p| try format(prefix, name, p, writer);
},
else => @compileError("Unsupported query type"),
}
}
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"));
}