1import type { NamespaceNode } from "./Namespace.ts";
2
3/**
4 * Separator used in FQN strings.
5 */
6export const FQN_SEPARATOR = "/";
7
8/**
9 * Encode an FQN for safe filesystem storage.
10 * Replaces `/` (FQN separator) with `__` to avoid subdirectory creation.
11 */
12export const encodeFqn = (fqn: string): string =>
13 fqn.replaceAll(FQN_SEPARATOR, "__");
14
15/**
16 * Decode a filename back to FQN.
17 */
18export const decodeFqn = (filename: string): string =>
19 filename.replaceAll("__", FQN_SEPARATOR);
20
21/**
22 * Convert a NamespaceNode chain to an array of namespace IDs, from root to leaf.
23 *
24 * @example
25 * ```ts
26 * const ns = { Id: "Child", Parent: { Id: "Parent", Parent: { Id: "Root" } } };
27 * toPath(ns); // ["Root", "Parent", "Child"]
28 * ```
29 */
30export const toPath = (ns: NamespaceNode | undefined): string[] => {
31 if (!ns) return [];
32 const path: string[] = [];
33 let current: NamespaceNode | undefined = ns;
34 while (current) {
35 path.unshift(current.Id);
36 current = current.Parent;
37 }
38 return path;
39};
40
41/**
42 * Create a fully-qualified name (FQN) from a namespace and logical ID.
43 * The FQN is a flat key suitable for state storage.
44 *
45 * @example
46 * ```ts
47 * const ns = { Id: "Child", Parent: { Id: "Parent" } };
48 * toFqn(ns, "MyResource"); // "Parent/Child/MyResource"
49 * toFqn(undefined, "MyResource"); // "MyResource"
50 * ```
51 */
52export const toFqn = (
53 ns: NamespaceNode | undefined,
54 logicalId: string,
55): string => {
56 const path = toPath(ns);
57 path.push(logicalId);
58 return path.join(FQN_SEPARATOR);
59};
60
61/**
62 * Parse an FQN back into namespace path segments and the logical ID.
63 *
64 * @example
65 * ```ts
66 * parseFqn("Parent/Child/MyResource"); // { path: ["Parent", "Child"], logicalId: "MyResource" }
67 * parseFqn("MyResource"); // { path: [], logicalId: "MyResource" }
68 * ```
69 */
70export const parseFqn = (
71 fqn: string,
72): { path: string[]; logicalId: string } => {
73 const parts = fqn.split(FQN_SEPARATOR);
74 const logicalId = parts.pop()!;
75 return { path: parts, logicalId };
76};
77
78/**
79 * Reconstruct a NamespaceNode chain from a path array.
80 *
81 * @example
82 * ```ts
83 * fromPath(["Parent", "Child"]);
84 * // { Id: "Child", Parent: { Id: "Parent" } }
85 * ```
86 */
87export const fromPath = (path: string[]): NamespaceNode | undefined => {
88 if (path.length === 0) return undefined;
89 let node: NamespaceNode | undefined;
90 for (const id of path) {
91 node = { Id: id, Parent: node };
92 }
93 return node;
94};