const std = @import("std"); const util = @import("util"); pub const Iter = struct { const Pair = struct { key: []const u8, value: ?[]const u8, }; iter: std.mem.SplitIterator(u8), pub fn from(q: []const u8) Iter { return Iter{ .iter = std.mem.split(u8, std.mem.trimLeft(u8, q, "?"), "&"), }; } pub fn next(self: *Iter) ?Pair { while (true) { const part = self.iter.next() orelse return null; if (part.len == 0) continue; const key = std.mem.sliceTo(part, '='); if (key.len == part.len) return Pair{ .key = key, .value = null, }; return Pair{ .key = key, .value = part[key.len + 1 ..], }; } } }; /// 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` /// pub fn parse(alloc: std.mem.Allocator, comptime T: type, query: []const u8) !T { var iter = Iter.from(query); var deserializer = Deserializer(T){}; while (iter.next()) |pair| { try deserializer.setSerializedField(pair.key, pair.value); } return try deserializer.finish(alloc); } fn Deserializer(comptime Result: type) type { return util.DeserializerContext(Result, ?[]const u8, struct { pub const options = util.serialize.default_options; pub fn deserializeScalar(_: @This(), alloc: std.mem.Allocator, comptime T: type, maybe_val: ?[]const u8) !T { const is_optional = comptime std.meta.trait.is(.Optional)(T); if (maybe_val) |val| { if (val.len == 0 and is_optional) return null; const decoded = try decodeString(alloc, val); defer alloc.free(decoded); return try util.serialize.deserializeString(alloc, T, decoded); } else { // If param is present, but without an associated value return if (is_optional) null else if (T == bool) true else error.InvalidValue; } } }); } pub fn parseFree(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(); var idx: usize = 0; while (idx < val.len) : (idx += 1) { if (val[idx] != '%') { try list.append(val[idx]); } else { if (val.len < idx + 2) return error.InvalidEscape; const buf = [2]u8{ val[idx + 1], val[idx + 2] }; idx += 2; const ch = try std.fmt.parseInt(u8, &buf, 16); try list.append(ch); } } return list.toOwnedSlice(); } 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 (comptime std.meta.trait.is(.EnumLiteral)(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 EncodeStruct(comptime Params: type) type { return struct { params: Params, pub fn format(v: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { try formatQuery("", "", v.params, writer); } }; } pub fn encodeStruct(val: anytype) EncodeStruct(@TypeOf(val)) { return EncodeStruct(@TypeOf(val)){ .params = val }; } fn urlFormatString(writer: anytype, val: []const u8) !void { for (val) |ch| { const printable = switch (ch) { '0'...'9', 'a'...'z', 'A'...'Z' => true, '-', '.', '_', '~', ':', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true, else => false, }; try if (printable) writer.writeByte(ch) else std.fmt.format(writer, "%{x:0>2}", .{ch}); } } fn formatScalar(comptime name: []const u8, val: anytype, writer: anytype) !void { const T = @TypeOf(val); if (comptime std.meta.trait.is(.Optional)(T)) { return if (val) |v| formatScalar(name, v, writer) else {}; } try urlFormatString(writer, name); try writer.writeByte('='); if (comptime std.meta.trait.isZigString(T)) { try urlFormatString(writer, val); } else try switch (@typeInfo(T)) { .EnumLiteral, .Enum => urlFormatString(writer, @tagName(val)), else => std.fmt.format(writer, "{}", .{val}), }; try writer.writeByte('&'); } fn formatQuery(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 formatQuery(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 formatQuery(prefix, tag_name, val, writer); } } }, .Optional => { if (params) |p| try formatQuery(prefix, name, p, writer); }, else => @compileError("Unsupported query type"), } } test "parse" { const testCase = struct { fn case(comptime T: type, expected: T, query_string: []const u8) !void { const result = try parse(std.testing.allocator, T, query_string); defer parseFree(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. What happens if the substruct doesn't have // default values? // const SubStruct2 = struct { // sub: ?struct { // foo: usize = 1, // } = null, // }; // try testCase(SubStruct2, .{ .sub = null }, ""); // try testCase(SubStruct2, .{ .sub = null }, "sub="); // TODO: also here (semantics are well defined it just breaks tests) // const SubUnion = struct { // sub: ?union(enum) { // foo: usize, // bar: usize, // } = null, // }; // try testCase(SubUnion, .{ .sub = null }, ""); // try testCase(SubUnion, .{ .sub = null }, "sub="); const SubUnion2 = struct { sub: ?struct { foo: usize, val: union(enum) { bar: []const u8, baz: []const u8, }, } = null, }; try testCase(SubUnion2, .{ .sub = null }, ""); try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .bar = "abc" } } }, "sub.foo=1&sub.bar=abc"); try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .baz = "abc" } } }, "sub.foo=1&sub.baz=abc"); } test "encodeStruct" { try std.testing.expectFmt("", "{}", .{encodeStruct(.{})}); try std.testing.expectFmt("id=3&", "{}", .{encodeStruct(.{ .id = 3 })}); try std.testing.expectFmt("id=3&id2=4&", "{}", .{encodeStruct(.{ .id = 3, .id2 = 4 })}); try std.testing.expectFmt("str=foo&", "{}", .{encodeStruct(.{ .str = "foo" })}); try std.testing.expectFmt("enum_str=foo&", "{}", .{encodeStruct(.{ .enum_str = .foo })}); try std.testing.expectFmt("boolean=false&", "{}", .{encodeStruct(.{ .boolean = false })}); try std.testing.expectFmt("boolean=true&", "{}", .{encodeStruct(.{ .boolean = true })}); } test "Iter" { const testCase = struct { fn case(str: []const u8, pairs: []const Iter.Pair) !void { var iter = Iter.from(str); for (pairs) |pair| { try util.testing.expectDeepEqual(@as(?Iter.Pair, pair), iter.next()); } try std.testing.expect(iter.next() == null); } }.case; try testCase("", &.{}); try testCase("abc", &.{.{ .key = "abc", .value = null }}); try testCase("abc=", &.{.{ .key = "abc", .value = "" }}); try testCase("abc=def", &.{.{ .key = "abc", .value = "def" }}); try testCase("abc=def&", &.{.{ .key = "abc", .value = "def" }}); try testCase("?abc=def&", &.{.{ .key = "abc", .value = "def" }}); try testCase("?abc=def&foo&bar=baz&qux=", &.{ .{ .key = "abc", .value = "def" }, .{ .key = "foo", .value = null }, .{ .key = "bar", .value = "baz" }, .{ .key = "qux", .value = "" }, }); try testCase("?abc=def&&foo&bar=baz&&qux=&", &.{ .{ .key = "abc", .value = "def" }, .{ .key = "foo", .value = null }, .{ .key = "bar", .value = "baz" }, .{ .key = "qux", .value = "" }, }); try testCase("&=def&", &.{.{ .key = "", .value = "def" }}); }