const std = @import("std"); const Allocator = std.mem.Allocator; pub const DidError = error{ InvalidDid, UnsupportedMethod, }; pub const Method = enum { plc, web, }; pub const Did = union(Method) { plc: [24]u8, web: Web, pub fn init(bytes: []const u8) DidError!Did { var iter = std.mem.splitScalar(u8, bytes, ':'); if (!std.mem.eql(u8, iter.first(), "did")) { return error.InvalidDid; } const kind_str = iter.next() orelse return error.InvalidDid; const kind = std.meta.stringToEnum(Method, kind_str) orelse return error.UnsupportedMethod; switch (kind) { .plc => { const id = iter.next() orelse return error.InvalidDid; if (id.len != 24) return error.InvalidDid; var did: [24]u8 = undefined; for (id, 0..) |c, i| { switch (c) { 'A'...'Z' => did[i] = c + 0x20, 'a'...'z', '2'...'7' => did[i] = c, else => return error.InvalidDid, } } return .{ .plc = did }; }, .web => { const id = iter.next() orelse return error.InvalidDid; const host: []const u8, const port: u16 = blk: { const end = std.mem.indexOfScalar(u8, id, '%') orelse break :blk .{ id, 443 }; const port = std.fmt.parseInt(u16, id[end + 3 ..], 10) catch return error.InvalidDid; break :blk .{ id[0..end], port }; }; return .{ .web = .{ .host = host, .port = port, .path = iter.rest(), } }; }, } } pub fn format( self: Did, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype, ) !void { switch (self) { .plc => |plc| try writer.print("did:plc:{s}", .{&plc}), .web => |web| try writer.print("did:web:{s}", .{web}), } } }; pub const Web = struct { host: []const u8, port: u16 = 443, path: []const u8 = "", pub fn format( self: Web, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype, ) !void { try writer.print("{s}", .{self.host}); switch (self.port) { 443 => {}, else => try writer.print("%3A{d}", .{self.port}), } if (self.path.len > 0) { try writer.print(":{s}", .{self.path}); } } }; test "did:plc: valid" { const id = "aBcDejfkllalsjlkllsjk234"; const did = try Did.init("did:plc:" ++ id); var out: [24]u8 = undefined; try std.testing.expectEqualStrings(std.ascii.lowerString(&out, id), &did.plc); } test "did:plc: invalid character" { const did = "did:plc:0BcDejfkllalsjlkllsjk234"; try std.testing.expectError(DidError.InvalidDid, Did.init(did)); } test "did:plc: invalid length" { const did = "did:plc:kllsjk234"; try std.testing.expectError(DidError.InvalidDid, Did.init(did)); } test "did:web: host only" { const did = "did:web:rockorager.dev"; const actual = try Did.init(did); try std.testing.expectEqualStrings("rockorager.dev", actual.web.host); try std.testing.expectEqual(443, actual.web.port); try std.testing.expectEqualStrings("", actual.web.path); const fmt = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{actual}); defer std.testing.allocator.free(fmt); try std.testing.expectEqualStrings("did:web:rockorager.dev", fmt); } test "did:web: host + port" { const did = "did:web:rockorager.dev%3a80"; const actual = try Did.init(did); try std.testing.expectEqualStrings("rockorager.dev", actual.web.host); try std.testing.expectEqual(80, actual.web.port); try std.testing.expectEqualStrings("", actual.web.path); const fmt = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{actual}); defer std.testing.allocator.free(fmt); try std.testing.expectEqualStrings("did:web:rockorager.dev%3A80", fmt); } test "did:web: host + path" { const did = "did:web:rockorager.dev:user:alice"; const actual = try Did.init(did); try std.testing.expectEqualStrings("rockorager.dev", actual.web.host); try std.testing.expectEqual(443, actual.web.port); try std.testing.expectEqualStrings("user:alice", actual.web.path); const fmt = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{actual}); defer std.testing.allocator.free(fmt); try std.testing.expectEqualStrings("did:web:rockorager.dev:user:alice", fmt); } test "did:web: host + port + path" { const did = "did:web:rockorager.dev%3A80:user:alice"; const actual = try Did.init(did); try std.testing.expectEqualStrings("rockorager.dev", actual.web.host); try std.testing.expectEqual(80, actual.web.port); try std.testing.expectEqualStrings("user:alice", actual.web.path); const fmt = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{actual}); defer std.testing.allocator.free(fmt); try std.testing.expectEqualStrings("did:web:rockorager.dev%3A80:user:alice", fmt); }