const std = @import("std"); const Allocator = std.mem.Allocator; const Entity = @import("./entity.zig").Entity; // 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) { id: u32, name: []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) t: { if (!std.meta.trait.isTuple(@TypeOf(parts))) @compileError("Expected tuple, got '" ++ @typeName(@TypeOf(parts)) ++ "'"); const len = @typeInfo(@TypeOf(parts)).Struct.fields.len; break :t [len]EntityPart; } { const len = @typeInfo(@TypeOf(parts)).Struct.fields.len; var result: [len]EntityPart = undefined; inline for (&result, parts) |*res, part| { const msg = "Expected '[]const u8' or 'u32', got '" ++ @typeName(@TypeOf(part)) ++ "'"; res.* = switch (@typeInfo(@TypeOf(part))) { .Pointer => |p| switch (p.size) { .One => switch (@typeInfo(p.child)) { .Array => |a| if (a.child == u8) .{ .name = part } else @compileError(msg), else => @compileError(msg), }, .Slice => if (p.child == u8) .{ .name = part } else @compileError(msg), else => @compileError(msg), }, .Int, .ComptimeInt => .{ .id = part }, else => @compileError(msg), }; } return result; } /// 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. /// /// 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 `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 = str }; return .{ .absolute = absolute, .parts = parts, .alloc = alloc, .owns_array = 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.getName()) |name| .{ .name = name } else .{ .id = current.getEntityId() }; num_parts += 1; // Move to the parent entity, if any. current = current.getParent() 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.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); } /// 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) |p| try writer.writeAll(p); } // Write the first part. switch (self.parts[0]) { .id => |id| try writer.writeIntNative(u32, id), .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.writeIntNative(u32, id), .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("./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, `absolute_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. // 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; // 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); try expect.fmt("mom/sister/child", "{}", .{twin}); twin.deinit(); }