2022-10-20 11:23:15 +00:00
|
|
|
const std = @import("std");
|
2022-10-21 18:38:16 +00:00
|
|
|
|
|
|
|
const json = @import("json.zig");
|
2022-10-20 11:23:15 +00:00
|
|
|
const Db = @import("Db.zig");
|
2022-10-21 18:38:16 +00:00
|
|
|
|
2022-10-21 16:58:44 +00:00
|
|
|
const Self = @This();
|
2022-10-31 07:55:35 +00:00
|
|
|
const Tag = [:0]u8;
|
2022-10-20 11:23:15 +00:00
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
id: ?[4:0]u8, // If null, the object hasn't been persisted
|
2022-10-20 11:23:15 +00:00
|
|
|
tags: ?[]Tag,
|
2022-10-22 03:15:36 +00:00
|
|
|
allocator: ?std.mem.Allocator = null,
|
2022-10-21 16:58:44 +00:00
|
|
|
|
2022-10-22 03:15:36 +00:00
|
|
|
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
|
2022-10-31 07:55:35 +00:00
|
|
|
for (tags) |tag| {
|
2022-10-27 00:54:00 +00:00
|
|
|
// Free name
|
2022-10-31 07:55:35 +00:00
|
|
|
allocator.free(tag);
|
2022-10-22 03:15:36 +00:00
|
|
|
}
|
2022-10-21 16:58:44 +00:00
|
|
|
|
2022-10-22 03:15:36 +00:00
|
|
|
// Free the tags buffer
|
|
|
|
allocator.free(tags);
|
|
|
|
}
|
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
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 {
|
2022-10-21 16:58:44 +00:00
|
|
|
// Insert only if there's no ID.
|
|
|
|
if (self.id == null) {
|
2022-10-31 07:55:35 +00:00
|
|
|
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);
|
2022-10-21 16:58:44 +00:00
|
|
|
|
|
|
|
// Update the ID field
|
2022-10-31 07:55:35 +00:00
|
|
|
self.id = [_:0]u8{0} ** 4;
|
|
|
|
std.mem.copy(u8, self.id.?[0..], id[0..]);
|
2022-10-21 16:58:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the tags haven't been initialized, don't touch anything
|
|
|
|
if (self.tags) |tags| {
|
2022-10-31 07:55:35 +00:00
|
|
|
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);
|
|
|
|
}
|
2022-10-21 16:58:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-10-21 18:38:16 +00:00
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
pub fn getById(id: []const u8, db: *Db, allocator: std.mem.Allocator) !?Self {
|
|
|
|
var item_sel = "item:----".*;
|
|
|
|
std.mem.copy(u8, item_sel[5..], id[0..]);
|
2022-10-24 00:49:59 +00:00
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
const tags = (try db.getList(&item_sel, allocator)) orelse return null;
|
2022-10-24 00:49:59 +00:00
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
var aid: [4:0]u8 = "0000".*;
|
|
|
|
std.mem.copy(u8, &aid, id);
|
2022-10-24 00:49:59 +00:00
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
return Self{
|
|
|
|
.id = aid,
|
|
|
|
.tags = tags,
|
|
|
|
};
|
2022-10-24 00:49:59 +00:00
|
|
|
}
|
|
|
|
|
2022-10-21 18:38:16 +00:00
|
|
|
/// Convert into a JSON object. A call to deinit() is necessary from the caller's side
|
|
|
|
pub fn toJson(self: Self) json.Obj {
|
2022-10-22 03:15:36 +00:00
|
|
|
// Main object
|
2022-10-21 18:38:16 +00:00
|
|
|
var jobj = json.Obj.newObject();
|
|
|
|
|
2022-10-22 03:15:36 +00:00
|
|
|
// Add id (i64|null)
|
2022-10-31 07:55:35 +00:00
|
|
|
jobj.objectAdd("id", if (self.id) |id| &json.Obj.newString(&id) else null);
|
2022-10-21 18:38:16 +00:00
|
|
|
|
2022-10-22 03:15:36 +00:00
|
|
|
// Add tags only if they're initialized
|
2022-10-21 18:38:16 +00:00
|
|
|
if (self.tags) |tags| {
|
2022-10-31 07:55:35 +00:00
|
|
|
var jtags = json.Obj.newArray();
|
2022-10-23 21:24:28 +00:00
|
|
|
jobj.objectAdd("tags", &jtags);
|
|
|
|
|
2022-10-21 18:38:16 +00:00
|
|
|
for (tags) |tag_i| {
|
2022-10-31 07:55:35 +00:00
|
|
|
jtags.arrayAdd(&json.Obj.newString(tag_i));
|
2022-10-21 18:38:16 +00:00
|
|
|
}
|
2022-10-23 21:24:28 +00:00
|
|
|
} else {
|
|
|
|
// If nothing is found, set it to null
|
|
|
|
// just to make it more explicit
|
|
|
|
jobj.objectAdd("tags", null);
|
2022-10-21 18:38:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return jobj;
|
|
|
|
}
|
2022-10-22 03:15:36 +00:00
|
|
|
|
|
|
|
pub fn fromJson(jobj: json.Obj, allocator: std.mem.Allocator) !Self {
|
2022-10-23 21:24:28 +00:00
|
|
|
// Try to assemble a Item object from a JSON string
|
|
|
|
// An item could be just an id, just tags or both.
|
2022-10-22 03:15:36 +00:00
|
|
|
|
2022-10-23 21:24:28 +00:00
|
|
|
var tags: ?[]Tag = null;
|
2022-10-22 03:15:36 +00:00
|
|
|
|
2022-10-23 22:10:48 +00:00
|
|
|
if (jobj.objectGet("tags") catch null) |*jtags| {
|
2022-10-23 21:24:28 +00:00
|
|
|
defer jtags.deinit();
|
2022-10-27 00:54:00 +00:00
|
|
|
tags = try tagsFromJson(jobj, allocator);
|
2022-10-22 03:15:36 +00:00
|
|
|
}
|
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
var id: ?[4:0]u8 = null;
|
2022-10-23 22:10:48 +00:00
|
|
|
|
|
|
|
if (jobj.objectGet("id") catch null) |*jid| {
|
|
|
|
defer jid.deinit();
|
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
std.mem.copy(u8, id, jid.getString());
|
2022-10-23 22:10:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: What should be done when both things are null?
|
|
|
|
// Return a null Obj or leave it as-is?
|
|
|
|
|
2022-10-22 03:15:36 +00:00
|
|
|
return Self{
|
2022-10-23 22:10:48 +00:00
|
|
|
.id = id,
|
2022-10-22 03:15:36 +00:00
|
|
|
.tags = tags,
|
|
|
|
.allocator = allocator,
|
|
|
|
};
|
|
|
|
}
|
2022-10-27 00:54:00 +00:00
|
|
|
|
|
|
|
pub fn tagsFromJson(jobj: *json.Obj, allocator: std.mem.Allocator) ![]Tag {
|
|
|
|
// Reserve space for slice of tags
|
2022-10-31 07:55:35 +00:00
|
|
|
const len = @intCast(usize, jobj.arrayLen());
|
2022-10-27 00:54:00 +00:00
|
|
|
var tags = try allocator.alloc(Tag, len);
|
|
|
|
|
2022-10-31 07:55:35 +00:00
|
|
|
var iter = jobj.arrayGetIterator();
|
2022-10-27 00:54:00 +00:00
|
|
|
|
|
|
|
var i: usize = 0;
|
|
|
|
while (iter.next()) |*tag| {
|
|
|
|
// Whe know it's not null at this point
|
2022-10-31 07:55:35 +00:00
|
|
|
tags[i] = try allocator.dupeZ(u8, tag.getString());
|
2022-10-27 00:54:00 +00:00
|
|
|
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return tags;
|
|
|
|
}
|