High-level wrapper around Flecs, a powerful ECS (Entity Component System) library, written in Zig language
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

439 lines
17 KiB

//! Represents the path of an `Entity`, describing its place in the world's
//! hierarchy which is constructed using `ChildOf` relationships.
const std = @import("std");
const Allocator = std.mem.Allocator;
const meta = @import("./meta.zig");
const Entity = @import("./entity.zig").Entity;
// TODO: See if we should even be using this?
const native_endian = @import("builtin").cpu.arch.endian();
// TODO: Do something better than just `std.debug.assert`.
// TODO: Offer a way to validate paths, like checking for empty parts.
const Path = @This();
/// 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) {
id: u32,
name: [:0]const u8,
};
/// 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 root separator 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.
root_sep: ?[]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{ .root_sep = null, .sep = "." };
/// The format used by Flecs' C++ API. For example `::flecs::core`.
pub const flecs_cpp = FormatOptions{ .root_sep = "::", .sep = "::" };
/// Unix-like format. For example `/flecs/core`.
pub const unix = FormatOptions{ .root_sep = "/", .sep = "/" };
/// The default format used when none is specified. Can be changed at runtime.
pub var default = flecs_c;
};
/// Creates an array of `EntityPath`s with known size equal to the number of
/// elements in the specified tuple argument. Each element of the tuple is
/// either converted to a `.name` part, or an `.id` part.
///
/// Keep in mind the mutability and lifetime of the string elements passed
/// to this function, as they aren't cloned and ownership stays the same.
/// In many cases, the lifetime of `Path`s is relatively short. When this
/// is not the case, it's recommended to `.clone()` the path after creation.
pub fn buildParts(parts: anytype) [numElements(parts)]EntityPart {
if (comptime !meta.isTuple(@TypeOf(parts)))
@compileError("Expected tuple, got '" ++ @typeName(@TypeOf(parts)) ++ "'");
var result: [numElements(parts)]EntityPart = undefined;
inline for (&result, parts) |*res, part| {
res.* = if (comptime meta.isZigString(@TypeOf(part)))
.{ .name = part }
else switch (@typeInfo(@TypeOf(part))) {
.Int, .ComptimeInt => .{ .id = part },
else => @compileError("Expected '[:0]const u8' or 'u32', got '" ++ @typeName(@TypeOf(part)) ++ "'"),
};
}
return result;
}
/// Returns the number of elements of the specified tuple type.
fn numElements(parts: anytype) usize {
return if (comptime meta.isTuple(@TypeOf(parts)))
@typeInfo(@TypeOf(parts)).Struct.fields.len
else
0;
}
/// Creates a new `Path` from the specified 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 root separator (if any), the
/// resulting path will be absolute. The rest of the string will be split
/// by the specified seperator, becoming its parts.
///
/// This function will allocate duplicate strings taken from the specified
/// source `path`, to ensure they are sentinel-terminated. The resulting
/// `Path` takes ownership of these.
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 `root_sep` is set and path starts with it, the path is absolute.
if (opt.root_sep) |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 = try alloc.dupeZ(u8, str) };
return .{
.absolute = absolute,
.parts = parts,
.alloc = alloc,
.owns_array = true,
.owns_parts = true,
};
}
/// Creates a `Path` for the specified `child` entity, optionally in
/// relation to the specified `parent` entity. If `parent` is not `null`,
/// the resulting path is relative. Otherwise it will be absolute.
///
/// This function allocates an array for the parts that make up the entity's
/// path, however each part itself is owned by Flecs and could change or be
/// invalidated ay any time, such as when an entity is renamed or removed.
pub fn fromEntity(comptime ctx: type, parent: ?Entity(ctx), child: Entity(ctx), alloc: Allocator) !Path {
std.debug.assert(child.raw != 0);
if (parent) |p| {
std.debug.assert(p.raw != 0);
std.debug.assert(p.raw != child.raw);
}
// TODO: Use a threadlocal field with reasonable size, then clone the result.
const starting_capacity: usize = 12;
var parts = try alloc.alloc(EntityPart, starting_capacity);
errdefer alloc.free(parts);
var num_parts: usize = 0;
// Traverse up the entity hierarchy starting from the specified child
// entity up until either the specified parent or root of the hierarchy.
var current = child;
while (true) {
std.debug.assert(num_parts == 0 or current.raw != child.raw); // Cycle detected.
parts[num_parts] = if (current.name()) |name|
.{ .name = name }
else
.{ .id = current.entityId() };
num_parts += 1;
// Move to the parent entity, if any.
current = current.parent() orelse
// If `parent` wasn't specified, we reached the root. Done.
// Otherwise, if the parent wasn't found, return an error.
if (parent == null) break else return error.ParentNotFound;
// If we reached the specified `parent`, we're done here!
if (parent != null and current.raw == parent.?.raw) break;
}
parts = try alloc.realloc(parts, num_parts);
std.mem.reverse(EntityPart, parts);
return .{
.absolute = parent != null,
.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.dupeZ(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);
const 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 = fmtToFormatOptions(fmt);
try self.write(opt, writer);
}
pub fn fmtToFormatOptions(comptime fmt: []const u8) FormatOptions {
return if (fmt.len == 0)
FormatOptions.default
else if (@hasDecl(FormatOptions, fmt) and @TypeOf(@field(FormatOptions, fmt)) == FormatOptions)
@field(FormatOptions, fmt)
else
@compileError("invalid format string '" ++ fmt ++ "'");
}
/// Returns whether the contents of the specified `Path`s are equivalent.
///
/// Path equivalency does not imply these paths are or are not referring to
/// the same `Entity`. For example, an entity that is referred to using its
/// entity id has a different path from the same entity referred to by name.
pub fn equals(first: Path, second: Path) bool {
if (first.absolute != second.absolute) return false;
if (first.parts.len != second.parts.len) return false;
for (first.parts, second.parts) |a, b| switch (a) {
.id => |a_id| if (b != .id or a_id != b.id) return false,
.name => |a_name| if (b != .name or !std.mem.eql(u8, a_name, b.name)) return false,
};
return true;
}
fn calculateLength(self: Path, opt: FormatOptions) usize {
// Separators.
var result = opt.sep.len * (self.parts.len - 1);
// Root separator.
if (self.absolute) {
if (opt.root_sep) |p|
result += p.len;
}
// Parts themselves.
for (self.parts) |part|
result += switch (part) {
.id => |id| numDigits(id),
.name => |name| name.len,
};
return result;
}
fn write(self: Path, opt: FormatOptions, writer: anytype) !void {
// Write root separator (if applicable).
if (self.absolute)
if (opt.root_sep) |sep|
try writer.writeAll(sep);
// Write the first part.
switch (self.parts[0]) {
.id => |id| try writer.writeInt(u32, id, native_endian),
.name => |name| try writer.writeAll(name),
}
// Write the remaining parts, each preceeded bu separator.
for (self.parts[1..]) |part| {
try writer.writeAll(opt.sep);
switch (part) {
.id => |id| try writer.writeInt(u32, id, native_endian),
.name => |name| try writer.writeAll(name),
}
}
}
/// 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 expect = @import("./test/expect.zig");
// 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.
// To do this you can use the `buildParts` helper function, which is less
// wordy than building `EntityPart` structs manually.
const absolute1_parts = Path.buildParts(.{ "I'm", "absolute!" });
const absolute1 = Path.fromParts(true, &absolute1_parts);
// No need to call `deinit()`. The path does not own the `absolute1_parts`
// array nor the string parts, which in this case are comptime constants.
// When handling paths, always be aware of the lifetime of its array and
// any string parts contained within it. This API allows you to completely
// avoid allocation if you know what you are doing.
// In the above example, `absolute1_parts` is an array allocated onto the
// stack, and as such will only be valid until the end of the scope. This
// means no allocation is necessary, but it also means `absolute1` is only
// valid for as long as its parts are.
// If a path instance is not immediately consumed and you're uncertain
// about the lifetime of its parts, consider using `.clone(alloc)`.
// 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 expect.false(relative.absolute);
try expect.true(absolute1.absolute);
try expect.true(absolute2.absolute);
// The internal component parts of the path can be accessed with `.parts`.
try expect.equal(3, relative.parts.len);
try expect.equal("some", relative.parts[0].name);
try expect.equal("relative", relative.parts[1].name);
try expect.equal("path", relative.parts[2].name);
// Parts can also be numeric ids, used for entities that don't have a name.
const numeric1 = try Path.fromString("100.101.bar", null, alloc);
defer numeric1.deinit();
try expect.equal(3, numeric1.parts.len);
try expect.equal(100, numeric1.parts[0].id);
try expect.equal(101, numeric1.parts[1].id);
try expect.equal("bar", numeric1.parts[2].name);
// Numeric ids can also be passed to `buildParts`.
const numeric2_parts = Path.buildParts(.{ 100, 101, "bar" });
const numeric2 = Path.fromParts(false, &numeric2_parts);
try expect.equalDeep(numeric1.parts, numeric2.parts);
// Paths are formattable. As format specifier you can use options defined
// on the `FormatOptions` type, or an empty string to use the default.
try expect.fmt("some.relative.path", "{}", .{relative});
try expect.fmt("::I'm::absolute!", "{flecs_cpp}", .{absolute1});
try expect.fmt("/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(.{ .root_sep = "= ", .sep = " + " }, alloc);
defer alloc.free(absolute1_str);
try expect.equal("= I'm + absolute!", absolute1_str);
// The default `FormatOptions` may be changed.
Path.FormatOptions.default = Path.FormatOptions.unix;
// But be sure to unset them after this test so other tests can succeed.
defer Path.FormatOptions.default = Path.FormatOptions.flecs_c;
// This affects functions that use them to parse or format strings.
try expect.fmt("some/relative/path", "{}", .{relative});
const another_relative = try Path.fromString("mom/sister/child", null, alloc);
defer another_relative.deinit();
try expect.fmt("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);
defer twin.deinit();
try expect.fmt("mom/sister/child", "{}", .{twin});
}