diff --git a/src/http/json.zig b/src/http/json.zig new file mode 100644 index 0000000..21474cc --- /dev/null +++ b/src/http/json.zig @@ -0,0 +1,677 @@ +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, + } +} diff --git a/src/http/query.zig b/src/http/query.zig new file mode 100644 index 0000000..1933429 --- /dev/null +++ b/src/http/query.zig @@ -0,0 +1,380 @@ +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")); +}