1import * as Effect from "effect/Effect";
2import { createPhysicalName } from "../../PhysicalName.ts";
3import {
4 alchemyLabelKeys,
5 createInternalLabels,
6 hasAlchemyLabels,
7} from "../Labels.ts";
8
9export const MAX_ID_LENGTH = 63;
10export const MAX_DISPLAY_NAME_LENGTH = 128;
11export const MAX_VIEW_DISPLAY_NAME_LENGTH = 64;
12export const MAX_LABEL_LENGTH = 256;
13
14export const lastSegment = (value: string) => {
15 const trimmed = value.replace(/\/+$/, "");
16 const parts = trimmed.split("/");
17 return parts[parts.length - 1] || trimmed;
18};
19
20export const locationOf = (name: string, fallback = "") => {
21 const parts = name.split("/");
22 const index = parts.indexOf("locations");
23 return index >= 0 ? (parts[index + 1] ?? fallback) : fallback;
24};
25
26export const parentOf = (name: string) => {
27 const parts = name.split("/").filter((part) => part.length > 0);
28 return parts.slice(0, -2).join("/");
29};
30
31export const locationParent = (project: string, location: string) =>
32 `projects/${project}/locations/${location}`;
33
34export const normalizeLocation = (location: string) =>
35 lastSegment(location).toLowerCase();
36
37export const sameText = (left: string | undefined, right: string | undefined) =>
38 (left ?? "") === (right ?? "");
39
40export const jsonEqual = (left: unknown, right: unknown) =>
41 JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
42
43export const updateMaskOf = (...fields: Array<string | undefined>) =>
44 fields.filter((field): field is string => field !== undefined).join(",");
45
46export const toResourceId = (
47 id: string,
48 requested: string | undefined,
49 existing: string | undefined,
50 maxLength = MAX_ID_LENGTH,
51) =>
52 Effect.gen(function* () {
53 if (requested !== undefined) return requested;
54 if (existing !== undefined) return existing;
55 const generated = yield* createPhysicalName({
56 id,
57 maxLength,
58 lowercase: true,
59 });
60 const next = /^[a-z]/.test(generated)
61 ? generated
62 : `c${generated}`.slice(0, maxLength);
63 return next.length >= 4 ? next : `${next}xxxx`.slice(0, maxLength);
64 });
65
66const markerOf = (
67 labels: Record<string, string>,
68 stack: string,
69 stage: string,
70 id: string,
71) =>
72 `[alchemy ${alchemyLabelKeys.stack}=${stack} ${alchemyLabelKeys.stage}=${stage} ${alchemyLabelKeys.id}=${id}]`;
73
74const fitMarker = (labels: Record<string, string>, maxLength: number) => {
75 let stack = labels[alchemyLabelKeys.stack] ?? "x";
76 let stage = labels[alchemyLabelKeys.stage] ?? "x";
77 let id = labels[alchemyLabelKeys.id] ?? "x";
78 let marker = markerOf(labels, stack, stage, id);
79 while (
80 marker.length > maxLength &&
81 (stack.length > 1 || stage.length > 1 || id.length > 1)
82 ) {
83 if (stack.length >= stage.length && stack.length >= id.length) {
84 stack = stack.slice(0, -1);
85 } else if (stage.length >= id.length) {
86 stage = stage.slice(0, -1);
87 } else {
88 id = id.slice(0, -1);
89 }
90 marker = markerOf(labels, stack, stage, id);
91 }
92 return marker.slice(0, maxLength);
93};
94
95export const encodeOwnership = (
96 labels: Record<string, string>,
97 text: string | undefined,
98): string => {
99 const marker = fitMarker(labels, 8000);
100 const trimmed = text?.trim();
101 return trimmed && trimmed.length > 0 ? `${marker}\n${trimmed}` : marker;
102};
103
104export const encodeOwnershipLine = (
105 labels: Record<string, string>,
106 text: string | undefined,
107 maxLength = MAX_DISPLAY_NAME_LENGTH,
108): string => {
109 const trimmed = text?.replace(/[\r\n]+/g, " ").trim();
110 if (!trimmed) return fitMarker(labels, maxLength);
111 // The shortest parseable marker (one-character stack, stage, and id);
112 // anything shorter loses its closing bracket.
113 const minMarker = markerOf(labels, "x", "x", "x").length;
114 const reserved = Math.min(
115 trimmed.length + 1,
116 Math.max(0, maxLength - minMarker),
117 );
118 const marker = fitMarker(labels, maxLength - reserved);
119 return `${marker} ${trimmed}`.slice(0, maxLength);
120};
121
122export const parseOwnership = (
123 text: string | undefined,
124): {
125 labels: Record<string, string>;
126 text: string | undefined;
127} => {
128 if (!text?.startsWith("[alchemy ")) {
129 return { labels: {}, text };
130 }
131 const end = text.indexOf("]");
132 if (end < 0) return { labels: {}, text };
133 const labels: Record<string, string> = {};
134 for (const part of text.slice("[alchemy ".length, end).split(/\s+/)) {
135 const eq = part.indexOf("=");
136 if (eq > 0) {
137 labels[part.slice(0, eq)] = part.slice(eq + 1);
138 }
139 }
140 const rest = text.slice(end + 1).replace(/^[\s\n]+/, "");
141 return { labels, text: rest.length > 0 ? rest : undefined };
142};
143
144export const hasOwnershipMarker = (text: string | undefined) =>
145 Object.keys(parseOwnership(text).labels).some((key) =>
146 key.startsWith("alchemy-"),
147 );
148
149const prefixMatch = (expected: string, observed: string) =>
150 expected === observed ||
151 expected.startsWith(observed) ||
152 observed.startsWith(expected);
153
154export const ownedByAlchemy = (id: string, text: string | undefined) =>
155 Effect.gen(function* () {
156 const expected = yield* createInternalLabels(id);
157 const { labels } = parseOwnership(text);
158 if (!hasOwnershipMarker(text)) return false;
159 const exact = yield* hasAlchemyLabels(id, labels);
160 if (exact) return true;
161 return (
162 prefixMatch(
163 expected[alchemyLabelKeys.stack] ?? "",
164 labels[alchemyLabelKeys.stack] ?? "",
165 ) &&
166 prefixMatch(
167 expected[alchemyLabelKeys.stage] ?? "",
168 labels[alchemyLabelKeys.stage] ?? "",
169 ) &&
170 prefixMatch(
171 expected[alchemyLabelKeys.id] ?? "",
172 labels[alchemyLabelKeys.id] ?? "",
173 )
174 );
175 });
176
177export const internalLabels = (id: string) => createInternalLabels(id);
178
179export const sameJson = jsonEqual;