| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- /**
- * 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<EntityPath> {
-
- private Invercargill.DataStructures.Vector<string> _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<string>();
- do_parse(path_string);
- }
-
- /**
- * Creates the root path.
- */
- public EntityPath.root() {
- _segments = new Invercargill.DataStructures.Vector<string>();
- }
-
- /**
- * Creates an EntityPath from an enumerable of segments.
- *
- * @param segments The path segments
- */
- public EntityPath.from_segments(Invercargill.Enumerable<string> segments) {
- _segments = new Invercargill.DataStructures.Vector<string>();
- 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<string>();
- 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<string> 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<string>();
- 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<string>();
- 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<T>() throws Invercargill.ElementError { return this; }
-
- /**
- * Asserts and casts this path to type T.
- *
- * @return This path as type T
- */
- public T assert_as<T>() { 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<T>() { 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<T>(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
|