mgtzm/src/Item.zig

235 lines
6.6 KiB
Zig

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:<tagname>"
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" => "<tag1> <tag2> <tag3> <tag4>"
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:<id>" => "<tag1> <tag2> <tag3>"
// NOTE: Only assign the whole tag. Ex.: "<tag>:<subtag>" but not "<tag>"
if (tag.len == subtag.len) db.append(item[0..], tag, separator);
// "tag:<tag>" => "<id1> <id2> <id3>"
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:<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;
}