const std = @import("std"); const Allocator = std.mem.Allocator; // 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, /// 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 }; } /// 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 /// owned by the resulting path. `deinit()` must be called to free it. 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 (opt.prefix) |p| { if (std.mem.startsWith(u8, remaining, p)) { remaining = remaining[p.len..]; absolute = true; } } 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, opt.sep); while (it.next()) |part| : (i += 1) parts[i] = part; return .{ .absolute = absolute, .parts = parts, .alloc = alloc, .owns_array = true, }; } /// 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 { 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 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(); }