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")); }