const std = @import("std"); const zat = @import("zat"); pub const SpaceUri = struct { authority_did: []const u8, space_type: []const u8, skey: []const u8, author_did: ?[]const u8 = null, collection: ?[]const u8 = null, rkey: ?[]const u8 = null, pub fn parse(uri: []const u8) ?SpaceUri { const prefix = "at://"; if (!std.mem.startsWith(u8, uri, prefix)) return null; const rest = uri[prefix.len..]; const authority_end = std.mem.indexOfScalar(u8, rest, '/') orelse return null; const authority_did = rest[0..authority_end]; if (zat.Did.parse(authority_did) == null) return null; const path = rest[authority_end + 1 ..]; const marker_end = std.mem.indexOfScalar(u8, path, '/') orelse return null; if (!std.mem.eql(u8, path[0..marker_end], "space")) return null; const after_marker = path[marker_end + 1 ..]; const type_end = std.mem.indexOfScalar(u8, after_marker, '/') orelse return null; const space_type = after_marker[0..type_end]; if (zat.Nsid.parse(space_type) == null) return null; const after_type = after_marker[type_end + 1 ..]; const skey_end = std.mem.indexOfScalar(u8, after_type, '/') orelse after_type.len; const skey = after_type[0..skey_end]; if (zat.Rkey.parse(skey) == null) return null; if (skey_end == after_type.len) return .{ .authority_did = authority_did, .space_type = space_type, .skey = skey, }; const record_path = after_type[skey_end + 1 ..]; const author_end = std.mem.indexOfScalar(u8, record_path, '/') orelse return null; const author_did = record_path[0..author_end]; if (zat.Did.parse(author_did) == null) return null; const after_author = record_path[author_end + 1 ..]; const collection_end = std.mem.indexOfScalar(u8, after_author, '/') orelse return null; const collection = after_author[0..collection_end]; if (zat.Nsid.parse(collection) == null) return null; const rkey = after_author[collection_end + 1 ..]; if (std.mem.indexOfScalar(u8, rkey, '/') != null or zat.Rkey.parse(rkey) == null) return null; return .{ .authority_did = authority_did, .space_type = space_type, .skey = skey, .author_did = author_did, .collection = collection, .rkey = rkey, }; } pub fn format( allocator: std.mem.Allocator, authority_did: []const u8, space_type: []const u8, skey: []const u8, ) ![]const u8 { if (zat.Did.parse(authority_did) == null) return error.InvalidDid; if (zat.Nsid.parse(space_type) == null) return error.InvalidNsid; if (zat.Rkey.parse(skey) == null) return error.InvalidRecordKey; return std.fmt.allocPrint(allocator, "at://{s}/space/{s}/{s}", .{ authority_did, space_type, skey }); } pub fn formatRecord( allocator: std.mem.Allocator, space: []const u8, author_did: []const u8, collection: []const u8, rkey: []const u8, ) ![]const u8 { const parsed = parse(space) orelse return error.InvalidSpaceUri; if (parsed.author_did != null) return error.InvalidSpaceUri; if (zat.Did.parse(author_did) == null) return error.InvalidDid; if (zat.Nsid.parse(collection) == null) return error.InvalidNsid; if (zat.Rkey.parse(rkey) == null) return error.InvalidRecordKey; return std.fmt.allocPrint(allocator, "{s}/{s}/{s}/{s}", .{ space, author_did, collection, rkey }); } }; test "parses canonical permissioned space uri" { const parsed = SpaceUri.parse("at://did:plc:abc/space/fm.plyr.privateMedia/self").?; try std.testing.expectEqualStrings("did:plc:abc", parsed.authority_did); try std.testing.expectEqualStrings("fm.plyr.privateMedia", parsed.space_type); try std.testing.expectEqualStrings("self", parsed.skey); } test "parses canonical permissioned record uri" { const parsed = SpaceUri.parse("at://did:plc:abc/space/fm.plyr.privateMedia/self/did:plc:author/fm.plyr.track/key").?; try std.testing.expectEqualStrings("did:plc:author", parsed.author_did.?); try std.testing.expectEqualStrings("fm.plyr.track", parsed.collection.?); try std.testing.expectEqualStrings("key", parsed.rkey.?); } test "formats canonical permissioned record uri" { const uri = try SpaceUri.formatRecord(std.testing.allocator, "at://did:plc:abc/space/fm.plyr.privateMedia/self", "did:plc:author", "fm.plyr.track", "key"); defer std.testing.allocator.free(uri); try std.testing.expectEqualStrings("at://did:plc:abc/space/fm.plyr.privateMedia/self/did:plc:author/fm.plyr.track/key", uri); } test "rejects legacy and malformed space uris" { try std.testing.expect(SpaceUri.parse("ats://did:plc:abc/fm.plyr.privateMedia/self") == null); try std.testing.expect(SpaceUri.parse("at://did:plc:abc/fm.plyr.privateMedia/self") == null); try std.testing.expect(SpaceUri.parse("at://did:plc:abc/space/fm.plyr.privateMedia/self/did:plc:author") == null); try std.testing.expect(SpaceUri.parse("at://did:plc:abc/space/fm.plyr.privateMedia/self/did:plc:author/fm.plyr.track/key/extra") == null); }