EntityPath.vala 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. /**
  2. * EntityPath - Represents a path to an entity in the database hierarchy
  3. *
  4. * The EntityPath class provides path parsing, manipulation, and comparison
  5. * for entity addressing in Implexus.
  6. *
  7. * @version 0.1
  8. * @since 0.1
  9. */
  10. namespace Implexus.Core {
  11. /**
  12. * Represents a path to an entity in the database hierarchy.
  13. *
  14. * Paths are immutable and use forward-slash separators like Unix paths.
  15. * The root path is represented as "/".
  16. *
  17. * Example paths:
  18. * - "/" (root)
  19. * - "/users" (top-level category)
  20. * - "/users/john" (nested entity)
  21. * - "/users/john/profile" (deeply nested)
  22. *
  23. * EntityPath implements Invercargill.Element, Hashable, and Equatable for
  24. * compatibility with Invercargill collections.
  25. */
  26. public class EntityPath : Object, Invercargill.Element, Invercargill.Hashable, Invercargill.Equatable<EntityPath> {
  27. private Invercargill.DataStructures.Vector<string> _segments;
  28. // === Constructors ===
  29. /**
  30. * Creates an EntityPath from a string representation.
  31. *
  32. * @param path_string The path string (e.g., "/users/john")
  33. */
  34. public EntityPath(string path_string) {
  35. _segments = new Invercargill.DataStructures.Vector<string>();
  36. do_parse(path_string);
  37. }
  38. /**
  39. * Creates the root path.
  40. */
  41. public EntityPath.root() {
  42. _segments = new Invercargill.DataStructures.Vector<string>();
  43. }
  44. /**
  45. * Creates an EntityPath from an enumerable of segments.
  46. *
  47. * @param segments The path segments
  48. */
  49. public EntityPath.from_segments(Invercargill.Enumerable<string> segments) {
  50. _segments = new Invercargill.DataStructures.Vector<string>();
  51. foreach (var seg in segments) {
  52. _segments.add(seg);
  53. }
  54. }
  55. /**
  56. * Creates a child path from a parent and name.
  57. *
  58. * @param parent The parent path
  59. * @param name The child name
  60. */
  61. public EntityPath.with_child(EntityPath parent, string name) {
  62. _segments = new Invercargill.DataStructures.Vector<string>();
  63. foreach (var seg in parent._segments) {
  64. _segments.add(seg);
  65. }
  66. _segments.add(name);
  67. }
  68. // === Properties ===
  69. /**
  70. * Indicates whether this is the root path.
  71. *
  72. * @return true if this is the root path
  73. */
  74. public bool is_root { get { return _segments.peek_count() == 0; } }
  75. /**
  76. * The name (last segment) of this path.
  77. *
  78. * Empty string for the root path.
  79. *
  80. * @return The name
  81. */
  82. public string name {
  83. owned get {
  84. if (is_root) return "";
  85. try {
  86. return _segments.last();
  87. } catch (Invercargill.SequenceError e) {
  88. return "";
  89. }
  90. }
  91. }
  92. /**
  93. * The parent path.
  94. *
  95. * For the root path, returns itself.
  96. *
  97. * @return The parent path
  98. */
  99. public EntityPath parent {
  100. owned get {
  101. if (is_root) return this;
  102. var parent_segments = _segments.take(_segments.peek_count() - 1);
  103. return new EntityPath.from_segments(parent_segments);
  104. }
  105. }
  106. /**
  107. * The depth (number of segments) of this path.
  108. *
  109. * Root has depth 0.
  110. *
  111. * @return The depth
  112. */
  113. public int depth { get { return (int) _segments.peek_count(); } }
  114. /**
  115. * The path segments as an enumerable.
  116. *
  117. * @return The segments
  118. */
  119. public Invercargill.Enumerable<string> segments {
  120. owned get { return _segments.as_enumerable(); }
  121. }
  122. // === Path Operations ===
  123. /**
  124. * Creates a child path by appending a name.
  125. *
  126. * @param name The child name
  127. * @return A new EntityPath representing the child
  128. */
  129. public EntityPath append_child(string name) {
  130. try {
  131. return new EntityPath.with_child(this, validate_name(name));
  132. } catch (EngineError e) {
  133. return new EntityPath.with_child(this, name);
  134. }
  135. }
  136. /**
  137. * Creates a sibling path with a different name.
  138. *
  139. * @param name The sibling name
  140. * @return A new EntityPath representing the sibling
  141. * @throws EngineError.INVALID_PATH if this is the root
  142. */
  143. public EntityPath sibling(string name) throws EngineError {
  144. if (is_root) {
  145. throw new EngineError.INVALID_PATH("Root has no siblings");
  146. }
  147. return parent.append_child(name);
  148. }
  149. /**
  150. * Gets an ancestor path by going up the specified number of levels.
  151. *
  152. * @param levels The number of levels to go up
  153. * @return The ancestor path
  154. * @throws EngineError.INVALID_PATH if levels is invalid
  155. */
  156. public EntityPath ancestor(int levels) throws EngineError {
  157. if (levels < 0 || levels > depth) {
  158. throw new EngineError.INVALID_PATH("Invalid ancestor level: %d".printf(levels));
  159. }
  160. var ancestor_segments = _segments.take((uint)(depth - levels));
  161. return new EntityPath.from_segments(ancestor_segments);
  162. }
  163. /**
  164. * Checks if this path is an ancestor of another path.
  165. *
  166. * @param other The potential descendant
  167. * @return true if this is an ancestor of other
  168. */
  169. public bool is_ancestor_of(EntityPath other) {
  170. if (depth >= other.depth) return false;
  171. for (int i = 0; i < depth; i++) {
  172. try {
  173. if (_segments.get(i) != other._segments.get(i)) return false;
  174. } catch (Invercargill.IndexError e) {
  175. return false;
  176. }
  177. }
  178. return true;
  179. }
  180. /**
  181. * Checks if this path is a descendant of another path.
  182. *
  183. * @param other The potential ancestor
  184. * @return true if this is a descendant of other
  185. */
  186. public bool is_descendant_of(EntityPath other) {
  187. return other.is_ancestor_of(this);
  188. }
  189. /**
  190. * Gets the relative path from an ancestor to this path.
  191. *
  192. * @param ancestor The ancestor path
  193. * @return The relative path
  194. * @throws EngineError.INVALID_PATH if ancestor is not actually an ancestor
  195. */
  196. public EntityPath relative_to(EntityPath ancestor) throws EngineError {
  197. if (!ancestor.is_ancestor_of(this)) {
  198. throw new EngineError.INVALID_PATH(
  199. "%s is not an ancestor of %s".printf(ancestor.to_string(), this.to_string())
  200. );
  201. }
  202. var relative_segments = _segments.skip((uint)ancestor.depth);
  203. return new EntityPath.from_segments(relative_segments);
  204. }
  205. /**
  206. * Resolves a relative path against this path.
  207. *
  208. * Supports ".." (parent) and "." (current) segments.
  209. *
  210. * @param relative_path The relative path to resolve
  211. * @return The resolved absolute path
  212. */
  213. public EntityPath resolve(EntityPath relative_path) {
  214. var result_segments = new Invercargill.DataStructures.Vector<string>();
  215. foreach (var seg in _segments) {
  216. result_segments.add(seg);
  217. }
  218. foreach (var seg in relative_path._segments) {
  219. if (seg == "..") {
  220. if (result_segments.peek_count() > 0) {
  221. try {
  222. result_segments.remove_at(result_segments.peek_count() - 1);
  223. } catch (Error e) {}
  224. }
  225. } else if (seg != ".") {
  226. result_segments.add(seg);
  227. }
  228. }
  229. return new EntityPath.from_segments(result_segments.as_enumerable());
  230. }
  231. // === String Conversion ===
  232. /**
  233. * Converts the path to a string representation.
  234. *
  235. * @return The path string (e.g., "/users/john")
  236. */
  237. public new string to_string() {
  238. if (is_root) return "/";
  239. var builder = new StringBuilder();
  240. foreach (var seg in _segments) {
  241. builder.append("/");
  242. builder.append(escape_segment(seg));
  243. }
  244. return builder.str;
  245. }
  246. /**
  247. * Key separator for storage serialization.
  248. * Uses "/" (0x2F) for consistency with path string representation.
  249. * Note: This makes "/" an illegal character in entity names.
  250. */
  251. public const string KEY_SEPARATOR = "/";
  252. /**
  253. * Converts the path to a compact key for storage.
  254. * Uses "/" (0x2F) as separator for consistency with path representation.
  255. *
  256. * @return The storage key
  257. */
  258. public string to_key() {
  259. if (is_root) return "";
  260. // Build the key with "/" separators
  261. var builder = new StringBuilder();
  262. bool first = true;
  263. foreach (var seg in _segments) {
  264. if (!first) {
  265. builder.append(KEY_SEPARATOR);
  266. }
  267. builder.append(seg);
  268. first = false;
  269. }
  270. return builder.str;
  271. }
  272. /**
  273. * Creates an EntityPath from a storage key.
  274. *
  275. * @param key The storage key
  276. * @return The EntityPath
  277. */
  278. public static EntityPath from_key(string key) {
  279. // Check for root path (empty string)
  280. if (key == "" || key.length == 0) {
  281. return new EntityPath.root();
  282. }
  283. // Parse "/"-separated segments
  284. var vec = new Invercargill.DataStructures.Vector<string>();
  285. int start = 0;
  286. for (int i = 0; i < key.length; i++) {
  287. if (key[i] == '/') {
  288. string segment = key.substring(start, i - start);
  289. vec.add(segment);
  290. start = i + 1;
  291. }
  292. }
  293. // Add the last segment
  294. if (start <= key.length) {
  295. vec.add(key.substring(start));
  296. }
  297. return new EntityPath.from_segments(vec.as_enumerable());
  298. }
  299. // === Parsing ===
  300. private void do_parse(string path_string) {
  301. if (path_string == null || path_string == "") {
  302. return; // Root path
  303. }
  304. var normalized = path_string;
  305. if (normalized.has_prefix("/")) {
  306. normalized = normalized.substring(1);
  307. }
  308. if (normalized.has_suffix("/")) {
  309. normalized = normalized.substring(0, normalized.length - 1);
  310. }
  311. if (normalized == "") {
  312. return; // Root path
  313. }
  314. var parts = normalized.split("/");
  315. foreach (var part in parts) {
  316. if (part == "") continue;
  317. _segments.add(unescape_segment(part));
  318. }
  319. }
  320. // === Validation ===
  321. private string validate_name(string name) throws EngineError {
  322. if (name == null || name == "") {
  323. throw new EngineError.INVALID_PATH("Entity name cannot be empty");
  324. }
  325. if (name.contains("/")) {
  326. throw new EngineError.INVALID_PATH("Entity name cannot contain /: %s".printf(name));
  327. }
  328. if (name == "." || name == "..") {
  329. throw new EngineError.INVALID_PATH("Entity name cannot be . or ..");
  330. }
  331. return name;
  332. }
  333. // === Escaping ===
  334. private string escape_segment(string segment) {
  335. return segment.replace("~", "~7e")
  336. .replace("/", "~2f")
  337. .replace("\\", "~5c")
  338. .replace("\0", "~00");
  339. }
  340. private string unescape_segment(string segment) {
  341. return segment.replace("~00", "\0")
  342. .replace("~5c", "\\")
  343. .replace("~2f", "/")
  344. .replace("~7e", "~");
  345. }
  346. // === Hashable ===
  347. /**
  348. * Computes a hash code for this path.
  349. *
  350. * @return The hash code
  351. */
  352. public uint hash_code() {
  353. uint h = 0;
  354. foreach (var seg in _segments) {
  355. h ^= str_hash(seg);
  356. }
  357. return h;
  358. }
  359. // === Equatable ===
  360. /**
  361. * Checks if this path equals another path.
  362. *
  363. * @param other The other path
  364. * @return true if the paths are equal
  365. */
  366. public bool equals(EntityPath other) {
  367. if (depth != other.depth) return false;
  368. for (int i = 0; i < depth; i++) {
  369. try {
  370. if (_segments.get(i) != other._segments.get(i)) return false;
  371. } catch (Invercargill.IndexError e) {
  372. return false;
  373. }
  374. }
  375. return true;
  376. }
  377. // === Invercargill.Element ===
  378. /**
  379. * Returns the GLib.Type for EntityPath.
  380. *
  381. * @return The EntityPath type
  382. */
  383. public Type? type() { return typeof(EntityPath); }
  384. /**
  385. * Returns the type name string.
  386. *
  387. * @return "EntityPath"
  388. */
  389. public string type_name() { return "EntityPath"; }
  390. /**
  391. * Paths are never null.
  392. *
  393. * @return false
  394. */
  395. public bool is_null() { return false; }
  396. /**
  397. * Checks if this path is of the specified type.
  398. *
  399. * @param t The type to check
  400. * @return true if this path is of type t
  401. */
  402. public bool is_type(Type t) { return t.is_a(typeof(EntityPath)); }
  403. /**
  404. * Checks if this path can be assigned to the specified type.
  405. *
  406. * @return true if assignable
  407. */
  408. public bool assignable_to_type(Type t) { return is_type(t); }
  409. /**
  410. * Casts this path to type T.
  411. *
  412. * @return This path as type T, or null if not possible
  413. */
  414. public T? @as<T>() throws Invercargill.ElementError { return this; }
  415. /**
  416. * Asserts and casts this path to type T.
  417. *
  418. * @return This path as type T
  419. */
  420. public T assert_as<T>() { return (T) this; }
  421. /**
  422. * Casts this path to type T, returning a default if not possible.
  423. *
  424. * @return This path as type T, or the default
  425. */
  426. public T? as_or_default<T>() { return this; }
  427. /**
  428. * Attempts to get this path as type T.
  429. *
  430. * @param result Output parameter for the result
  431. * @return true if successful
  432. */
  433. public bool try_get_as<T>(out T result) { result = this; return true; }
  434. /**
  435. * Converts this path to a value of type T.
  436. *
  437. * @return The value
  438. */
  439. public GLib.Value to_value(GLib.Type requested_type) throws GLib.Error {
  440. var v = Value(typeof(EntityPath));
  441. v.set_object(this);
  442. return v;
  443. }
  444. // === Static Factory Methods ===
  445. /**
  446. * Parses a path string.
  447. *
  448. * @param path_string The path string to parse
  449. * @return The parsed EntityPath
  450. */
  451. public static EntityPath parse(string path_string) {
  452. return new EntityPath(path_string);
  453. }
  454. /**
  455. * Combines a base path with a relative path string.
  456. *
  457. * @param base_path The base path
  458. * @param relative The relative path string
  459. * @return The combined path
  460. */
  461. public static EntityPath combine(EntityPath base_path, string relative) {
  462. return base_path.resolve(new EntityPath(relative));
  463. }
  464. }
  465. } // namespace Implexus.Core