All-around improvements to Path

- Use `FormatOptions` struct to specify
  how paths get parsed and stringified
- Replace .alloc union with Allocator and
  booleans specifying what the path owns
- Implement clone function
- Add some helper functions
- More and improved documentation
main
copygirl 1 year ago
parent 9d0d30a608
commit edea471785
  1. 281
      src/path.zig

@ -1,132 +1,253 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; 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: 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 { pub const Path = struct {
/// Whether the path is specified to be absolute. /// Whether the path is specified to be absolute.
/// Note that a relative path can still be interpreted as absolute. /// Note that a relative path can still be interpreted as absolute.
absolute: bool, absolute: bool,
/// The string parts that make up the path. /// The string parts that make up the path.
parts: []const []const u8, parts: []const []const u8,
/// Specifies how the path has been allocated. /// The allocator that was used to allocate `parts`.
/// Defines what happens when `deinit()` is called. alloc: ?Allocator = null,
alloc: union(enum) { /// Whether the parts outer array itself is owned by this `Path`.
/// None of the data is owned by the path. owns_array: bool = false,
none, /// Whether the parts inner strings are owned by this `Path`.
/// The parts array itself is owned by the path. owns_parts: bool = false,
array: Allocator,
/// The parts array and the contained parts are owned by the path. /// Format used to parse and stringify `Path`s.
parts: Allocator, ///
}, /// 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. /// Creates a new `Path` from the specified string parts.
///
/// The resulting path does not own any of the given slices. /// The resulting path does not own any of the given slices.
pub fn fromParts(absolute: bool, parts: []const []const u8) !Path { pub fn fromParts(absolute: bool, parts: []const []const u8) !Path {
std.debug.assert(parts.len > 0); 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 /// Parses a string as a `Path` using the `FormatOptions` specified,
/// seperator and prefix strings. /// or `FormatOptions.default` if the argument is `null`.
///
/// 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.
/// ///
/// If the string starts with the specified prefix (if any), the resulting /// 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 /// path will be absolute. The rest of the string will be split by the
/// specified seperator, becoming its parts. /// 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. /// 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 { pub fn fromString(path: []const u8, options: ?FormatOptions, alloc: Allocator) !Path {
std.debug.assert(path.len > 0); if (path.len == 0) return error.MustNotBeEmpty;
const opt = options orelse FormatOptions.default;
var remaining = path; var remaining = path;
var absolute = false; var absolute = false;
// If prefix is defined and path starts with it, the path is absolute. // 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)) { if (std.mem.startsWith(u8, remaining, p)) {
remaining = remaining[p.len..]; remaining = remaining[p.len..];
absolute = true; 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); const parts = try alloc.alloc([]const u8, parts_len);
var i: usize = 0; 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; 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,
};
}
/// 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;
} }
pub fn clone(self: Path) !Path { return .{
_ = self; .absolute = orig.absolute,
return error.NotImplemented; .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 { pub fn deinit(self: Path) void {
switch (self.alloc) { if (self.owns_parts)
.none => {}, // Do nothing. for (self.parts) |part|
.array => |alloc| alloc.free(self.parts), self.alloc.?.free(part);
.parts => |alloc| { if (self.owns_array)
for (self.parts) |p| alloc.free(p); self.alloc.?.free(self.parts);
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 { test Path {
const alloc = std.testing.allocator; const alloc = std.testing.allocator;
// const expect = std.testing.expect; const expectFmt = std.testing.expectFmt;
const expectEql = std.testing.expectEqual; const expectEqual = std.testing.expectEqual;
const expectStrEql = std.testing.expectEqualStrings; const expectEqualDeep = std.testing.expectEqualDeep;
const expectEqualStrings = std.testing.expectEqualStrings;
const path1 = try Path.fromStringCustom("some/relative/path", "/", "/", alloc);
defer path1.deinit(); // Paths may be constructed by parsing strings.
try expectEql(false, path1.absolute); const relative = try Path.fromString("some.relative.path", null, alloc);
try expectEql(@as(usize, 3), path1.parts.len); defer relative.deinit();
try expectStrEql("some", path1.parts[0]);
try expectStrEql("relative", path1.parts[1]); // Alternatively they can be made by specifying the individual component
try expectStrEql("path", path1.parts[2]); // parts they're made of, as well as an argument specifying whether it's
// an absolute path.
const path2 = try Path.fromStringCustom("/absolute", "/", "/", alloc); const absolute1 = try Path.fromParts(true, &.{ "I'm", "absolute!" });
defer path2.deinit(); // No need for `deinit()`, does not own outer array nor inner string parts.
try expectEql(true, path2.absolute);
try expectEql(@as(usize, 1), path2.parts.len); // With `options` unspecified, it's not possible to represent an absolute
try expectStrEql("absolute", path2.parts[0]); // path using a string. Pass your own `FormatOptions` to be able to.
const absolute2 = try Path.fromString("/home/copygirl", Path.FormatOptions.unix, alloc);
const path3 = try Path.fromStringCustom("foo.bar.baz", ".", null, alloc); defer absolute2.deinit();
defer path3.deinit();
try expectEql(false, path3.absolute); // Use `.absolute` to test if the path is absolute.
try expectEql(@as(usize, 3), path3.parts.len); // Relative paths can be treated as absolute in the absence of a context.
try expectPartsEql(&.{ "foo", "bar", "baz" }, path3.parts); // Using absolute paths where a relative one is expected may cause an error.
try expectEqual(false, relative.absolute);
const path4 = try Path.fromParts(true, &.{ "I", "am", "Groot!" }); try expectEqual(true, absolute1.absolute);
// defer path4.deinit(); -- Not necessary, cannot leak. try expectEqual(true, absolute2.absolute);
try expectEql(true, path4.absolute);
try expectPartsEql(&.{ "I", "am", "Groot!" }, path4.parts); // 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]);
fn expectPartsEql(expected: []const []const u8, actual: []const []const u8) !void { try expectEqualStrings("relative", relative.parts[1]);
try std.testing.expectEqualDeep(expected, actual); 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();
} }

Loading…
Cancel
Save