diff --git a/src/api/lib.zig b/src/api/lib.zig index ab19097..4b2a434 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -85,7 +85,7 @@ pub const TimelineArgs = struct { }; pub const TimelineResult = struct { - items: []services.notes.Note, + items: []services.notes.NoteDetailed, prev_page: TimelineArgs, next_page: TimelineArgs, diff --git a/src/api/services/actors.zig b/src/api/services/actors.zig index 2a7f964..30dd6d2 100644 --- a/src/api/services/actors.zig +++ b/src/api/services/actors.zig @@ -13,6 +13,13 @@ pub const CreateError = error{ DatabaseFailure, }; +pub const ActorDetailed = struct { + id: Uuid, + username: []const u8, + host: []const u8, + created_at: DateTime, +}; + pub const LookupError = error{ DatabaseFailure, }; diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index 086a85a..f413f76 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -14,6 +14,17 @@ pub const Note = struct { created_at: DateTime, }; +pub const NoteDetailed = struct { + id: Uuid, + + author: struct { + id: Uuid, + username: []const u8, + }, + content: []const u8, + created_at: DateTime, +}; + pub const CreateError = error{ DatabaseFailure, }; @@ -81,7 +92,7 @@ pub const QueryArgs = struct { }; pub const QueryResult = struct { - items: []Note, + items: []NoteDetailed, prev_page: QueryArgs, next_page: QueryArgs, @@ -91,7 +102,9 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); - try builder.appendSlice(selectStarFromNote ++ + try builder.appendSlice( + \\SELECT note.id, note.content, note.created_at, actor.id AS "author.id", actor.username AS "author.username" + \\FROM note \\ JOIN actor ON actor.id = note.author_id \\ ); @@ -140,7 +153,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul }; const results = try db.queryRowsWithOptions( - Note, + NoteDetailed, try builder.terminate(), query_args, max_items, diff --git a/src/sql/lib.zig b/src/sql/lib.zig index 9b1f599..88c2bf1 100644 --- a/src/sql/lib.zig +++ b/src/sql/lib.zig @@ -120,12 +120,60 @@ const RawResults = union(Engine) { } }; +fn FieldPtr(comptime Ptr: type, comptime names: []const []const u8) type { + if (names.len == 0) return Ptr; + + const T = std.meta.Child(Ptr); + + const field = for (@typeInfo(T).Struct.fields) |f| { + if (std.mem.eql(u8, f.name, names[0])) break f; + } else @compileError("Unknown field " ++ names[0] ++ " in type " ++ @typeName(T)); + + return FieldPtr(*field.field_type, names[1..]); +} + +fn fieldPtr(ptr: anytype, comptime names: []const []const u8) FieldPtr(@TypeOf(ptr), names) { + if (names.len == 0) return ptr; + + return fieldPtr(&@field(ptr.*, names[0]), names[1..]); +} + +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; +} + +fn recursiveFieldPaths(comptime T: type, comptime prefix: []const []const u8) []const []const []const u8 { + comptime { + var fields: []const []const []const u8 = &.{}; + + for (std.meta.fields(T)) |f| { + const full_name = prefix ++ [_][]const u8{f.name}; + if (isScalar(f.field_type)) { + fields = fields ++ [_][]const []const u8{full_name}; + } else { + fields = fields ++ recursiveFieldPaths(f.field_type, full_name); + } + } + + return fields; + } +} + // Represents a set of results. // row() must be called until it returns null, or the query may not complete // Must be deallocated by a call to finish() pub fn Results(comptime T: type) type { // would normally make this a declaration of the struct, but it causes the compiler to crash - const fields = if (T == void) .{} else std.meta.fields(T); + const fields = if (T == void) .{} else recursiveFieldPaths(T, &.{}); return struct { const Self = @This(); @@ -141,13 +189,16 @@ pub fn Results(comptime T: type) type { return Self{ .underlying = underlying, .column_indices = blk: { var indices: [fields.len]u15 = undefined; inline for (fields) |f, i| { - indices[i] = if (!std.meta.trait.isTuple(T)) - underlying.columnIndex(f.name) catch { - std.log.err("Could not find column index for field {s}", .{f.name}); + if (comptime std.meta.trait.isTuple(T)) { + indices[i] = i; + } else { + const name = util.comptimeJoin(".", f); + indices[i] = + underlying.columnIndex(name) catch { + std.log.err("Could not find column index for field {s}", .{name}); return error.ColumnMismatch; - } - else - i; + }; + } } break :blk indices; } }; @@ -168,15 +219,19 @@ pub fn Results(comptime T: type) type { // Iteration bounds must be defined at comptime (inline for) but the number of fields we could // successfully allocate is defined at runtime. So we iterate over the entire field array and // conditionally deallocate fields in the loop. - if (i < fields_allocated) util.deepFree(alloc, @field(result, f.name)); + const ptr = fieldPtr(&result, f); + if (i < fields_allocated) util.deepFree(alloc, ptr.*); }; inline for (fields) |f, i| { // TODO: Causes compiler segfault. why? //const F = f.field_type; - const F = @TypeOf(@field(result, f.name)); - @field(result, f.name) = row_val.get(F, self.column_indices[i], alloc) catch |err| { - std.log.err("SQL: Error getting column {s} of type {}", .{ f.name, F }); + //const F = @TypeOf(@field(result, f.name)); + const F = std.meta.Child(FieldPtr(*@TypeOf(result), f)); + const ptr = fieldPtr(&result, f); + const name = util.comptimeJoin(".", f); + ptr.* = row_val.get(F, self.column_indices[i], alloc) catch |err| { + std.log.err("SQL: Error getting column {s} of type {}", .{ name, F }); return err; }; fields_allocated += 1; diff --git a/src/template/lib.zig b/src/template/lib.zig new file mode 100644 index 0000000..c997629 --- /dev/null +++ b/src/template/lib.zig @@ -0,0 +1,402 @@ +const std = @import("std"); + +pub fn main() !void { + try execute(std.io.getStdOut().writer(), @embedFile("./test.tmp.html"), .{ + .community = .{ .name = "abcd" }, + .foo = [_][]const u8{ "5", "4", "3", "2", "1" }, + .baz = [_][]const []const u8{ + &.{ "5", "4", "3", "2", "1" }, + &.{ "5", "4", "3", "2", "1" }, + }, + }); +} + +pub fn execute(writer: anytype, comptime template: []const u8, args: anytype) !void { + @setEvalBranchQuota(@intCast(u32, template.len * 6)); + const tmpl = comptime parseTemplate(TokenIter{ .text = template }, .root); + try executeTemplate(writer, tmpl.item, args, .{}); +} + +fn executeTemplate(writer: anytype, comptime items: []const TemplateItem, args: anytype, captures: anytype) !void { + inline for (items) |it| switch (it) { + .text => |text| try writer.writeAll(text), + .statement => |stmt| try executeStatement(writer, stmt, args, captures), + }; +} + +fn executeStatement(writer: anytype, comptime stmt: Statement, args: anytype, captures: anytype) !void { + switch (stmt) { + .expression => |expr| { + const val = evaluateExpression(expr, args, captures); + try print(writer, val); + }, + .for_loop => |loop| { + const iterable = evaluateExpression(loop.iterable, args, captures); + const subtemplate = loop.subtemplate; + for (iterable) |v| { + try executeTemplate( + writer, + subtemplate, + args, + addCapture(captures, loop.capture, v), + ); + } + }, + else => @compileError("TODO"), + } +} + +fn print(writer: anytype, arg: anytype) !void { + if (comptime std.meta.trait.isZigString(@TypeOf(arg))) return writer.writeAll(arg); + @compileLog(@TypeOf(arg)); + + @compileError("TODO"); +} + +fn Deref(comptime T: type, comptime names: []const []const u8) type { + if (names.len == 0) return T; + + // Compiler segfaults when I use std.meta to get this info so we search it manually + const field = for (@typeInfo(T).Struct.fields) |f| { + if (std.mem.eql(u8, f.name, names[0])) break f; + } else @compileError("Unknown field " ++ names[0] ++ " in type " ++ @typeName(T)); + + return Deref(field.field_type, names[1..]); +} + +fn deref(arg: anytype, comptime names: []const []const u8) Deref(@TypeOf(arg), names) { + if (names.len == 0) return arg; + return deref(@field(arg, names[0]), names[1..]); +} + +fn EvaluateExpression(comptime expression: Expression, comptime Args: type, comptime Captures: type) type { + return switch (expression) { + .arg_deref => |names| Deref(Args, names), + .capture_deref => |names| Deref(Captures, names), + }; +} + +fn evaluateExpression( + comptime expression: Expression, + args: anytype, + captures: anytype, +) EvaluateExpression(expression, @TypeOf(args), @TypeOf(captures)) { + return switch (expression) { + .arg_deref => |names| deref(args, names), + .capture_deref => |names| deref(captures, names), + }; +} + +fn AddCapture(comptime Root: type, comptime name: []const u8, comptime Val: type) type { + var fields = std.meta.fields(Root) ++ [_]std.builtin.Type.StructField{.{ + .name = name, + .field_type = Val, + .default_value = null, + .is_comptime = false, + .alignment = @alignOf(Val), + }}; + + return @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = fields, + .decls = &.{}, + .is_tuple = false, + } }); +} + +fn addCapture(root: anytype, comptime name: []const u8, val: anytype) AddCapture(@TypeOf(root), name, @TypeOf(val)) { + var result = std.mem.zeroInit(AddCapture(@TypeOf(root), name, @TypeOf(val)), root); + @field(result, name) = val; + return result; +} + +const TemplateType = enum { + root, + subtemplate, +}; + +fn parseTemplate(comptime tokens: TokenIter, comptime template_type: TemplateType) ParseResult([]const TemplateItem) { + comptime { + var iter = tokens; + var items: []const TemplateItem = &.{}; + var current_text: []const u8 = ""; + + parse_loop: while (iter.next()) |token| { + switch (token) { + .whitespace, .text => |text| current_text = current_text ++ text, + .open_bracket => { + const next = iter.peek() orelse @compileError("Unexpected end of template"); + if (next == .open_bracket) { + current_text = current_text ++ "{"; + _ = iter.next(); + } else { + if (current_text.len != 0) { + items = items ++ [_]TemplateItem{.{ .text = current_text }}; + current_text = ""; + } + const result = parseExpressionOrStatement(iter, true); + iter = result.new_iter; + if (result.item == .end_for) { + if (template_type == .subtemplate) break :parse_loop else @compileError("Unexpected end statement"); + } + items = items ++ [_]TemplateItem{.{ .statement = result.item }}; + } + }, + .close_bracket => { + const next = iter.next() orelse @compileError("Unexpected end of template"); + if (next == .close_bracket) current_text = current_text ++ "}" else @compileError("Unpaired close bracket, did you mean \"}}\"?"); + }, + .period => current_text = current_text ++ ".", + .pound => current_text = current_text ++ "#", + .pipe => current_text = current_text ++ "|", + .dollar => current_text = current_text ++ "$", + } + } + + if (current_text.len != 0) { + items = items ++ [_]TemplateItem{.{ .text = current_text }}; + } + + return .{ + .new_iter = iter, + .item = items, + }; + } +} + +fn parseExpressionOrStatement( + comptime tokens: TokenIter, + comptime as_statement: bool, +) ParseResult(if (as_statement) Statement else Expression) { + comptime { + var iter = tokens; + var stmt: Statement = while (iter.next()) |token| switch (token) { + .whitespace => {}, + .pound => { + if (!as_statement) @compileError("Unexpected Token"); + const next = iter.next() orelse @compileError("Unexpected end of template"); + if (next != .text) @compileError("Expected keyword following '#' character"); + const text = next.text; + const keyword = std.meta.stringToEnum(Keyword, text) orelse @compileError("Unknown keyword: " ++ text); + + switch (keyword) { + .end_for => break .{ .end_for = {} }, + .@"for" => { + const result = parseForLoop(iter); + // statemnt already finished so just return + return .{ + .new_iter = result.new_iter, + .item = .{ .for_loop = result.item }, + }; + }, + + //else => @compileError("TODO"), + } + }, + .period => { + const names = parseDeref(iter); + iter = names.new_iter; + break .{ .expression = .{ .arg_deref = names.item } }; + }, + .dollar => { + const names = parseDeref(iter); + iter = names.new_iter; + break .{ .expression = .{ .capture_deref = names.item } }; + }, + else => if (as_statement) @compileError("TODO") else break, + }; + + if (as_statement) { + // search for end of statement + while (iter.next()) |token| switch (token) { + .whitespace => {}, + .close_bracket => return .{ + .new_iter = iter, + .item = stmt, + }, + else => { + @compileLog(iter.row); + @compileError("TODO" ++ @tagName(token)); + }, + }; + + @compileError("Unexpected end of template"); + } else return .{ .new_iter = iter, .item = stmt.expression }; + } +} + +fn skipWhitespace(comptime tokens: TokenIter) TokenIter { + comptime { + var iter = tokens; + while (iter.peek()) |token| switch (token) { + .whitespace => _ = iter.next(), + else => break, + }; + + return iter; + } +} + +fn endStatement(comptime tokens: TokenIter) TokenIter { + comptime { + var iter = skipWhitespace(tokens); + + const token = iter.next() orelse @compileError("Unexpected end of template"); + if (token != .close_bracket) @compileError("Unexpected token"); + return iter; + } +} + +fn parseForLoop(comptime tokens: TokenIter) ParseResult(ForLoop) { + comptime { + const iterable = parseExpressionOrStatement(tokens, false); + var iter = iterable.new_iter; + + iter = skipWhitespace(iter); + { + const token = iter.next() orelse @compileError("Unexpected end of template"); + if (token != .pipe) @compileError("Unexpected token"); + } + { + const token = iter.next() orelse @compileError("Unexpected end of template"); + if (token != .dollar) @compileError("Unexpected token"); + } + const capture = blk: { + const token = iter.next() orelse @compileError("Unexpected end of template"); + if (token != .text) @compileError("Unexpected token"); + break :blk token.text; + }; + { + const token = iter.next() orelse @compileError("Unexpected end of template"); + if (token != .pipe) @compileError("Unexpected token"); + } + iter = endStatement(iter); + + const subtemplate = parseTemplate(iter, .subtemplate); + + return .{ .new_iter = subtemplate.new_iter, .item = .{ .iterable = iterable.item, .subtemplate = subtemplate.item, .capture = capture } }; + } +} + +fn parseDeref(comptime tokens: TokenIter) ParseResult([]const []const u8) { + comptime { + var iter = tokens; + var fields: []const []const u8 = &.{}; + var wants = .text; + while (iter.peek()) |token| { + switch (token) { + .whitespace => {}, + .text => |text| { + if (wants != .text) @compileError("Unexpected token \"" ++ text ++ "\""); + fields = fields ++ [1][]const u8{text}; + wants = .period; + }, + .period => { + if (wants != .period) @compileError("Unexpected token \".\""); + wants = .text; + }, + else => if (wants == .period) return .{ + .new_iter = iter, + .item = fields, + } else @compileError("Unexpected token"), + } + _ = iter.next(); + } + } +} + +fn ParseResult(comptime T: type) type { + return struct { + new_iter: TokenIter, + item: T, + }; +} + +const TemplateItem = union(enum) { + text: []const u8, + statement: Statement, +}; + +const Expression = union(enum) { + arg_deref: []const []const u8, + capture_deref: []const []const u8, +}; + +const ForLoop = struct { + subtemplate: []const TemplateItem, + iterable: Expression, + capture: []const u8, +}; + +const Statement = union(enum) { + expression: Expression, + for_loop: ForLoop, + end_for: void, +}; + +const Keyword = enum { + @"for", + end_for, +}; + +const Token = union(enum) { + text: []const u8, + open_bracket: void, + close_bracket: void, + period: void, + whitespace: []const u8, + pound: void, + pipe: void, + dollar: void, +}; + +const TokenIter = struct { + start: usize = 0, + text: []const u8, + peeked_token: ?Token = null, + + row: usize = 0, + + fn next(self: *TokenIter) ?Token { + if (self.peeked_token) |token| { + self.peeked_token = null; + return token; + } + + const remaining = self.text[self.start..]; + if (remaining.len == 0) return null; + + const ch = remaining[0]; + self.start += 1; + switch (ch) { + '{' => return .{ .open_bracket = {} }, + '}' => return .{ .close_bracket = {} }, + '.' => return .{ .period = {} }, + '#' => return .{ .pound = {} }, + '|' => return .{ .pipe = {} }, + '$' => return .{ .dollar = {} }, + ' ', '\t', '\n', '\r' => { + var idx: usize = 0; + while (idx < remaining.len and std.mem.indexOfScalar(u8, " \t\n\r", remaining[idx]) != null) : (idx += 1) {} + const newline_count = std.mem.count(u8, remaining[0..idx], "\n"); + self.row += newline_count; + + self.start += idx - 1; + return .{ .whitespace = remaining[0..idx] }; + }, + else => { + var idx: usize = 0; + while (idx < remaining.len and std.mem.indexOfScalar(u8, "{}.#|$ \t\n\r", remaining[idx]) == null) : (idx += 1) {} + + self.start += idx - 1; + return .{ .text = remaining[0..idx] }; + }, + } + } + + fn peek(self: *TokenIter) ?Token { + const token = self.next(); + self.peeked_token = token; + return token; + } +}; diff --git a/src/template/test.tmp.html b/src/template/test.tmp.html new file mode 100644 index 0000000..87bac71 --- /dev/null +++ b/src/template/test.tmp.html @@ -0,0 +1,16 @@ + + +
+