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;
11
12export const lastSegment = (value: string) => {
13 const trimmed = value.replace(/\/+$/, "");
14 const parts = trimmed.split("/");
15 return parts[parts.length - 1] || trimmed;
16};
17
18export const locationOf = (name: string, fallback = "") => {
19 const parts = name.split("/");
20 const index = parts.indexOf("locations");
21 return index >= 0 ? (parts[index + 1] ?? fallback) : fallback;
22};
23
24export const parentOf = (name: string) => {
25 const parts = name.split("/").filter((part) => part.length > 0);
26 return parts.slice(0, -2).join("/");
27};
28
29export const locationParent = (project: string, location: string) =>
30 `projects/${project}/locations/${location}`;
31
32export const toResourceId = (
33 id: string,
34 requested: string | undefined,
35 existing: string | undefined,
36 maxLength = MAX_ID_LENGTH,
37) =>
38 Effect.gen(function* () {
39 if (requested !== undefined) return requested;
40 if (existing !== undefined) return existing;
41 const generated = yield* createPhysicalName({
42 id,
43 maxLength,
44 lowercase: true,
45 });
46 return /^[a-z]/.test(generated)
47 ? generated
48 : `a${generated}`.slice(0, maxLength);
49 });
50
51const markerOf = (labels: Record<string, string>) =>
52 `[alchemy ${alchemyLabelKeys.stack}=${labels[alchemyLabelKeys.stack]} ${alchemyLabelKeys.stage}=${labels[alchemyLabelKeys.stage]} ${alchemyLabelKeys.id}=${labels[alchemyLabelKeys.id]}]`;
53
54export const encodeOwnership = (
55 labels: Record<string, string>,
56 text: string | undefined,
57): string => {
58 const marker = markerOf(labels);
59 return text ? `${marker}\n${text}` : marker;
60};
61
62export const encodeOwnershipLine = (
63 labels: Record<string, string>,
64 text: string | undefined,
65 maxLength = MAX_DISPLAY_NAME_LENGTH,
66): string => {
67 const marker = markerOf(labels);
68 if (!text) return marker.slice(0, maxLength);
69 return `${marker} ${text}`.slice(0, maxLength);
70};
71
72export const parseOwnership = (
73 text: string | undefined,
74): {
75 labels: Record<string, string>;
76 text: string | undefined;
77} => {
78 if (!text?.startsWith("[alchemy ")) {
79 return { labels: {}, text };
80 }
81 const end = text.indexOf("]");
82 if (end < 0) return { labels: {}, text };
83 const labels: Record<string, string> = {};
84 for (const part of text.slice("[alchemy ".length, end).split(/\s+/)) {
85 const eq = part.indexOf("=");
86 if (eq > 0) {
87 labels[part.slice(0, eq)] = part.slice(eq + 1);
88 }
89 }
90 const rest = text.slice(end + 1).replace(/^[\s\n]+/, "");
91 return { labels, text: rest.length > 0 ? rest : undefined };
92};
93
94export const hasOwnershipMarker = (text: string | undefined) =>
95 Object.keys(parseOwnership(text).labels).some((key) =>
96 key.startsWith("alchemy-"),
97 );
98
99export const ownedByAlchemy = (id: string, text: string | undefined) =>
100 Effect.gen(function* () {
101 const { labels } = parseOwnership(text);
102 return yield* hasAlchemyLabels(id, labels);
103 });
104
105export const internalLabels = (id: string) => createInternalLabels(id);