/** * EntityPath - Represents a path to an entity in the database hierarchy * * The EntityPath class provides path parsing, manipulation, and comparison * for entity addressing in Implexus. * * @version 0.1 * @since 0.1 */ namespace Implexus.Core { /** * Represents a path to an entity in the database hierarchy. * * Paths are immutable and use forward-slash separators like Unix paths. * The root path is represented as "/". * * Example paths: * - "/" (root) * - "/users" (top-level category) * - "/users/john" (nested entity) * - "/users/john/profile" (deeply nested) * * EntityPath implements Invercargill.Element, Hashable, and Equatable for * compatibility with Invercargill collections. */ public class EntityPath : Object, Invercargill.Element, Invercargill.Hashable, Invercargill.Equatable { private Invercargill.DataStructures.Vector _segments; // === Constructors === /** * Creates an EntityPath from a string representation. * * @param path_string The path string (e.g., "/users/john") */ public EntityPath(string path_string) { _segments = new Invercargill.DataStructures.Vector(); do_parse(path_string); } /** * Creates the root path. */ public EntityPath.root() { _segments = new Invercargill.DataStructures.Vector(); } /** * Creates an EntityPath from an enumerable of segments. * * @param segments The path segments */ public EntityPath.from_segments(Invercargill.Enumerable segments) { _segments = new Invercargill.DataStructures.Vector(); foreach (var seg in segments) { _segments.add(seg); } } /** * Creates a child path from a parent and name. * * @param parent The parent path * @param name The child name */ public EntityPath.with_child(EntityPath parent, string name) { _segments = new Invercargill.DataStructures.Vector(); foreach (var seg in parent._segments) { _segments.add(seg); } _segments.add(name); } // === Properties === /** * Indicates whether this is the root path. * * @return true if this is the root path */ public bool is_root { get { return _segments.peek_count() == 0; } } /** * The name (last segment) of this path. * * Empty string for the root path. * * @return The name */ public string name { owned get { if (is_root) return ""; try { return _segments.last(); } catch (Invercargill.SequenceError e) { return ""; } } } /** * The parent path. * * For the root path, returns itself. * * @return The parent path */ public EntityPath parent { owned get { if (is_root) return this; var parent_segments = _segments.take(_segments.peek_count() - 1); return new EntityPath.from_segments(parent_segments); } } /** * The depth (number of segments) of this path. * * Root has depth 0. * * @return The depth */ public int depth { get { return (int) _segments.peek_count(); } } /** * The path segments as an enumerable. * * @return The segments */ public Invercargill.Enumerable segments { owned get { return _segments.as_enumerable(); } } // === Path Operations === /** * Creates a child path by appending a name. * * @param name The child name * @return A new EntityPath representing the child */ public EntityPath append_child(string name) { try { return new EntityPath.with_child(this, validate_name(name)); } catch (EngineError e) { return new EntityPath.with_child(this, name); } } /** * Creates a sibling path with a different name. * * @param name The sibling name * @return A new EntityPath representing the sibling * @throws EngineError.INVALID_PATH if this is the root */ public EntityPath sibling(string name) throws EngineError { if (is_root) { throw new EngineError.INVALID_PATH("Root has no siblings"); } return parent.append_child(name); } /** * Gets an ancestor path by going up the specified number of levels. * * @param levels The number of levels to go up * @return The ancestor path * @throws EngineError.INVALID_PATH if levels is invalid */ public EntityPath ancestor(int levels) throws EngineError { if (levels < 0 || levels > depth) { throw new EngineError.INVALID_PATH("Invalid ancestor level: %d".printf(levels)); } var ancestor_segments = _segments.take((uint)(depth - levels)); return new EntityPath.from_segments(ancestor_segments); } /** * Checks if this path is an ancestor of another path. * * @param other The potential descendant * @return true if this is an ancestor of other */ public bool is_ancestor_of(EntityPath other) { if (depth >= other.depth) return false; for (int i = 0; i < depth; i++) { try { if (_segments.get(i) != other._segments.get(i)) return false; } catch (Invercargill.IndexError e) { return false; } } return true; } /** * Checks if this path is a descendant of another path. * * @param other The potential ancestor * @return true if this is a descendant of other */ public bool is_descendant_of(EntityPath other) { return other.is_ancestor_of(this); } /** * Gets the relative path from an ancestor to this path. * * @param ancestor The ancestor path * @return The relative path * @throws EngineError.INVALID_PATH if ancestor is not actually an ancestor */ public EntityPath relative_to(EntityPath ancestor) throws EngineError { if (!ancestor.is_ancestor_of(this)) { throw new EngineError.INVALID_PATH( "%s is not an ancestor of %s".printf(ancestor.to_string(), this.to_string()) ); } var relative_segments = _segments.skip((uint)ancestor.depth); return new EntityPath.from_segments(relative_segments); } /** * Resolves a relative path against this path. * * Supports ".." (parent) and "." (current) segments. * * @param relative_path The relative path to resolve * @return The resolved absolute path */ public EntityPath resolve(EntityPath relative_path) { var result_segments = new Invercargill.DataStructures.Vector(); foreach (var seg in _segments) { result_segments.add(seg); } foreach (var seg in relative_path._segments) { if (seg == "..") { if (result_segments.peek_count() > 0) { try { result_segments.remove_at(result_segments.peek_count() - 1); } catch (Error e) {} } } else if (seg != ".") { result_segments.add(seg); } } return new EntityPath.from_segments(result_segments.as_enumerable()); } // === String Conversion === /** * Converts the path to a string representation. * * @return The path string (e.g., "/users/john") */ public new string to_string() { if (is_root) return "/"; var builder = new StringBuilder(); foreach (var seg in _segments) { builder.append("/"); builder.append(escape_segment(seg)); } return builder.str; } /** * Key separator for storage serialization. * Uses "/" (0x2F) for consistency with path string representation. * Note: This makes "/" an illegal character in entity names. */ public const string KEY_SEPARATOR = "/"; /** * Converts the path to a compact key for storage. * Uses "/" (0x2F) as separator for consistency with path representation. * * @return The storage key */ public string to_key() { if (is_root) return ""; // Build the key with "/" separators var builder = new StringBuilder(); bool first = true; foreach (var seg in _segments) { if (!first) { builder.append(KEY_SEPARATOR); } builder.append(seg); first = false; } return builder.str; } /** * Creates an EntityPath from a storage key. * * @param key The storage key * @return The EntityPath */ public static EntityPath from_key(string key) { // Check for root path (empty string) if (key == "" || key.length == 0) { return new EntityPath.root(); } // Parse "/"-separated segments var vec = new Invercargill.DataStructures.Vector(); int start = 0; for (int i = 0; i < key.length; i++) { if (key[i] == '/') { string segment = key.substring(start, i - start); vec.add(segment); start = i + 1; } } // Add the last segment if (start <= key.length) { vec.add(key.substring(start)); } return new EntityPath.from_segments(vec.as_enumerable()); } // === Parsing === private void do_parse(string path_string) { if (path_string == null || path_string == "") { return; // Root path } var normalized = path_string; if (normalized.has_prefix("/")) { normalized = normalized.substring(1); } if (normalized.has_suffix("/")) { normalized = normalized.substring(0, normalized.length - 1); } if (normalized == "") { return; // Root path } var parts = normalized.split("/"); foreach (var part in parts) { if (part == "") continue; _segments.add(unescape_segment(part)); } } // === Validation === private string validate_name(string name) throws EngineError { if (name == null || name == "") { throw new EngineError.INVALID_PATH("Entity name cannot be empty"); } if (name.contains("/")) { throw new EngineError.INVALID_PATH("Entity name cannot contain /: %s".printf(name)); } if (name == "." || name == "..") { throw new EngineError.INVALID_PATH("Entity name cannot be . or .."); } return name; } // === Escaping === private string escape_segment(string segment) { return segment.replace("~", "~7e") .replace("/", "~2f") .replace("\\", "~5c") .replace("\0", "~00"); } private string unescape_segment(string segment) { return segment.replace("~00", "\0") .replace("~5c", "\\") .replace("~2f", "/") .replace("~7e", "~"); } // === Hashable === /** * Computes a hash code for this path. * * @return The hash code */ public uint hash_code() { uint h = 0; foreach (var seg in _segments) { h ^= str_hash(seg); } return h; } // === Equatable === /** * Checks if this path equals another path. * * @param other The other path * @return true if the paths are equal */ public bool equals(EntityPath other) { if (depth != other.depth) return false; for (int i = 0; i < depth; i++) { try { if (_segments.get(i) != other._segments.get(i)) return false; } catch (Invercargill.IndexError e) { return false; } } return true; } // === Invercargill.Element === /** * Returns the GLib.Type for EntityPath. * * @return The EntityPath type */ public Type? type() { return typeof(EntityPath); } /** * Returns the type name string. * * @return "EntityPath" */ public string type_name() { return "EntityPath"; } /** * Paths are never null. * * @return false */ public bool is_null() { return false; } /** * Checks if this path is of the specified type. * * @param t The type to check * @return true if this path is of type t */ public bool is_type(Type t) { return t.is_a(typeof(EntityPath)); } /** * Checks if this path can be assigned to the specified type. * * @return true if assignable */ public bool assignable_to_type(Type t) { return is_type(t); } /** * Casts this path to type T. * * @return This path as type T, or null if not possible */ public T? @as() throws Invercargill.ElementError { return this; } /** * Asserts and casts this path to type T. * * @return This path as type T */ public T assert_as() { return (T) this; } /** * Casts this path to type T, returning a default if not possible. * * @return This path as type T, or the default */ public T? as_or_default() { return this; } /** * Attempts to get this path as type T. * * @param result Output parameter for the result * @return true if successful */ public bool try_get_as(out T result) { result = this; return true; } /** * Converts this path to a value of type T. * * @return The value */ public GLib.Value to_value(GLib.Type requested_type) throws GLib.Error { var v = Value(typeof(EntityPath)); v.set_object(this); return v; } // === Static Factory Methods === /** * Parses a path string. * * @param path_string The path string to parse * @return The parsed EntityPath */ public static EntityPath parse(string path_string) { return new EntityPath(path_string); } /** * Combines a base path with a relative path string. * * @param base_path The base path * @param relative The relative path string * @return The combined path */ public static EntityPath combine(EntityPath base_path, string relative) { return base_path.resolve(new EntityPath(relative)); } } } // namespace Implexus.Core