queryStringify cleanup
This commit is contained in:
parent
abf31ea33c
commit
b2c69c2df8
|
@ -16,6 +16,7 @@ pub const Handler = server.Handler;
|
||||||
pub const Server = server.Server;
|
pub const Server = server.Server;
|
||||||
|
|
||||||
pub const middleware = @import("./middleware.zig");
|
pub const middleware = @import("./middleware.zig");
|
||||||
|
pub const queryStringify = @import("./query.zig").queryStringify;
|
||||||
|
|
||||||
pub const Fields = @import("./headers.zig").Fields;
|
pub const Fields = @import("./headers.zig").Fields;
|
||||||
|
|
||||||
|
|
|
@ -295,6 +295,7 @@ fn isScalar(comptime T: type) bool {
|
||||||
if (comptime std.meta.trait.isIntegral(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.isFloat(T)) return true;
|
||||||
if (comptime std.meta.trait.is(.Enum)(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 (T == bool) return true;
|
||||||
if (comptime std.meta.trait.hasFn("parse")(T)) return true;
|
if (comptime std.meta.trait.hasFn("parse")(T)) return true;
|
||||||
|
|
||||||
|
@ -303,8 +304,16 @@ fn isScalar(comptime T: type) bool {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn formatQuery(params: anytype, writer: anytype) !void {
|
pub fn QueryStringify(comptime Params: type) type {
|
||||||
try format("", "", params, writer);
|
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 queryStringify(val: anytype) QueryStringify(@TypeOf(val)) {
|
||||||
|
return QueryStringify(@TypeOf(val)){ .params = val };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn urlFormatString(writer: anytype, val: []const u8) !void {
|
fn urlFormatString(writer: anytype, val: []const u8) !void {
|
||||||
|
@ -330,14 +339,14 @@ fn formatScalar(comptime name: []const u8, val: anytype, writer: anytype) !void
|
||||||
if (comptime std.meta.trait.isZigString(T)) {
|
if (comptime std.meta.trait.isZigString(T)) {
|
||||||
try urlFormatString(writer, val);
|
try urlFormatString(writer, val);
|
||||||
} else try switch (@typeInfo(T)) {
|
} else try switch (@typeInfo(T)) {
|
||||||
.Enum => urlFormatString(writer, @tagName(val)),
|
.EnumLiteral, .Enum => urlFormatString(writer, @tagName(val)),
|
||||||
else => std.fmt.format(writer, "{}", .{val}),
|
else => std.fmt.format(writer, "{}", .{val}),
|
||||||
};
|
};
|
||||||
|
|
||||||
try writer.writeByte('&');
|
try writer.writeByte('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void {
|
fn formatQuery(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void {
|
||||||
const T = @TypeOf(params);
|
const T = @TypeOf(params);
|
||||||
const eff_prefix = if (prefix.len == 0) "" else prefix ++ ".";
|
const eff_prefix = if (prefix.len == 0) "" else prefix ++ ".";
|
||||||
if (comptime isScalar(T)) return formatScalar(eff_prefix ++ name, params, writer);
|
if (comptime isScalar(T)) return formatScalar(eff_prefix ++ name, params, writer);
|
||||||
|
@ -346,7 +355,7 @@ fn format(comptime prefix: []const u8, comptime name: []const u8, params: anytyp
|
||||||
.Struct => {
|
.Struct => {
|
||||||
inline for (std.meta.fields(T)) |field| {
|
inline for (std.meta.fields(T)) |field| {
|
||||||
const val = @field(params, field.name);
|
const val = @field(params, field.name);
|
||||||
try format(eff_prefix ++ name, field.name, val, writer);
|
try formatQuery(eff_prefix ++ name, field.name, val, writer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.Union => {
|
.Union => {
|
||||||
|
@ -355,12 +364,12 @@ fn format(comptime prefix: []const u8, comptime name: []const u8, params: anytyp
|
||||||
const tag_name = field.name;
|
const tag_name = field.name;
|
||||||
if (@as(std.meta.Tag(T), params) == tag) {
|
if (@as(std.meta.Tag(T), params) == tag) {
|
||||||
const val = @field(params, tag_name);
|
const val = @field(params, tag_name);
|
||||||
try format(prefix, tag_name, val, writer);
|
try formatQuery(prefix, tag_name, val, writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.Optional => {
|
.Optional => {
|
||||||
if (params) |p| try format(prefix, name, p, writer);
|
if (params) |p| try formatQuery(prefix, name, p, writer);
|
||||||
},
|
},
|
||||||
else => @compileError("Unsupported query type"),
|
else => @compileError("Unsupported query type"),
|
||||||
}
|
}
|
||||||
|
@ -455,3 +464,15 @@ test "parseQuery" {
|
||||||
try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .bar = "abc" } } }, "sub.foo=1&sub.bar=abc");
|
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");
|
try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .baz = "abc" } } }, "sub.foo=1&sub.baz=abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "formatQuery" {
|
||||||
|
try std.testing.expectFmt("", "{}", .{queryStringify(.{})});
|
||||||
|
try std.testing.expectFmt("id=3&", "{}", .{queryStringify(.{ .id = 3 })});
|
||||||
|
try std.testing.expectFmt("id=3&id2=4&", "{}", .{queryStringify(.{ .id = 3, .id2 = 4 })});
|
||||||
|
|
||||||
|
try std.testing.expectFmt("str=foo&", "{}", .{queryStringify(.{ .str = "foo" })});
|
||||||
|
try std.testing.expectFmt("enum_str=foo&", "{}", .{queryStringify(.{ .enum_str = .foo })});
|
||||||
|
|
||||||
|
try std.testing.expectFmt("boolean=false&", "{}", .{queryStringify(.{ .boolean = false })});
|
||||||
|
try std.testing.expectFmt("boolean=true&", "{}", .{queryStringify(.{ .boolean = true })});
|
||||||
|
}
|
||||||
|
|
|
@ -4,8 +4,6 @@ const builtin = @import("builtin");
|
||||||
const http = @import("http");
|
const http = @import("http");
|
||||||
const api = @import("api");
|
const api = @import("api");
|
||||||
const util = @import("util");
|
const util = @import("util");
|
||||||
const query_utils = @import("./query.zig");
|
|
||||||
const json_utils = @import("./json.zig");
|
|
||||||
|
|
||||||
const web_endpoints = @import("./controllers/web.zig").routes;
|
const web_endpoints = @import("./controllers/web.zig").routes;
|
||||||
const api_endpoints = @import("./controllers/api.zig").routes;
|
const api_endpoints = @import("./controllers/api.zig").routes;
|
||||||
|
@ -268,16 +266,8 @@ pub const helpers = struct {
|
||||||
// TODO: percent-encode
|
// TODO: percent-encode
|
||||||
try std.fmt.format(
|
try std.fmt.format(
|
||||||
writer,
|
writer,
|
||||||
"<{s}://{s}/{s}?",
|
"<{s}://{s}/{s}?{}>; rel=\"{s}\"",
|
||||||
.{ @tagName(community.scheme), community.host, path },
|
.{ @tagName(community.scheme), community.host, path, http.queryStringify(params), rel },
|
||||||
);
|
|
||||||
|
|
||||||
try query_utils.formatQuery(params, writer);
|
|
||||||
|
|
||||||
try std.fmt.format(
|
|
||||||
writer,
|
|
||||||
">; rel=\"{s}\"",
|
|
||||||
.{rel},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,677 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const mem = std.mem;
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
// This file is largely a copy of std.json
|
|
||||||
|
|
||||||
const StreamingParser = std.json.StreamingParser;
|
|
||||||
const Token = std.json.Token;
|
|
||||||
const unescapeValidString = std.json.unescapeValidString;
|
|
||||||
const UnescapeValidStringError = std.json.UnescapeValidStringError;
|
|
||||||
|
|
||||||
pub fn parse(comptime T: type, body: []const u8, alloc: std.mem.Allocator) !T {
|
|
||||||
var tokens = TokenStream.init(body);
|
|
||||||
|
|
||||||
const options = ParseOptions{ .allocator = alloc };
|
|
||||||
|
|
||||||
const token = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
|
|
||||||
const r = try parseInternal(T, token, &tokens, options);
|
|
||||||
errdefer parseFreeInternal(T, r, options);
|
|
||||||
if (!options.allow_trailing_data) {
|
|
||||||
if ((try tokens.next()) != null) unreachable;
|
|
||||||
assert(tokens.i >= tokens.slice.len);
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parseFree(value: anytype, alloc: std.mem.Allocator) void {
|
|
||||||
parseFreeInternal(@TypeOf(value), value, .{ .allocator = alloc });
|
|
||||||
}
|
|
||||||
|
|
||||||
// WARNING: the objects "parse" method must not contain a reference to the original value
|
|
||||||
fn hasCustomParse(comptime T: type) bool {
|
|
||||||
if (!std.meta.trait.hasFn("parse")(T)) return false;
|
|
||||||
if (!@hasDecl(T, "JsonParseAs")) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
///// The rest is (modified) from std.json
|
|
||||||
|
|
||||||
/// A small wrapper over a StreamingParser for full slices. Returns a stream of json Tokens.
|
|
||||||
pub const TokenStream = struct {
|
|
||||||
i: usize,
|
|
||||||
slice: []const u8,
|
|
||||||
parser: StreamingParser,
|
|
||||||
token: ?Token,
|
|
||||||
|
|
||||||
pub const Error = StreamingParser.Error || error{UnexpectedEndOfJson};
|
|
||||||
|
|
||||||
pub fn init(slice: []const u8) TokenStream {
|
|
||||||
return TokenStream{
|
|
||||||
.i = 0,
|
|
||||||
.slice = slice,
|
|
||||||
.parser = StreamingParser.init(),
|
|
||||||
.token = null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stackUsed(self: *TokenStream) usize {
|
|
||||||
return self.parser.stack.len + if (self.token != null) @as(usize, 1) else 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(self: *TokenStream) Error!?Token {
|
|
||||||
if (self.token) |token| {
|
|
||||||
self.token = null;
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
var t1: ?Token = undefined;
|
|
||||||
var t2: ?Token = undefined;
|
|
||||||
|
|
||||||
while (self.i < self.slice.len) {
|
|
||||||
try self.parser.feed(self.slice[self.i], &t1, &t2);
|
|
||||||
self.i += 1;
|
|
||||||
|
|
||||||
if (t1) |token| {
|
|
||||||
self.token = t2;
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Without this a bare number fails, the streaming parser doesn't know the input ended
|
|
||||||
try self.parser.feed(' ', &t1, &t2);
|
|
||||||
self.i += 1;
|
|
||||||
|
|
||||||
if (t1) |token| {
|
|
||||||
return token;
|
|
||||||
} else if (self.parser.complete) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return error.UnexpectedEndOfJson;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Checks to see if a string matches what it would be as a json-encoded string
|
|
||||||
/// Assumes that `encoded` is a well-formed json string
|
|
||||||
fn encodesTo(decoded: []const u8, encoded: []const u8) bool {
|
|
||||||
var i: usize = 0;
|
|
||||||
var j: usize = 0;
|
|
||||||
while (i < decoded.len) {
|
|
||||||
if (j >= encoded.len) return false;
|
|
||||||
if (encoded[j] != '\\') {
|
|
||||||
if (decoded[i] != encoded[j]) return false;
|
|
||||||
j += 1;
|
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
const escape_type = encoded[j + 1];
|
|
||||||
if (escape_type != 'u') {
|
|
||||||
const t: u8 = switch (escape_type) {
|
|
||||||
'\\' => '\\',
|
|
||||||
'/' => '/',
|
|
||||||
'n' => '\n',
|
|
||||||
'r' => '\r',
|
|
||||||
't' => '\t',
|
|
||||||
'f' => 12,
|
|
||||||
'b' => 8,
|
|
||||||
'"' => '"',
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
if (decoded[i] != t) return false;
|
|
||||||
j += 2;
|
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
var codepoint = std.fmt.parseInt(u21, encoded[j + 2 .. j + 6], 16) catch unreachable;
|
|
||||||
j += 6;
|
|
||||||
if (codepoint >= 0xD800 and codepoint < 0xDC00) {
|
|
||||||
// surrogate pair
|
|
||||||
assert(encoded[j] == '\\');
|
|
||||||
assert(encoded[j + 1] == 'u');
|
|
||||||
const low_surrogate = std.fmt.parseInt(u21, encoded[j + 2 .. j + 6], 16) catch unreachable;
|
|
||||||
codepoint = 0x10000 + (((codepoint & 0x03ff) << 10) | (low_surrogate & 0x03ff));
|
|
||||||
j += 6;
|
|
||||||
}
|
|
||||||
var buf: [4]u8 = undefined;
|
|
||||||
const len = std.unicode.utf8Encode(codepoint, &buf) catch unreachable;
|
|
||||||
if (i + len > decoded.len) return false;
|
|
||||||
if (!mem.eql(u8, decoded[i .. i + len], buf[0..len])) return false;
|
|
||||||
i += len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert(i == decoded.len);
|
|
||||||
assert(j == encoded.len);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parse tokens from a stream, returning `false` if they do not decode to `value`
|
|
||||||
fn parsesTo(comptime T: type, value: T, tokens: *TokenStream, options: ParseOptions) !bool {
|
|
||||||
// TODO: should be able to write this function to not require an allocator
|
|
||||||
const tmp = try parse(T, tokens, options);
|
|
||||||
defer parseFree(T, tmp, options);
|
|
||||||
|
|
||||||
return parsedEqual(tmp, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns if a value returned by `parse` is deep-equal to another value
|
|
||||||
fn parsedEqual(a: anytype, b: @TypeOf(a)) bool {
|
|
||||||
switch (@typeInfo(@TypeOf(a))) {
|
|
||||||
.Optional => {
|
|
||||||
if (a == null and b == null) return true;
|
|
||||||
if (a == null or b == null) return false;
|
|
||||||
return parsedEqual(a.?, b.?);
|
|
||||||
},
|
|
||||||
.Union => |info| {
|
|
||||||
if (info.tag_type) |UnionTag| {
|
|
||||||
const tag_a = std.meta.activeTag(a);
|
|
||||||
const tag_b = std.meta.activeTag(b);
|
|
||||||
if (tag_a != tag_b) return false;
|
|
||||||
|
|
||||||
inline for (info.fields) |field_info| {
|
|
||||||
if (@field(UnionTag, field_info.name) == tag_a) {
|
|
||||||
return parsedEqual(@field(a, field_info.name), @field(b, field_info.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
unreachable;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Array => {
|
|
||||||
for (a) |e, i|
|
|
||||||
if (!parsedEqual(e, b[i])) return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.Struct => |info| {
|
|
||||||
inline for (info.fields) |field_info| {
|
|
||||||
if (!parsedEqual(@field(a, field_info.name), @field(b, field_info.name))) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.Pointer => |ptrInfo| switch (ptrInfo.size) {
|
|
||||||
.One => return parsedEqual(a.*, b.*),
|
|
||||||
.Slice => {
|
|
||||||
if (a.len != b.len) return false;
|
|
||||||
for (a) |e, i|
|
|
||||||
if (!parsedEqual(e, b[i])) return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.Many, .C => unreachable,
|
|
||||||
},
|
|
||||||
else => return a == b,
|
|
||||||
}
|
|
||||||
unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ParseOptions = struct {
|
|
||||||
allocator: ?Allocator = null,
|
|
||||||
|
|
||||||
/// Behaviour when a duplicate field is encountered.
|
|
||||||
duplicate_field_behavior: enum {
|
|
||||||
UseFirst,
|
|
||||||
Error,
|
|
||||||
UseLast,
|
|
||||||
} = .Error,
|
|
||||||
|
|
||||||
/// If false, finding an unknown field returns an error.
|
|
||||||
ignore_unknown_fields: bool = false,
|
|
||||||
|
|
||||||
allow_trailing_data: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SkipValueError = error{UnexpectedJsonDepth} || TokenStream.Error;
|
|
||||||
|
|
||||||
fn skipValue(tokens: *TokenStream) SkipValueError!void {
|
|
||||||
const original_depth = tokens.stackUsed();
|
|
||||||
|
|
||||||
// Return an error if no value is found
|
|
||||||
_ = try tokens.next();
|
|
||||||
if (tokens.stackUsed() < original_depth) return error.UnexpectedJsonDepth;
|
|
||||||
if (tokens.stackUsed() == original_depth) return;
|
|
||||||
|
|
||||||
while (try tokens.next()) |_| {
|
|
||||||
if (tokens.stackUsed() == original_depth) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ParseInternalError(comptime T: type) type {
|
|
||||||
// `inferred_types` is used to avoid infinite recursion for recursive type definitions.
|
|
||||||
const inferred_types = [_]type{};
|
|
||||||
return ParseInternalErrorImpl(T, &inferred_types);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ParseInternalErrorImpl(comptime T: type, comptime inferred_types: []const type) type {
|
|
||||||
if (hasCustomParse(T)) {
|
|
||||||
return ParseInternalError(T.JsonParseAs) || T.ParseError;
|
|
||||||
}
|
|
||||||
for (inferred_types) |ty| {
|
|
||||||
if (T == ty) return error{};
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (@typeInfo(T)) {
|
|
||||||
.Bool => return error{UnexpectedToken},
|
|
||||||
.Float, .ComptimeFloat => return error{UnexpectedToken} || std.fmt.ParseFloatError,
|
|
||||||
.Int, .ComptimeInt => {
|
|
||||||
return error{ UnexpectedToken, InvalidNumber, Overflow } ||
|
|
||||||
std.fmt.ParseIntError || std.fmt.ParseFloatError;
|
|
||||||
},
|
|
||||||
.Optional => |optionalInfo| {
|
|
||||||
return ParseInternalErrorImpl(optionalInfo.child, inferred_types ++ [_]type{T});
|
|
||||||
},
|
|
||||||
.Enum => return error{ UnexpectedToken, InvalidEnumTag } || std.fmt.ParseIntError ||
|
|
||||||
std.meta.IntToEnumError || std.meta.IntToEnumError,
|
|
||||||
.Union => |unionInfo| {
|
|
||||||
if (unionInfo.tag_type) |_| {
|
|
||||||
var errors = error{NoUnionMembersMatched};
|
|
||||||
for (unionInfo.fields) |u_field| {
|
|
||||||
errors = errors || ParseInternalErrorImpl(u_field.field_type, inferred_types ++ [_]type{T});
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
} else {
|
|
||||||
@compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Struct => |structInfo| {
|
|
||||||
var errors = error{
|
|
||||||
DuplicateJSONField,
|
|
||||||
UnexpectedEndOfJson,
|
|
||||||
UnexpectedToken,
|
|
||||||
UnexpectedValue,
|
|
||||||
UnknownField,
|
|
||||||
MissingField,
|
|
||||||
} || SkipValueError || TokenStream.Error;
|
|
||||||
for (structInfo.fields) |field| {
|
|
||||||
errors = errors || ParseInternalErrorImpl(field.field_type, inferred_types ++ [_]type{T});
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
},
|
|
||||||
.Array => |arrayInfo| {
|
|
||||||
return error{ UnexpectedEndOfJson, UnexpectedToken } || TokenStream.Error ||
|
|
||||||
UnescapeValidStringError ||
|
|
||||||
ParseInternalErrorImpl(arrayInfo.child, inferred_types ++ [_]type{T});
|
|
||||||
},
|
|
||||||
.Pointer => |ptrInfo| {
|
|
||||||
var errors = error{AllocatorRequired} || std.mem.Allocator.Error;
|
|
||||||
switch (ptrInfo.size) {
|
|
||||||
.One => {
|
|
||||||
return errors || ParseInternalErrorImpl(ptrInfo.child, inferred_types ++ [_]type{T});
|
|
||||||
},
|
|
||||||
.Slice => {
|
|
||||||
return errors || error{ UnexpectedEndOfJson, UnexpectedToken } ||
|
|
||||||
ParseInternalErrorImpl(ptrInfo.child, inferred_types ++ [_]type{T}) ||
|
|
||||||
UnescapeValidStringError || TokenStream.Error;
|
|
||||||
},
|
|
||||||
else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => return error{},
|
|
||||||
}
|
|
||||||
unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseInternal(
|
|
||||||
comptime T: type,
|
|
||||||
token: Token,
|
|
||||||
tokens: *TokenStream,
|
|
||||||
options: ParseOptions,
|
|
||||||
) ParseInternalError(T)!T {
|
|
||||||
if (comptime hasCustomParse(T)) {
|
|
||||||
const val = try parseInternal(T.JsonParseAs, token, tokens, options);
|
|
||||||
defer parseFreeInternal(T.JsonParseAs, val, options);
|
|
||||||
return try T.parse(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (@typeInfo(T)) {
|
|
||||||
.Bool => {
|
|
||||||
return switch (token) {
|
|
||||||
.True => true,
|
|
||||||
.False => false,
|
|
||||||
else => error.UnexpectedToken,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.Float, .ComptimeFloat => {
|
|
||||||
switch (token) {
|
|
||||||
.Number => |numberToken| return try std.fmt.parseFloat(T, numberToken.slice(tokens.slice, tokens.i - 1)),
|
|
||||||
.String => |stringToken| return try std.fmt.parseFloat(T, stringToken.slice(tokens.slice, tokens.i - 1)),
|
|
||||||
else => return error.UnexpectedToken,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Int, .ComptimeInt => {
|
|
||||||
switch (token) {
|
|
||||||
.Number => |numberToken| {
|
|
||||||
if (numberToken.is_integer)
|
|
||||||
return try std.fmt.parseInt(T, numberToken.slice(tokens.slice, tokens.i - 1), 10);
|
|
||||||
const float = try std.fmt.parseFloat(f128, numberToken.slice(tokens.slice, tokens.i - 1));
|
|
||||||
if (@round(float) != float) return error.InvalidNumber;
|
|
||||||
if (float > std.math.maxInt(T) or float < std.math.minInt(T)) return error.Overflow;
|
|
||||||
return @floatToInt(T, float);
|
|
||||||
},
|
|
||||||
.String => |stringToken| {
|
|
||||||
return std.fmt.parseInt(T, stringToken.slice(tokens.slice, tokens.i - 1), 10) catch |err| {
|
|
||||||
switch (err) {
|
|
||||||
error.Overflow => return err,
|
|
||||||
error.InvalidCharacter => {
|
|
||||||
const float = try std.fmt.parseFloat(f128, stringToken.slice(tokens.slice, tokens.i - 1));
|
|
||||||
if (@round(float) != float) return error.InvalidNumber;
|
|
||||||
if (float > std.math.maxInt(T) or float < std.math.minInt(T)) return error.Overflow;
|
|
||||||
return @floatToInt(T, float);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
else => return error.UnexpectedToken,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Optional => |optionalInfo| {
|
|
||||||
if (token == .Null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return try parseInternal(optionalInfo.child, token, tokens, options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Enum => |enumInfo| {
|
|
||||||
switch (token) {
|
|
||||||
.Number => |numberToken| {
|
|
||||||
if (!numberToken.is_integer) return error.UnexpectedToken;
|
|
||||||
const n = try std.fmt.parseInt(enumInfo.tag_type, numberToken.slice(tokens.slice, tokens.i - 1), 10);
|
|
||||||
return try std.meta.intToEnum(T, n);
|
|
||||||
},
|
|
||||||
.String => |stringToken| {
|
|
||||||
const source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
|
|
||||||
switch (stringToken.escapes) {
|
|
||||||
.None => return std.meta.stringToEnum(T, source_slice) orelse return error.InvalidEnumTag,
|
|
||||||
.Some => {
|
|
||||||
inline for (enumInfo.fields) |field| {
|
|
||||||
if (field.name.len == stringToken.decodedLength() and encodesTo(field.name, source_slice)) {
|
|
||||||
return @field(T, field.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return error.InvalidEnumTag;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => return error.UnexpectedToken,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Union => |unionInfo| {
|
|
||||||
if (unionInfo.tag_type) |_| {
|
|
||||||
// try each of the union fields until we find one that matches
|
|
||||||
inline for (unionInfo.fields) |u_field| {
|
|
||||||
// take a copy of tokens so we can withhold mutations until success
|
|
||||||
var tokens_copy = tokens.*;
|
|
||||||
if (parseInternal(u_field.field_type, token, &tokens_copy, options)) |value| {
|
|
||||||
tokens.* = tokens_copy;
|
|
||||||
return @unionInit(T, u_field.name, value);
|
|
||||||
} else |err| {
|
|
||||||
// Bubble up error.OutOfMemory
|
|
||||||
// Parsing some types won't have OutOfMemory in their
|
|
||||||
// error-sets, for the condition to be valid, merge it in.
|
|
||||||
if (@as(@TypeOf(err) || error{OutOfMemory}, err) == error.OutOfMemory) return err;
|
|
||||||
// Bubble up AllocatorRequired, as it indicates missing option
|
|
||||||
if (@as(@TypeOf(err) || error{AllocatorRequired}, err) == error.AllocatorRequired) return err;
|
|
||||||
// otherwise continue through the `inline for`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return error.NoUnionMembersMatched;
|
|
||||||
} else {
|
|
||||||
@compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Struct => |structInfo| {
|
|
||||||
switch (token) {
|
|
||||||
.ObjectBegin => {},
|
|
||||||
else => return error.UnexpectedToken,
|
|
||||||
}
|
|
||||||
var r: T = undefined;
|
|
||||||
var fields_seen = [_]bool{false} ** structInfo.fields.len;
|
|
||||||
errdefer {
|
|
||||||
inline for (structInfo.fields) |field, i| {
|
|
||||||
if (fields_seen[i] and !field.is_comptime) {
|
|
||||||
parseFreeInternal(field.field_type, @field(r, field.name), options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
switch ((try tokens.next()) orelse return error.UnexpectedEndOfJson) {
|
|
||||||
.ObjectEnd => break,
|
|
||||||
.String => |stringToken| {
|
|
||||||
const key_source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
|
|
||||||
var child_options = options;
|
|
||||||
child_options.allow_trailing_data = true;
|
|
||||||
var found = false;
|
|
||||||
inline for (structInfo.fields) |field, i| {
|
|
||||||
// TODO: using switches here segfault the compiler (#2727?)
|
|
||||||
if ((stringToken.escapes == .None and mem.eql(u8, field.name, key_source_slice)) or (stringToken.escapes == .Some and (field.name.len == stringToken.decodedLength() and encodesTo(field.name, key_source_slice)))) {
|
|
||||||
// if (switch (stringToken.escapes) {
|
|
||||||
// .None => mem.eql(u8, field.name, key_source_slice),
|
|
||||||
// .Some => (field.name.len == stringToken.decodedLength() and encodesTo(field.name, key_source_slice)),
|
|
||||||
// }) {
|
|
||||||
if (fields_seen[i]) {
|
|
||||||
// switch (options.duplicate_field_behavior) {
|
|
||||||
// .UseFirst => {},
|
|
||||||
// .Error => {},
|
|
||||||
// .UseLast => {},
|
|
||||||
// }
|
|
||||||
if (options.duplicate_field_behavior == .UseFirst) {
|
|
||||||
// unconditonally ignore value. for comptime fields, this skips check against default_value
|
|
||||||
const next_token = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
|
|
||||||
parseFreeInternal(field.field_type, try parseInternal(field.field_type, next_token, tokens, child_options), child_options);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
} else if (options.duplicate_field_behavior == .Error) {
|
|
||||||
return error.DuplicateJSONField;
|
|
||||||
} else if (options.duplicate_field_behavior == .UseLast) {
|
|
||||||
if (!field.is_comptime) {
|
|
||||||
parseFreeInternal(field.field_type, @field(r, field.name), child_options);
|
|
||||||
}
|
|
||||||
fields_seen[i] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (field.is_comptime) {
|
|
||||||
if (!try parsesTo(field.field_type, @ptrCast(*const field.field_type, field.default_value.?).*, tokens, child_options)) {
|
|
||||||
return error.UnexpectedValue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const next_token = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
|
|
||||||
@field(r, field.name) = try parseInternal(field.field_type, next_token, tokens, child_options);
|
|
||||||
}
|
|
||||||
fields_seen[i] = true;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
if (options.ignore_unknown_fields) {
|
|
||||||
try skipValue(tokens);
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
return error.UnknownField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => return error.UnexpectedToken,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inline for (structInfo.fields) |field, i| {
|
|
||||||
if (!fields_seen[i]) {
|
|
||||||
if (field.default_value) |default_ptr| {
|
|
||||||
if (!field.is_comptime) {
|
|
||||||
const default = @ptrCast(*align(1) const field.field_type, default_ptr).*;
|
|
||||||
@field(r, field.name) = default;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return error.MissingField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
},
|
|
||||||
.Array => |arrayInfo| {
|
|
||||||
switch (token) {
|
|
||||||
.ArrayBegin => {
|
|
||||||
var r: T = undefined;
|
|
||||||
var i: usize = 0;
|
|
||||||
var child_options = options;
|
|
||||||
child_options.allow_trailing_data = true;
|
|
||||||
errdefer {
|
|
||||||
// Without the r.len check `r[i]` is not allowed
|
|
||||||
if (r.len > 0) while (true) : (i -= 1) {
|
|
||||||
parseFreeInternal(arrayInfo.child, r[i], options);
|
|
||||||
if (i == 0) break;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
while (i < r.len) : (i += 1) {
|
|
||||||
const next_token = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
|
|
||||||
r[i] = try parseInternal(arrayInfo.child, next_token, tokens, child_options);
|
|
||||||
}
|
|
||||||
const tok = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
|
|
||||||
switch (tok) {
|
|
||||||
.ArrayEnd => {},
|
|
||||||
else => return error.UnexpectedToken,
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
},
|
|
||||||
.String => |stringToken| {
|
|
||||||
if (arrayInfo.child != u8) return error.UnexpectedToken;
|
|
||||||
var r: T = undefined;
|
|
||||||
const source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
|
|
||||||
switch (stringToken.escapes) {
|
|
||||||
.None => mem.copy(u8, &r, source_slice),
|
|
||||||
.Some => try unescapeValidString(&r, source_slice),
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
},
|
|
||||||
else => return error.UnexpectedToken,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Pointer => |ptrInfo| {
|
|
||||||
const allocator = options.allocator orelse return error.AllocatorRequired;
|
|
||||||
switch (ptrInfo.size) {
|
|
||||||
.One => {
|
|
||||||
const r: T = try allocator.create(ptrInfo.child);
|
|
||||||
errdefer allocator.destroy(r);
|
|
||||||
r.* = try parseInternal(ptrInfo.child, token, tokens, options);
|
|
||||||
return r;
|
|
||||||
},
|
|
||||||
.Slice => {
|
|
||||||
switch (token) {
|
|
||||||
.ArrayBegin => {
|
|
||||||
var arraylist = std.ArrayList(ptrInfo.child).init(allocator);
|
|
||||||
errdefer {
|
|
||||||
while (arraylist.popOrNull()) |v| {
|
|
||||||
parseFreeInternal(ptrInfo.child, v, options);
|
|
||||||
}
|
|
||||||
arraylist.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const tok = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
|
|
||||||
switch (tok) {
|
|
||||||
.ArrayEnd => break,
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
try arraylist.ensureUnusedCapacity(1);
|
|
||||||
const v = try parseInternal(ptrInfo.child, tok, tokens, options);
|
|
||||||
arraylist.appendAssumeCapacity(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ptrInfo.sentinel) |some| {
|
|
||||||
const sentinel_value = @ptrCast(*const ptrInfo.child, some).*;
|
|
||||||
try arraylist.append(sentinel_value);
|
|
||||||
const output = arraylist.toOwnedSlice();
|
|
||||||
return output[0 .. output.len - 1 :sentinel_value];
|
|
||||||
}
|
|
||||||
|
|
||||||
return arraylist.toOwnedSlice();
|
|
||||||
},
|
|
||||||
.String => |stringToken| {
|
|
||||||
if (ptrInfo.child != u8) return error.UnexpectedToken;
|
|
||||||
const source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
|
|
||||||
const len = stringToken.decodedLength();
|
|
||||||
const output = try allocator.alloc(u8, len + @boolToInt(ptrInfo.sentinel != null));
|
|
||||||
errdefer allocator.free(output);
|
|
||||||
switch (stringToken.escapes) {
|
|
||||||
.None => mem.copy(u8, output, source_slice),
|
|
||||||
.Some => try unescapeValidString(output, source_slice),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ptrInfo.sentinel) |some| {
|
|
||||||
const char = @ptrCast(*const u8, some).*;
|
|
||||||
output[len] = char;
|
|
||||||
return output[0..len :char];
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
},
|
|
||||||
else => return error.UnexpectedToken,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
|
|
||||||
}
|
|
||||||
unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ParseError(comptime T: type) type {
|
|
||||||
return ParseInternalError(T) || error{UnexpectedEndOfJson} || TokenStream.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Releases resources created by `parse`.
|
|
||||||
/// Should be called with the same type and `ParseOptions` that were passed to `parse`
|
|
||||||
fn parseFreeInternal(comptime T: type, value: T, options: ParseOptions) void {
|
|
||||||
switch (@typeInfo(T)) {
|
|
||||||
.Bool, .Float, .ComptimeFloat, .Int, .ComptimeInt, .Enum => {},
|
|
||||||
.Optional => {
|
|
||||||
if (value) |v| {
|
|
||||||
return parseFreeInternal(@TypeOf(v), v, options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Union => |unionInfo| {
|
|
||||||
if (unionInfo.tag_type) |UnionTagType| {
|
|
||||||
inline for (unionInfo.fields) |u_field| {
|
|
||||||
if (value == @field(UnionTagType, u_field.name)) {
|
|
||||||
parseFreeInternal(u_field.field_type, @field(value, u_field.name), options);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unreachable;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Struct => |structInfo| {
|
|
||||||
inline for (structInfo.fields) |field| {
|
|
||||||
if (!field.is_comptime) {
|
|
||||||
parseFreeInternal(field.field_type, @field(value, field.name), options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Array => |arrayInfo| {
|
|
||||||
for (value) |v| {
|
|
||||||
parseFreeInternal(arrayInfo.child, v, options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Pointer => |ptrInfo| {
|
|
||||||
const allocator = options.allocator orelse unreachable;
|
|
||||||
switch (ptrInfo.size) {
|
|
||||||
.One => {
|
|
||||||
parseFreeInternal(ptrInfo.child, value.*, options);
|
|
||||||
allocator.destroy(value);
|
|
||||||
},
|
|
||||||
.Slice => {
|
|
||||||
for (value) |v| {
|
|
||||||
parseFreeInternal(ptrInfo.child, v, options);
|
|
||||||
}
|
|
||||||
allocator.free(value);
|
|
||||||
},
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,380 +0,0 @@
|
||||||
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(alloc: std.mem.Allocator, 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(alloc, T, "", "", fields)) orelse error.NoQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decodeString(alloc: std.mem.Allocator, val: []const u8) ![]const 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 parseScalar(alloc: std.mem.Allocator, 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(alloc, T, null),
|
|
||||||
.value => |v| try parseQueryValue(alloc, T, v),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(
|
|
||||||
alloc: std.mem.Allocator,
|
|
||||||
comptime T: type,
|
|
||||||
comptime prefix: []const u8,
|
|
||||||
comptime name: []const u8,
|
|
||||||
fields: anytype,
|
|
||||||
) !?T {
|
|
||||||
if (comptime isScalar(T)) return parseScalar(alloc, 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(alloc, 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(alloc, F, prefix ++ "." ++ name, field.name, fields)) |v| {
|
|
||||||
maybe_value = v;
|
|
||||||
} else if (field.default_value) |default| {
|
|
||||||
if (comptime @sizeOf(F) != 0) {
|
|
||||||
maybe_value = @ptrCast(*const F, @alignCast(@alignOf(F), default)).*;
|
|
||||||
} else {
|
|
||||||
maybe_value = std.mem.zeroes(F);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
std.log.debug("{} {s} {s}", .{ T, prefix, name });
|
|
||||||
return error.PartiallySpecifiedStruct;
|
|
||||||
} else {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Only applies to non-scalar optionals
|
|
||||||
.Optional => |info| return try parse(alloc, 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(alloc: std.mem.Allocator, 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(alloc, 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(alloc: std.mem.Allocator, comptime T: type, value: []const u8) !T {
|
|
||||||
const decoded = try decodeString(alloc, value);
|
|
||||||
errdefer alloc.free(decoded);
|
|
||||||
|
|
||||||
if (comptime std.meta.trait.isZigString(T)) return decoded;
|
|
||||||
|
|
||||||
const result = if (comptime std.meta.trait.isIntegral(T))
|
|
||||||
try std.fmt.parseInt(T, decoded, 0)
|
|
||||||
else if (comptime std.meta.trait.isFloat(T))
|
|
||||||
try std.fmt.parseFloat(T, decoded)
|
|
||||||
else if (comptime std.meta.trait.is(.Enum)(T))
|
|
||||||
std.meta.stringToEnum(T, decoded) orelse return error.InvalidEnumValue
|
|
||||||
else if (T == bool)
|
|
||||||
bool_map.get(value) orelse return error.InvalidBool
|
|
||||||
else if (comptime std.meta.trait.hasFn("parse")(T))
|
|
||||||
try T.parse(value)
|
|
||||||
else
|
|
||||||
@compileError("Invalid type " ++ @typeName(T));
|
|
||||||
|
|
||||||
alloc.free(decoded);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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)) {
|
|
||||||
.Enum => urlFormatString(writer, @tagName(val)),
|
|
||||||
else => std.fmt.format(writer, "{}", .{val}),
|
|
||||||
};
|
|
||||||
|
|
||||||
try writer.writeByte('&');
|
|
||||||
}
|
|
||||||
|
|
||||||
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"));
|
|
||||||
}
|
|
Loading…
Reference in New Issue