1import { alchemyLabelKeys } from "../Labels.ts";
2
3export const lastSegment = (value: string) => {
4 const trimmed = value.replace(/\/+$/, "");
5 const parts = trimmed.split("/");
6 return parts[parts.length - 1] || trimmed;
7};
8
9export const parentOf = (project: string) => `projects/${project}`;
10
11const markerOf = (labels: Record<string, string>) =>
12 `[alchemy ${alchemyLabelKeys.stack}=${labels[alchemyLabelKeys.stack]} ${alchemyLabelKeys.stage}=${labels[alchemyLabelKeys.stage]} ${alchemyLabelKeys.id}=${labels[alchemyLabelKeys.id]}]`;
13
14export const encodeMarker = (
15 labels: Record<string, string>,
16 rest: string | undefined,
17 separator: string,
18): string => {
19 const marker = markerOf(labels);
20 return rest ? `${marker}${separator}${rest}` : marker;
21};
22
23export const encodeDescription = (
24 labels: Record<string, string>,
25 description: string | undefined,
26): string => encodeMarker(labels, description, "\n");
27
28export const encodeDisplayName = (
29 labels: Record<string, string>,
30 displayName: string | undefined,
31): string => encodeMarker(labels, displayName, " ");
32
33export const parseMarker = (
34 value: string | undefined,
35): {
36 labels: Record<string, string>;
37 rest: string | undefined;
38} => {
39 if (!value?.startsWith("[alchemy ")) {
40 return { labels: {}, rest: value };
41 }
42 const end = value.indexOf("]");
43 if (end < 0) return { labels: {}, rest: value };
44 const labels: Record<string, string> = {};
45 for (const part of value.slice("[alchemy ".length, end).split(/\s+/)) {
46 const eq = part.indexOf("=");
47 if (eq > 0) {
48 labels[part.slice(0, eq)] = part.slice(eq + 1);
49 }
50 }
51 const rest = value.slice(end + 1).replace(/^[\n\s]+/, "");
52 return { labels, rest: rest.length > 0 ? rest : undefined };
53};
54
55export const hasOwnershipMarker = (value: string | undefined) =>
56 Object.keys(parseMarker(value).labels).some((key) =>
57 key.startsWith("alchemy-"),
58 );
59
60export const compactStringMap = (
61 value: Record<string, string | undefined> | null | undefined,
62): Record<string, string> | undefined => {
63 if (value == null) return undefined;
64 return Object.fromEntries(
65 Object.entries(value).filter(
66 (entry): entry is [string, string] => entry[1] !== undefined,
67 ),
68 );
69};
70
71export const toEmptyObject = (
72 value: unknown,
73): Record<string, never> | undefined => (value === undefined ? undefined : {});
74
75export const jsonEqual = (left: unknown, right: unknown) =>
76 JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
77
78const canonicalize = (value: unknown): unknown => {
79 if (Array.isArray(value)) return value.map(canonicalize);
80 if (value !== null && typeof value === "object") {
81 return Object.fromEntries(
82 Object.entries(value as Record<string, unknown>)
83 .filter(([, item]) => item !== undefined)
84 .sort(([left], [right]) => left.localeCompare(right))
85 .map(([key, item]) => [key, canonicalize(item)]),
86 );
87 }
88 return value;
89};