const std = @import("std"); const zat = @import("zat"); pub const Error = error{ InvalidAtUri, InvalidCollection, InvalidRepoPath, InvalidRecordKey, MissingCollection, MissingRecordKey, }; pub const RecordRef = struct { uri: zat.AtUri, cid: ?[]const u8 = null, pub fn parse(uri: []const u8, cid: ?[]const u8) Error!RecordRef { const parsed = zat.AtUri.parse(uri) orelse return Error.InvalidAtUri; if (!parsed.hasCollection()) return Error.MissingCollection; if (!parsed.hasRkey()) return Error.MissingRecordKey; return .{ .uri = parsed, .cid = cid }; } }; pub const RepoPath = struct { raw: []const u8, slash: usize, pub fn parse(path: []const u8) Error!RepoPath { const slash = std.mem.indexOfScalar(u8, path, '/') orelse return Error.InvalidRepoPath; if (std.mem.indexOfScalarPos(u8, path, slash + 1, '/') != null) return Error.InvalidRepoPath; if (zat.Nsid.parse(path[0..slash]) == null) return Error.InvalidCollection; if (zat.Rkey.parse(path[slash + 1 ..]) == null) return Error.InvalidRecordKey; return .{ .raw = path, .slash = slash }; } pub fn collection(self: RepoPath) []const u8 { return self.raw[0..self.slash]; } pub fn rkey(self: RepoPath) []const u8 { return self.raw[self.slash + 1 ..]; } }; pub const WriteAction = enum { create, update, delete, }; pub const WriteOp = struct { action: WriteAction, path: RepoPath, cid: ?[]const u8 = null, prev: ?[]const u8 = null, pub fn create(path: []const u8, cid: []const u8) Error!WriteOp { return .{ .action = .create, .path = try RepoPath.parse(path), .cid = cid }; } pub fn update(path: []const u8, cid: []const u8, prev: []const u8) Error!WriteOp { return .{ .action = .update, .path = try RepoPath.parse(path), .cid = cid, .prev = prev }; } pub fn delete(path: []const u8, prev: []const u8) Error!WriteOp { return .{ .action = .delete, .path = try RepoPath.parse(path), .prev = prev }; } }; test "parses repo paths" { const path = try RepoPath.parse("app.bsky.feed.post/3jxtb5w2hkt2m"); try std.testing.expectEqualStrings("app.bsky.feed.post", path.collection()); try std.testing.expectEqualStrings("3jxtb5w2hkt2m", path.rkey()); } test "requires full record refs" { const ref = try RecordRef.parse("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jxtb5w2hkt2m", "bafyreiblah"); try std.testing.expectEqualStrings("app.bsky.feed.post", ref.uri.collection().?); try std.testing.expectError(Error.MissingRecordKey, RecordRef.parse("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post", null)); } test "models sync write ops" { const op = try WriteOp.update("app.bsky.feed.post/3jxtb5w2hkt2m", "new", "old"); try std.testing.expectEqual(.update, op.action); try std.testing.expectEqualStrings("old", op.prev.?); }