1import * as Effect from "effect/Effect";
2import { createPhysicalName } from "../../PhysicalName.ts";
3import { tagRecord } from "../../Tags.ts";
4import { stripInternalLabels } from "../Labels.ts";
5
6export const lastSegment = (value: string) => {
7 const trimmed = value.replace(/\/+$/, "");
8 const parts = trimmed.split("/");
9 return parts[parts.length - 1] || trimmed;
10};
11
12export const normalizeLocation = (
13 location: string | undefined,
14 fallback: string,
15) => lastSegment(location ?? fallback).toLowerCase();
16
17export const rfc1035 = (name: string, maxLength: number): string => {
18 let next = name
19 .toLowerCase()
20 .replace(/[^a-z0-9-]/g, "-")
21 .replace(/-+/g, "-")
22 .replace(/^-+|-+$/g, "");
23 if (!/^[a-z]/.test(next)) next = `a${next}`;
24 next = next.slice(0, maxLength).replace(/-+$/g, "");
25 if (next.length === 0) return "aiplatform";
26 if (!/[a-z0-9]$/.test(next)) next = `${next.slice(0, maxLength - 1)}0`;
27 return next.slice(0, maxLength);
28};
29
30export const snakeId = (name: string, maxLength: number): string => {
31 let next = name
32 .toLowerCase()
33 .replace(/[^a-z0-9_]/g, "_")
34 .replace(/_+/g, "_")
35 .replace(/^_+|_+$/g, "");
36 if (!/^[a-z]/.test(next)) next = `f${next}`;
37 next = next.slice(0, maxLength).replace(/_+$/g, "");
38 if (next.length === 0) return "feature";
39 if (!/[a-z0-9]$/.test(next)) next = `${next.slice(0, maxLength - 1)}0`;
40 return next.slice(0, maxLength);
41};
42
43export const toPhysicalRfc1035 = (
44 id: string,
45 explicit: string | undefined,
46 existing: string | undefined,
47 maxLength: number,
48) =>
49 Effect.gen(function* () {
50 if (explicit !== undefined) return rfc1035(explicit, maxLength);
51 if (existing !== undefined) return existing;
52 return rfc1035(
53 yield* createPhysicalName({ id, maxLength, lowercase: true }),
54 maxLength,
55 );
56 });
57
58export const toPhysicalSnake = (
59 id: string,
60 explicit: string | undefined,
61 existing: string | undefined,
62 maxLength: number,
63) =>
64 Effect.gen(function* () {
65 if (explicit !== undefined) return snakeId(explicit, maxLength);
66 if (existing !== undefined) return existing;
67 return snakeId(
68 yield* createPhysicalName({
69 id,
70 maxLength,
71 lowercase: true,
72 delimiter: "_",
73 }),
74 maxLength,
75 );
76 });
77
78export const parseResourceName = (name: string, collection: string) => {
79 const parts = name.split("/").filter((part) => part.length > 0);
80 const collectionAt = parts.lastIndexOf(collection);
81 const locationsAt = parts.lastIndexOf("locations");
82 const projectsAt = parts.lastIndexOf("projects");
83 return {
84 project:
85 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : "",
86 location:
87 locationsAt >= 0 && parts[locationsAt + 1] ? parts[locationsAt + 1]! : "",
88 id:
89 collectionAt >= 0 && parts[collectionAt + 1]
90 ? parts[collectionAt + 1]!
91 : lastSegment(name),
92 parent:
93 collectionAt > 0
94 ? parts.slice(0, collectionAt).join("/")
95 : parts.slice(0, Math.max(0, parts.length - 1)).join("/"),
96 };
97};
98
99export const userLabels = (
100 labels: Record<string, string | undefined> | null | undefined,
101): Record<string, string> => stripInternalLabels(tagRecord(labels));
102
103export const hasAlchemyLabelMap = (
104 labels: Record<string, string | undefined> | null | undefined,
105) => Object.keys(labels ?? {}).some((key) => key.startsWith("alchemy-"));
106
107export const expandParent = (
108 value: string,
109 project: string,
110 location: string,
111 collection: string,
112) => {
113 if (value.includes("/")) return value;
114 return `projects/${project}/locations/${location}/${collection}/${value}`;
115};
116
117export const canonical = (value: unknown): unknown => {
118 if (value === undefined || value === null) return undefined;
119 if (typeof value === "boolean" || typeof value === "number") return value;
120 if (typeof value === "string") {
121 return value.length === 0 ? undefined : value;
122 }
123 if (Array.isArray(value)) {
124 const items = value.map(canonical).filter((item) => item !== undefined);
125 return items.length === 0 ? undefined : items;
126 }
127 if (typeof value === "object") {
128 const entries = Object.entries(value as Record<string, unknown>)
129 .map(([key, item]) => [key, canonical(item)] as const)
130 .filter(([, item]) => item !== undefined)
131 .sort(([left], [right]) => left.localeCompare(right));
132 if (entries.length === 0) return undefined;
133 return Object.fromEntries(entries);
134 }
135 return undefined;
136};
137
138export const fingerprint = (value: unknown): string =>
139 JSON.stringify(canonical(value) ?? null);
140
141export const specifiedEquals = (
142 desired: unknown,
143 observed: unknown,
144): boolean => {
145 if (desired === undefined) return true;
146 if (
147 typeof desired === "boolean" ||
148 typeof desired === "number" ||
149 typeof desired === "string" ||
150 Array.isArray(desired)
151 ) {
152 return fingerprint(desired) === fingerprint(observed);
153 }
154 if (desired !== null && typeof desired === "object") {
155 const obs =
156 observed !== null && typeof observed === "object"
157 ? (observed as Record<string, unknown>)
158 : {};
159 return Object.entries(desired as Record<string, unknown>).every(
160 ([key, value]) => specifiedEquals(value, obs[key]),
161 );
162 }
163 return fingerprint(desired) === fingerprint(observed);
164};