const std = @import("std"); const json = @import("json.zig"); const Db = @import("Db.zig"); const Self = @This(); const Tag = [:0]u8; id: ?[4:0]u8, // If null, the object hasn't been persisted tags: ?[]Tag, allocator: ?std.mem.Allocator = null, pub fn deinit(self: *Self) void { // If there's no allocator there has been no allocations const allocator = self.allocator orelse return; const tags = self.tags orelse return; // Free each tag for (tags) |tag| { // Free name allocator.free(tag); } // Free the tags buffer allocator.free(tags); } const SubtagIterator = struct { index: ?usize, orig_tag: []const u8, pub fn init(tag: []const u8) @This() { return @This(){ .index = 0, .orig_tag = tag, }; } pub fn next(self: *@This()) ?[]const u8 { if (self.index) |index| { const opt_i = std.mem.indexOfScalarPos(u8, self.orig_tag, index, ':'); if (opt_i) |i| { defer self.index = i + 1; return self.orig_tag[0..i]; } // Use the nullability as a marker to not enter here again defer self.index = null; return self.orig_tag[0..]; } return null; } }; pub fn persist(self: *Self, db: *Db, allocator: std.mem.Allocator) !void { // Insert only if there's no ID. if (self.id == null) { var opt_last_id = db.get("item:last"); defer if (opt_last_id) |i| Db.free(i.ptr); // Get ID. Should be last+1 or "0000" const id = if (opt_last_id) |last_id| try Db.numEncode(Db.numDecode(last_id) + 1) else try Db.numEncode(0); // Insert db.append("item", &id, " "); db.set("item:last", &id); // Update the ID field self.id = [_:0]u8{0} ** 4; std.mem.copy(u8, self.id.?[0..], id[0..]); } // If the tags haven't been initialized, don't touch anything if (self.tags) |tags| { const separator = " "; var item = "item:----".*; std.mem.copy(u8, item[5..], self.id.?[0..]); for (tags) |tag| { // Go through every subtag (tag -> tag:subtag -> tag:subtag:subsubtag -> ...) var iter = SubtagIterator.init(tag); while (iter.next()) |subtag| { // allocate "tag:" var tagkey = try std.mem.concat(allocator, u8, &[_][]const u8{ "tag:", subtag }); defer allocator.free(tagkey); // Append the tag to the total of tags if it doesn't exist // "tag" => " " if (!db.check(tagkey)) db.append("tag", subtag, separator); // Skip if the item is already tagged with it // TODO: Test if this works, lol if (db.isInList(&item, subtag)) continue; // Insert the tag into the item. // "item:" => " " // NOTE: Only assign the whole tag. Ex.: ":" but not "" if (tag.len == subtag.len) db.append(item[0..], tag, separator); // "tag:" => " " db.append(tagkey, &self.id.?, separator); } } } } // TODO: Return error or something, check for it pub fn delete(self: Self, db: *Db) !void { // TODO: Throw proper error const id = self.id orelse return; var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); // Delete the id inside the tags if (self.tags) |tags| { for (tags) |tag| { // Get the tag selector: "tag:" // TODO: Put this inside a function so is is comfier const tag_sel = try std.mem.concat(allocator, u8, &[_][]const u8{ "tag:", tag }); defer allocator.free(tag_sel); // TODO: See what to do when the tag doesn't exist // TODO: Grab when it's the last of that tag db.removeFromList(tag_sel, tag); } } var item_sel = "item:----".*; std.mem.copy(u8, item_sel[5..], &id); // Finally delete the item per se // TODO: Return error on error _ = db.remove(&item_sel); // Remove from the global item list db.removeFromList("item", &id); } pub fn getById(id: []const u8, db: *Db, allocator: std.mem.Allocator) !?Self { // TODO: Create a function that returns the selector var item_sel = "item:----".*; std.mem.copy(u8, item_sel[5..], id[0..]); const tags = (try db.getList(&item_sel, allocator)) orelse return null; var aid: [4:0]u8 = "0000".*; std.mem.copy(u8, &aid, id); return Self{ .id = aid, .tags = tags, .allocator = allocator, }; } /// Convert into a JSON object. A call to deinit() is necessary from the caller's side pub fn toJson(self: Self) json.Obj { // Main object var jobj = json.Obj.newObject(); // Add id (i64|null) jobj.objectAdd("id", if (self.id) |id| &json.Obj.newString(&id) else null); // Add tags only if they're initialized if (self.tags) |tags| { var jtags = json.Obj.newArray(); jobj.objectAdd("tags", &jtags); for (tags) |tag_i| { jtags.arrayAdd(&json.Obj.newString(tag_i)); } } else { // If nothing is found, set it to null // just to make it more explicit jobj.objectAdd("tags", null); } return jobj; } pub fn fromJson(jobj: json.Obj, allocator: std.mem.Allocator) !Self { // Try to assemble a Item object from a JSON string // An item could be just an id, just tags or both. var tags: ?[]Tag = null; if (jobj.objectGet("tags") catch null) |*jtags| { defer jtags.deinit(); tags = try tagsFromJson(jobj, allocator); } var id: ?[4:0]u8 = null; if (jobj.objectGet("id") catch null) |*jid| { defer jid.deinit(); std.mem.copy(u8, id, jid.getString()); } // TODO: What should be done when both things are null? // Return a null Obj or leave it as-is? return Self{ .id = id, .tags = tags, .allocator = allocator, }; } pub fn tagsFromJson(jobj: *const json.Obj, allocator: std.mem.Allocator) ![]Tag { // Reserve space for slice of tags const len = @intCast(usize, jobj.arrayLen()); var tags = try allocator.alloc(Tag, len); var iter = jobj.arrayGetIterator(); var i: usize = 0; while (iter.next()) |*tag| { // Whe know it's not null at this point tags[i] = try allocator.dupeZ(u8, tag.getString()); i += 1; } return tags; }