diff --git a/src/path.zig b/src/path.zig index 713379a..48f7047 100644 --- a/src/path.zig +++ b/src/path.zig @@ -1,132 +1,253 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const c = @import("./c.zig"); - -pub var default_seperator = "/"; -pub var default_prefix = "/"; - // TODO: Do something better than just `std.debug.assert`. +// TODO: Offer a way to validate paths, like checking for empty parts. +/// Represents the path of an `Entity`, describing its place in the world's +/// hierarchy which is constructed using `ChildOf` relationships. pub const Path = struct { /// Whether the path is specified to be absolute. /// Note that a relative path can still be interpreted as absolute. absolute: bool, /// The string parts that make up the path. parts: []const []const u8, - /// Specifies how the path has been allocated. - /// Defines what happens when `deinit()` is called. - alloc: union(enum) { - /// None of the data is owned by the path. - none, - /// The parts array itself is owned by the path. - array: Allocator, - /// The parts array and the contained parts are owned by the path. - parts: Allocator, - }, + /// The allocator that was used to allocate `parts`. + alloc: ?Allocator = null, + /// Whether the parts outer array itself is owned by this `Path`. + owns_array: bool = false, + /// Whether the parts inner strings are owned by this `Path`. + owns_parts: bool = false, + + /// Format used to parse and stringify `Path`s. + /// + /// When this type is formatted, you may use any of the `FormatOption` + /// constants in this type by name as `fmt` specifier, such as `"{unix}"`. + /// An empty `fmt` results in the `default` format being used, which can be + /// changed at runtime. + pub const FormatOptions = struct { + /// The prefix used for an absolute `Path`, if any. + /// + /// If set to `null`, absolute paths can't be represented using strings. + /// In this case, an absolue path and relative path with identical parts + /// will be indistinguishable. + prefix: ?[]const u8, + /// The separator used between parts that make up a `Path`. + sep: []const u8, + + /// The format used by Flecs' C API. For example `flecs.core`. + pub const flecs_c = FormatOptions{ .prefix = null, .sep = "." }; + /// The format used by Flecs' C++ API. For example `::flecs::core`. + pub const flecs_cpp = FormatOptions{ .prefix = "::", .sep = "::" }; + /// Unix-like format. For example `/flecs/core`. + pub const unix = FormatOptions{ .prefix = "/", .sep = "/" }; + + /// The default format used when none is specified. Can be changed at runtime. + pub var default = flecs_c; + }; /// Creates a new `Path` from the specified string parts. + /// /// The resulting path does not own any of the given slices. pub fn fromParts(absolute: bool, parts: []const []const u8) !Path { std.debug.assert(parts.len > 0); - return .{ .absolute = absolute, .parts = parts, .alloc = .none }; + return .{ .absolute = absolute, .parts = parts }; } - /// Creates a new `Path` from the specified string using the default - /// seperator and prefix strings. - /// - /// If the string starts with the default prefix (if any), the resulting - /// path will be absolute. The rest of the string will be split by the - /// default seperator, becoming its parts. - /// - /// The parts array will be allocated with the specified allocator and is - /// owned by the resulting path. `deinit()` must be called to free it. - pub fn fromString(path: []const u8, alloc: Allocator) !Path { - return fromStringCustom(path, default_seperator, default_prefix, alloc); - } - - /// Creates a new `Path` from the specified string using the specified - /// seperator and optionally a prefix strings. + /// Parses a string as a `Path` using the `FormatOptions` specified, + /// or `FormatOptions.default` if the argument is `null`. /// /// If the string starts with the specified prefix (if any), the resulting /// path will be absolute. The rest of the string will be split by the /// specified seperator, becoming its parts. /// - /// The parts array will be allocated with the specified allocator and is + /// The parts array will be allocated with the specified `Allocator` and is /// owned by the resulting path. `deinit()` must be called to free it. - pub fn fromStringCustom(path: []const u8, sep: []const u8, prefix: ?[]const u8, alloc: Allocator) !Path { - std.debug.assert(path.len > 0); + pub fn fromString(path: []const u8, options: ?FormatOptions, alloc: Allocator) !Path { + if (path.len == 0) return error.MustNotBeEmpty; + const opt = options orelse FormatOptions.default; var remaining = path; var absolute = false; // If prefix is defined and path starts with it, the path is absolute. - if (prefix) |p| { + if (opt.prefix) |p| { if (std.mem.startsWith(u8, remaining, p)) { remaining = remaining[p.len..]; absolute = true; } } - const parts_len = std.mem.count(u8, remaining, sep) + 1; + const parts_len = std.mem.count(u8, remaining, opt.sep) + 1; const parts = try alloc.alloc([]const u8, parts_len); var i: usize = 0; - var it = std.mem.splitSequence(u8, remaining, sep); + var it = std.mem.splitSequence(u8, remaining, opt.sep); while (it.next()) |part| : (i += 1) parts[i] = part; - return .{ .absolute = absolute, .parts = parts, .alloc = .{ .array = alloc } }; + return .{ + .absolute = absolute, + .parts = parts, + .alloc = alloc, + .owns_array = true, + }; } - pub fn clone(self: Path) !Path { - _ = self; - return error.NotImplemented; + /// Creates a deep clone of this `Path` using the specified `Allocator`. + pub fn clone(orig: Path, alloc: Allocator) !Path { + var parts = try alloc.dupe([]const u8, orig.parts); + errdefer alloc.free(parts); + + var num_allocated: usize = 0; + errdefer for (parts[0..num_allocated]) |part| + alloc.free(part); + + for (parts) |*part| { + part.* = try alloc.dupe(u8, part.*); + num_allocated += 1; + } + + return .{ + .absolute = orig.absolute, + .parts = parts, + .alloc = alloc, + .owns_array = true, + .owns_parts = true, + }; } + /// Destroys any memory owned by this `Path`, if any. pub fn deinit(self: Path) void { - switch (self.alloc) { - .none => {}, // Do nothing. - .array => |alloc| alloc.free(self.parts), - .parts => |alloc| { - for (self.parts) |p| alloc.free(p); - alloc.free(self.parts); - }, + if (self.owns_parts) + for (self.parts) |part| + self.alloc.?.free(part); + if (self.owns_array) + self.alloc.?.free(self.parts); + } + + pub fn toString( + self: Path, + options: ?FormatOptions, + alloc: Allocator, + ) ![:0]const u8 { + const opt = options orelse FormatOptions.default; + const length = self.calculateLength(opt); + var result = try alloc.allocSentinel(u8, length, 0); + var stream = std.io.fixedBufferStream(result); + try write(self, opt, stream.writer()); + return result; + } + + pub fn format( + self: Path, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = options; // TODO: Actually make use of this. + + const opt = if (fmt.len == 0) + FormatOptions.default + else if (@hasDecl(FormatOptions, fmt) and @TypeOf(@field(FormatOptions, fmt)) == FormatOptions) + @field(FormatOptions, fmt) + else + std.fmt.invalidFmtError(fmt, Path); + + try self.write(opt, writer); + } + + fn calculateLength(self: Path, opt: FormatOptions) usize { + // Separators. + var result = opt.sep.len * (self.parts.len - 1); + // Prefix. + if (self.absolute) { + if (opt.prefix) |p| + result += p.len; + } + // Parts. + for (self.parts) |part| + result += part.len; + return result; + } + + fn write(self: Path, opt: FormatOptions, writer: anytype) !void { + // Write prefix (if applicable), + if (self.absolute) { + if (opt.prefix) |p| + try writer.writeAll(p); + } + // Write first part. + try writer.writeAll(self.parts[0]); + // Write other parts, each preceeded by separator. + for (self.parts[1..]) |part| { + try writer.writeAll(opt.sep); + try writer.writeAll(part); } } }; test Path { const alloc = std.testing.allocator; - // const expect = std.testing.expect; - const expectEql = std.testing.expectEqual; - const expectStrEql = std.testing.expectEqualStrings; - - const path1 = try Path.fromStringCustom("some/relative/path", "/", "/", alloc); - defer path1.deinit(); - try expectEql(false, path1.absolute); - try expectEql(@as(usize, 3), path1.parts.len); - try expectStrEql("some", path1.parts[0]); - try expectStrEql("relative", path1.parts[1]); - try expectStrEql("path", path1.parts[2]); - - const path2 = try Path.fromStringCustom("/absolute", "/", "/", alloc); - defer path2.deinit(); - try expectEql(true, path2.absolute); - try expectEql(@as(usize, 1), path2.parts.len); - try expectStrEql("absolute", path2.parts[0]); - - const path3 = try Path.fromStringCustom("foo.bar.baz", ".", null, alloc); - defer path3.deinit(); - try expectEql(false, path3.absolute); - try expectEql(@as(usize, 3), path3.parts.len); - try expectPartsEql(&.{ "foo", "bar", "baz" }, path3.parts); - - const path4 = try Path.fromParts(true, &.{ "I", "am", "Groot!" }); - // defer path4.deinit(); -- Not necessary, cannot leak. - try expectEql(true, path4.absolute); - try expectPartsEql(&.{ "I", "am", "Groot!" }, path4.parts); -} - -fn expectPartsEql(expected: []const []const u8, actual: []const []const u8) !void { - try std.testing.expectEqualDeep(expected, actual); + const expectFmt = std.testing.expectFmt; + const expectEqual = std.testing.expectEqual; + const expectEqualDeep = std.testing.expectEqualDeep; + const expectEqualStrings = std.testing.expectEqualStrings; + + // Paths may be constructed by parsing strings. + const relative = try Path.fromString("some.relative.path", null, alloc); + defer relative.deinit(); + + // Alternatively they can be made by specifying the individual component + // parts they're made of, as well as an argument specifying whether it's + // an absolute path. + const absolute1 = try Path.fromParts(true, &.{ "I'm", "absolute!" }); + // No need for `deinit()`, does not own outer array nor inner string parts. + + // With `options` unspecified, it's not possible to represent an absolute + // path using a string. Pass your own `FormatOptions` to be able to. + const absolute2 = try Path.fromString("/home/copygirl", Path.FormatOptions.unix, alloc); + defer absolute2.deinit(); + + // Use `.absolute` to test if the path is absolute. + // Relative paths can be treated as absolute in the absence of a context. + // Using absolute paths where a relative one is expected may cause an error. + try expectEqual(false, relative.absolute); + try expectEqual(true, absolute1.absolute); + try expectEqual(true, absolute2.absolute); + + // The internal component parts of the path can be accessed with `.parts`. + try expectEqual(@as(usize, 3), relative.parts.len); + try expectEqualStrings("some", relative.parts[0]); + try expectEqualStrings("relative", relative.parts[1]); + try expectEqualStrings("path", relative.parts[2]); + try expectEqualDeep(@as([]const []const u8, &.{ "I'm", "absolute!" }), absolute1.parts); + try expectEqualDeep(@as([]const []const u8, &.{ "home", "copygirl" }), absolute2.parts); + + // Paths are formattable, and as the `fmt` specifier you can either use one + // of the pre-defined ones defined in the `FormatOptions` type, or just an + // empty string to use the default. + try expectFmt("some.relative.path", "{}", .{relative}); + try expectFmt("::I'm::absolute!", "{flecs_cpp}", .{absolute1}); + try expectFmt("/home/copygirl", "{unix}", .{absolute2}); + + // They can also be turned directly into strings, which + // allows you to use entirely custom `FormatOptions`s: + const absolute1_str = try absolute1.toString(.{ .prefix = "= ", .sep = " + " }, alloc); + defer alloc.free(absolute1_str); + try expectEqualStrings("= I'm + absolute!", absolute1_str); + + // The default `FormatOptions` may be changed. + Path.FormatOptions.default = Path.FormatOptions.unix; + + // This affects functions that use them to parse or format strings. + try expectFmt("some/relative/path", "{}", .{relative}); + const another_relative = try Path.fromString("mom/sister/child", null, alloc); + defer another_relative.deinit(); + try expectFmt("mom/sister/child", "{}", .{another_relative}); + + // A deep clone of a path can be allocated using `.clone()`. + // This clone owns the outer array and its inner strings. + const twin = try another_relative.clone(alloc); + try expectFmt("mom/sister/child", "{}", .{twin}); + twin.deinit(); }