diff --git a/src/http/server/response.zig b/src/http/server/response.zig index 057c51a..296382d 100644 --- a/src/http/server/response.zig +++ b/src/http/server/response.zig @@ -38,11 +38,7 @@ fn writeStatusLine(writer: anytype, status: Status) !void { fn writeFields(writer: anytype, headers: *const Fields) !void { var iter = headers.iterator(); - std.log.debug("{any}", .{headers}); - std.log.debug("{any}", .{iter}); while (iter.next()) |header| { - std.log.debug("{any}", .{headers}); - std.log.debug("{any}", .{iter}); for (header.value_ptr.*) |ch| { if (ch == '\r' or ch == '\n') @panic("newlines not yet supported in headers"); } diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 27f90e7..902711e 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -153,7 +153,6 @@ pub fn Context(comptime Route: type) type { if (Body != void) { var stream = req.body orelse return error.NoBody; const body = try stream.reader().readAllAlloc(self.allocator, 1 << 16); - std.log.debug("{s}", .{body}); errdefer self.allocator.free(body); self.body = try json_utils.parse(Body, body, self.allocator); self.body_buf = body; diff --git a/src/main/controllers/communities.zig b/src/main/controllers/communities.zig index b7d33e9..7e1995a 100644 --- a/src/main/controllers/communities.zig +++ b/src/main/controllers/communities.zig @@ -28,7 +28,7 @@ pub const query = struct { // NOTE: This has to match QueryArgs // TODO: Support union fields in query strings natively, so we don't // have to keep these in sync - pub const Query = struct { + pub const QueryOld = struct { const OrderBy = QueryArgs.OrderBy; const Direction = QueryArgs.Direction; const PageDirection = QueryArgs.PageDirection; @@ -62,41 +62,10 @@ pub const query = struct { pub const format = formatQueryParams; }; + pub const Query = QueryArgs; + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const q = req.query; - const query_matches = if (q.prev.id) |_| switch (q.order_by) { - .name => q.prev.name != null and q.prev.host == null and q.prev.created_at == null, - .host => q.prev.name == null and q.prev.host != null and q.prev.created_at == null, - .created_at => q.prev.name == null and q.prev.host == null and q.prev.created_at != null, - } else (q.prev.name == null and q.prev.host == null and q.prev.created_at == null); - - if (!query_matches) return res.err(.bad_request, "prev.* parameters do not match", {}); - - const prev_arg: ?QueryArgs.Prev = if (q.prev.id) |id| .{ - .id = id, - .order_val = switch (q.order_by) { - .name => .{ .name = q.prev.name.? }, - .host => .{ .host = q.prev.host.? }, - .created_at => .{ .created_at = q.prev.created_at.? }, - }, - } else null; - - const query_args = QueryArgs{ - .max_items = q.max_items, - .owner_id = q.owner_id, - .like = q.like, - .created_before = q.created_before, - .created_after = q.created_after, - - .order_by = q.order_by, - .direction = q.direction, - - .prev = prev_arg, - - .page_direction = q.page_direction, - }; - - const results = try srv.queryCommunities(query_args); + const results = try srv.queryCommunities(req.query); var link = std.ArrayList(u8).init(req.allocator); const link_writer = link.writer(); @@ -187,26 +156,28 @@ fn formatRecursive(comptime prefix: []const u8, params: anytype, writer: anytype } fn queryArgsToControllerQuery(args: QueryArgs) query.Query { - var result = query.Query{ - .max_items = args.max_items, - .owner_id = args.owner_id, - .like = args.like, - .created_before = args.created_before, - .created_after = args.created_after, - .order_by = args.order_by, - .direction = args.direction, - .prev = .{}, - .page_direction = args.page_direction, - }; + return args; - if (args.prev) |prev| { - result.prev = .{ - .id = prev.id, - .name = if (prev.order_val == .name) prev.order_val.name else null, - .host = if (prev.order_val == .host) prev.order_val.host else null, - .created_at = if (prev.order_val == .created_at) prev.order_val.created_at else null, - }; - } + // var result = query.Query{ + // .max_items = args.max_items, + // .owner_id = args.owner_id, + // .like = args.like, + // .created_before = args.created_before, + // .created_after = args.created_after, + // .order_by = args.order_by, + // .direction = args.direction, + // .prev = .{}, + // .page_direction = args.page_direction, + // }; - return result; + // if (args.prev) |prev| { + // result.prev = .{ + // .id = prev.id, + // .name = if (prev.order_val == .name) prev.order_val.name else null, + // .host = if (prev.order_val == .host) prev.order_val.host else null, + // .created_at = if (prev.order_val == .created_at) prev.order_val.created_at else null, + // }; + // } + + // return result; } diff --git a/src/main/query.zig b/src/main/query.zig index e3bc725..75f9051 100644 --- a/src/main/query.zig +++ b/src/main/query.zig @@ -71,37 +71,132 @@ const QueryIter = @import("util").QueryIter; /// TODO: values are currently case-sensitive, and are not url-decoded properly. /// This should be fixed. pub fn parseQuery(comptime T: type, query: []const u8) !T { - //if (!std.meta.trait.isContainer(T)) @compileError("T must be a struct"); + if (comptime !std.meta.trait.isContainer(T)) @compileError("T must be a struct"); var iter = QueryIter.from(query); - var result = T{}; + + var fields = Intermediary(T){}; while (iter.next()) |pair| { - try parseQueryPair(T, &result, pair.key, pair.value); + // 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 result; + return (try parse(T, "", "", fields)).?; } -fn parseQueryPair(comptime T: type, result: *T, key: []const u8, value: ?[]const u8) !void { - const key_part = std.mem.sliceTo(key, '.'); - const field_idx = std.meta.stringToEnum(std.meta.FieldEnum(T), key_part) orelse return error.UnknownField; +fn parseScalar(comptime T: type, comptime name: []const u8, fields: anytype) !?T { + const param = @field(fields, name); + return switch (param) { + .not_specified => null, + .no_value => try parseQueryValue(T, null), + .value => |v| try parseQueryValue(T, v), + }; +} - inline for (std.meta.fields(T)) |info, idx| { - if (@enumToInt(field_idx) == idx) { - if (comptime isScalar(info.field_type)) { - if (key_part.len == key.len) { - @field(result, info.name) = try parseQueryValue(info.field_type, value); - return; - } else { - return error.UnknownField; +fn parse(comptime T: type, comptime prefix: []const u8, comptime name: []const u8, fields: anytype) !?T { + if (comptime isScalar(T)) return parseScalar(T, prefix ++ "." ++ name, fields); + switch (@typeInfo(T)) { + .Union => |info| { + var result: ?T = null; + inline for (info.fields) |field| { + const F = field.field_type; + + const maybe_value = try parse(F, prefix, field.name, fields); + if (maybe_value) |value| { + if (result != null) return error.DuplicateUnionField; + + result = @unionInit(T, field.name, value); } + } + std.log.debug("{any}", .{result}); + return result; + }, + + .Struct => |info| { + var result: T = undefined; + var fields_specified: usize = 0; + + inline for (info.fields) |field| { + const F = field.field_type; + + var maybe_value: ?F = null; + if (try parse(F, prefix ++ "." ++ name, field.name, fields)) |v| { + maybe_value = v; + } else if (field.default_value) |default| { + maybe_value = @ptrCast(*const F, @alignCast(@alignOf(F), default)).*; + } + + if (maybe_value) |v| { + fields_specified += 1; + @field(result, field.name) = v; + } + } + + if (fields_specified == 0) { + return null; + } else if (fields_specified != info.fields.len) { + return error.PartiallySpecifiedStruct; } else { - const remaining = std.mem.trimLeft(u8, key[key_part.len..], "."); - return try parseQueryPair(info.field_type, &@field(result, info.name), remaining, value); + return result; + } + }, + + // Only applies to non-scalar optionals + .Optional => |info| return try parse(info.child, prefix, name, fields), + + else => @compileError("tmp"), + } +} + +fn recursiveFieldPaths(comptime T: type, comptime prefix: []const u8) []const []const u8 { + comptime { + if (std.meta.trait.is(.Optional)(T)) return recursiveFieldPaths(std.meta.Child(T), prefix); + + var fields: []const []const u8 = &.{}; + + for (std.meta.fields(T)) |f| { + const full_name = prefix ++ f.name; + + if (isScalar(f.field_type)) { + fields = fields ++ @as([]const []const u8, &.{full_name}); + } else { + const field_prefix = if (@typeInfo(f.field_type) == .Union) prefix else full_name ++ "."; + fields = fields ++ recursiveFieldPaths(f.field_type, field_prefix); } } - } - return error.UnknownField; + return fields; + } +} + +const QueryParam = union(enum) { + not_specified: void, + no_value: void, + value: []const u8, +}; + +fn Intermediary(comptime T: type) type { + const field_names = recursiveFieldPaths(T, ".."); + + var fields: [field_names.len]std.builtin.Type.StructField = undefined; + for (field_names) |name, i| fields[i] = .{ + .name = name, + .field_type = QueryParam, + .default_value = &QueryParam{ .not_specified = {} }, + .is_comptime = false, + .alignment = @alignOf(QueryParam), + }; + + return @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = &fields, + .decls = &.{}, + .is_tuple = false, + } }); } fn parseQueryValue(comptime T: type, value: ?[]const u8) !T {