This document describes how paths are parsed, resolved, and managed in Implexus.
The Path class represents a path to an entity in the database hierarchy.
namespace Implexus.Core {
public class Path : Object, Invercargill.Element, Invercargill.Hashable, Invercargill.Equatable {
private Invercargill.DataStructures.Vector<string> _segments;
// Constructors
public Path(string path_string) {
_segments = new Invercargill.DataStructures.Vector<string>();
parse(path_string);
}
public Path.root() {
_segments = new Invercargill.DataStructures.Vector<string>();
}
public Path.from_segments(Invercargill.ReadOnlyCollection<string> segments) {
_segments = new Invercargill.DataStructures.Vector<string>();
foreach (var seg in segments) {
_segments.add(seg);
}
}
public Path.child(Path parent, string name) {
_segments = new Invercargill.DataStructures.Vector<string>();
foreach (var seg in parent._segments) {
_segments.add(seg);
}
_segments.add(name);
}
// Properties
public bool is_root { get { return _segments.count == 0; } }
public string name {
owned get {
if (is_root) return "";
return _segments.last();
}
}
public Path parent {
owned get {
if (is_root) return this;
var parent_segments = _segments.take(_segments.count - 1);
return new Path.from_segments(parent_segments);
}
}
public int depth { get { return _segments.count; } }
public Invercargill.ReadOnlyCollection<string> segments {
owned get { return _segments.as_read_only(); }
}
// Path Operations
public Path child(string name) {
return new Path.child(this, validate_name(name));
}
public Path sibling(string name) {
if (is_root) {
throw new EngineError.INVALID_PATH("Root has no siblings");
}
return parent.child(name);
}
public Path ancestor(int levels) {
if (levels < 0 || levels > depth) {
throw new EngineError.INVALID_PATH("Invalid ancestor level: %d", levels);
}
var ancestor_segments = _segments.take(depth - levels);
return new Path.from_segments(ancestor_segments);
}
public bool is_ancestor_of(Path other) {
if (depth >= other.depth) return false;
for (int i = 0; i < depth; i++) {
if (_segments.get(i) != other._segments.get(i)) return false;
}
return true;
}
public bool is_descendant_of(Path other) {
return other.is_ancestor_of(this);
}
public Path relative_to(Path ancestor) {
if (!ancestor.is_ancestor_of(this)) {
throw new EngineError.INVALID_PATH("%s is not an ancestor of %s",
ancestor.to_string(), this.to_string());
}
var relative_segments = _segments.skip(ancestor.depth);
return new Path.from_segments(relative_segments);
}
public Path resolve(Path 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.count > 0) {
result_segments.remove_at(result_segments.count - 1);
}
} else if (seg != ".") {
result_segments.add(seg);
}
}
return new Path.from_segments(result_segments);
}
// String Conversion
public 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;
}
public string to_key() {
// Compact representation for storage keys
if (is_root) return "\0";
return string.joinv("\0", _segments.to_array());
}
public static Path from_key(string key) {
if (key == "\0") return new Path.root();
var segments = key.split("\0");
return new Path.from_segments(new Invercargill.DataStructures.Vector<string>.from_array(segments));
}
// Parsing
private void 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) {
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", name);
}
if (name == "." || name == "..") {
throw new EngineError.INVALID_PATH("Entity name cannot be . or ..");
}
return name;
}
// Escaping for special characters in segment names
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
public uint hash() {
uint h = 0;
foreach (var seg in _segments) {
h ^= str_hash(seg);
}
return h;
}
// Equatable
public bool equals(Path other) {
if (depth != other.depth) return false;
for (int i = 0; i < depth; i++) {
if (_segments.get(i) != other._segments.get(i)) return false;
}
return true;
}
// Element interface
public Type type() { return typeof(Path); }
public string type_name() { return "Path"; }
public bool is_null() { return false; }
public bool is_type(Type t) { return t.is_a(typeof(Path)); }
public bool assignable_to(Type t) { return t.is_a(typeof(Path)); }
public bool assignable_to_type(Type t) { return is_type(t); }
public T? as<T>() { return this; }
public T assert_as<T>() { return (T) this; }
public T? as_or_default<T>(T default_value) { return this; }
public bool try_get_as<T>(out T result) { result = this; return true; }
public T to_value<T>() { return (T) this; }
// Static factory methods
public static Path parse(string path_string) {
return new Path(path_string);
}
public static Path combine(Path base_path, string relative) {
return base_path.resolve(new Path(relative));
}
}
} // namespace Implexus.Core
sequenceDiagram
participant App as Application
participant Engine as Engine
participant Storage as Storage
participant Cache as Entity Cache
App->>Engine: get_entity with /users/john/profile
Engine->>Engine: Create Path object
Engine->>Storage: has_entity with path
Storage-->>Engine: true
Engine->>Cache: check cache for path
Cache-->>Engine: not found
Engine->>Storage: load_entity with path
Storage-->>Engine: binary data
Engine->>Engine: deserialize entity
Engine->>Cache: cache entity
Engine-->>App: Entity object
| Path String | Segments | is_root | depth |
|---|---|---|---|
/ |
[] |
true | 0 |
/users |
["users"] |
false | 1 |
/users/john |
["users", "john"] |
false | 2 |
/users/john/profile |
["users", "john", "profile"] |
false | 3 |
// Creation
var root = new Path.root();
var users = new Path("/users");
var john = users.child("john");
// Navigation
assert(john.parent.equals(users));
assert(john.name == "john");
assert(john.depth == 2);
// Relationships
assert(users.is_ancestor_of(john));
assert(john.is_descendant_of(users));
assert(!john.is_ancestor_of(users));
// Relative paths
var profile = john.child("profile");
var relative = profile.relative_to(users);
assert(relative.to_string() == "john/profile");
// Resolution
var resolved = users.resolve(new Path("john/profile"));
assert(resolved.equals(profile));
// With .. and .
var complex = john.resolve(new Path("../jane"));
assert(complex.to_string() == "/users/jane");
/. or .."""a/b""."".."Special characters in names are escaped using tilde encoding:
| Character | Escaped |
|---|---|
~ |
~7e |
/ |
~2f |
\ |
~5c |
\0 |
~00 |
This allows names like "a/b" (escaped as "a~2fb") to be stored safely.
The engine resolves paths to entities through the storage layer:
public Entity? resolve_path(Path path) {
// 1. Check cache
if (_cache.has(path)) {
return _cache.get(path);
}
// 2. Load from storage
if (!_storage.has_entity(path)) {
return null;
}
// 3. Deserialize
var data = _storage.load_entity(path);
var entity = _deserializer.deserialize(data, this, path);
// 4. Cache and return
_cache.set(path, entity);
return entity;
}
Storage tracks child names for each path to enable efficient enumeration:
Key: children:/users
Value: ["john", "jane", "admin"]
Key: children:/users/john
Value: ["profile", "settings"]
This allows get_child_names() to return quickly without scanning all entities.