385 lines
15 KiB
Zig
385 lines
15 KiB
Zig
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, allow_unknown_fields: bool, comptime T: type, query: []const u8) !T {
|
|
var iter = Iter.from(query);
|
|
|
|
var deserializer = Deserializer(T).init(alloc);
|
|
defer deserializer.deinit();
|
|
|
|
while (iter.next()) |pair| {
|
|
try deserializer.setSerializedField(pair.key, pair.value);
|
|
deserializer.setSerializedField(pair.key, pair.value) catch |err| switch (err) {
|
|
error.UnknownField => if (allow_unknown_fields) continue else return err,
|
|
else => |e| return e,
|
|
};
|
|
}
|
|
|
|
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" }});
|
|
}
|