- 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 documentationmain
parent
9d0d30a608
commit
edea471785
1 changed files with 201 additions and 80 deletions
@ -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, |
||||||
|
}; |
||||||
} |
} |
||||||
|
|
||||||
pub fn clone(self: Path) !Path { |
/// Creates a deep clone of this `Path` using the specified `Allocator`. |
||||||
_ = self; |
pub fn clone(orig: Path, alloc: Allocator) !Path { |
||||||
return error.NotImplemented; |
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 { |
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…
Reference in new issue