|
|
|
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 EntityPart,
|
|
|
|
/// 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,
|
|
|
|
|
|
|
|
/// Represents an `Entity` in a `Path`, either by name or its numeric id.
|
|
|
|
pub const EntityPart = union(enum) {
|
|
|
|
name: []const u8,
|
|
|
|
id: u32,
|
|
|
|
};
|
|
|
|
|
|
|
|
/// 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 EntityPart) !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(EntityPart, parts_len);
|
|
|
|
|
|
|
|
var i: usize = 0;
|
|
|
|
var it = std.mem.splitSequence(u8, remaining, opt.sep);
|
|
|
|
while (it.next()) |str| : (i += 1)
|
|
|
|
parts[i] = if (parseNumericId(str)) |id|
|
|
|
|
.{ .id = id }
|
|
|
|
else
|
|
|
|
.{ .name = str };
|
|
|
|
|
|
|
|
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(EntityPart, orig.parts);
|
|
|
|
errdefer alloc.free(parts);
|
|
|
|
|
|
|
|
var num_allocated: usize = 0;
|
|
|
|
errdefer for (parts[0..num_allocated]) |part|
|
|
|
|
if (part == .name) alloc.free(part.name);
|
|
|
|
|
|
|
|
for (parts) |*part| {
|
|
|
|
if (part.* == .name)
|
|
|
|
part.* = .{ .name = try alloc.dupe(u8, part.name) };
|
|
|
|
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|
|
|
|
|
if (part == .name)
|
|
|
|
self.alloc.?.free(part.name);
|
|
|
|
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 += switch (part) {
|
|
|
|
.name => |name| name.len,
|
|
|
|
.id => |id| numDigits(id),
|
|
|
|
};
|
|
|
|
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 the first part.
|
|
|
|
switch (self.parts[0]) {
|
|
|
|
.name => |name| try writer.writeAll(name),
|
|
|
|
.id => |id| try writer.writeIntNative(u32, id),
|
|
|
|
}
|
|
|
|
// Write the remaining parts, each preceeded bu separator.
|
|
|
|
for (self.parts[1..]) |part| {
|
|
|
|
try writer.writeAll(opt.sep);
|
|
|
|
switch (part) {
|
|
|
|
.name => |name| try writer.writeAll(name),
|
|
|
|
.id => |id| try writer.writeIntNative(u32, id),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempts to parse the specified string as a numeric entity id.
|
|
|
|
fn parseNumericId(str: []const u8) ?u32 {
|
|
|
|
if (str.len == 0 or str.len > 10) return null;
|
|
|
|
var result: u32 = 0;
|
|
|
|
var place: u32 = 1;
|
|
|
|
var it = std.mem.reverseIterator(str);
|
|
|
|
while (it.next()) |chr| {
|
|
|
|
const d = std.fmt.charToDigit(chr, 10) catch return null;
|
|
|
|
result = std.math.add(u32, result, d * place) catch return null;
|
|
|
|
place *%= 10; // Wrapping to avoid overflow error.
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculates the numbers of digits of an entity id when formatted as a
|
|
|
|
/// string. Since entity ids tend to be small this is technically optimized
|
|
|
|
/// for smaller numbers but performance likely doesn't matter much.
|
|
|
|
fn numDigits(n: u32) usize {
|
|
|
|
// zig fmt: off
|
|
|
|
return if (n < 10) 1
|
|
|
|
else if (n < 100) 2
|
|
|
|
else if (n < 1000) 3
|
|
|
|
else if (n < 10000) 4
|
|
|
|
else if (n < 100000) 5
|
|
|
|
else if (n < 1000000) 6
|
|
|
|
else if (n < 10000000) 7
|
|
|
|
else if (n < 100000000) 8
|
|
|
|
else if (n < 1000000000) 9
|
|
|
|
else 10;
|
|
|
|
// zig fmt: on
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
test Path {
|
|
|
|
const alloc = std.testing.allocator;
|
|
|
|
const expectFmt = std.testing.expectFmt;
|
|
|
|
const expectEqual = std.testing.expectEqual;
|
|
|
|
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, &.{ .{ .name = "I'm" }, .{ .name = "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].name);
|
|
|
|
try expectEqualStrings("relative", relative.parts[1].name);
|
|
|
|
try expectEqualStrings("path", relative.parts[2].name);
|
|
|
|
|
|
|
|
// Parts can also be numeric ids, used for entities that don't have a name.
|
|
|
|
const numeric = try Path.fromString("100.101.bar", null, alloc);
|
|
|
|
defer numeric.deinit();
|
|
|
|
try expectEqual(@as(usize, 3), numeric.parts.len);
|
|
|
|
try expectEqual(@as(u32, 100), numeric.parts[0].id);
|
|
|
|
try expectEqual(@as(u32, 101), numeric.parts[1].id);
|
|
|
|
try expectEqualStrings("bar", numeric.parts[2].name);
|
|
|
|
|
|
|
|
// Paths are formattable. As format specifier you can use options defined
|
|
|
|
// on the `FormatOptions` type, or 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();
|
|
|
|
}
|