1import * as Effect from "effect/Effect";
2import { createPhysicalName } from "../../PhysicalName.ts";
3import { tagRecord } from "../../Tags.ts";
4import { stripInternalLabels } from "../Labels.ts";
5
6export const DEFAULT_LOCATION = "global";
7export const MAX_NAME_LENGTH = 63;
8
9export const lastSegment = (value: string) => {
10 const trimmed = value.replace(/\/+$/, "");
11 const parts = trimmed.split("/");
12 return parts[parts.length - 1] || trimmed;
13};
14
15export const rfc1035 = (name: string, fallback: string): string => {
16 let next = name
17 .toLowerCase()
18 .replace(/[^a-z0-9-]/g, "-")
19 .replace(/-+/g, "-")
20 .replace(/^-+/, "")
21 .replace(/-+$/, "");
22 if (!/^[a-z]/.test(next)) {
23 next = `${fallback[0] ?? "n"}${next}`;
24 }
25 next = next.slice(0, MAX_NAME_LENGTH).replace(/-+$/, "");
26 return next.length > 0 ? next : fallback;
27};
28
29export const normalizeLocation = (
30 location: string | undefined,
31 fallback = DEFAULT_LOCATION,
32) => lastSegment(location ?? fallback).toLowerCase();
33
34export const parseResourceName = (name: string, collection: string) => {
35 const parts = name.split("/").filter((part) => part.length > 0);
36 const collectionAt = parts.lastIndexOf(collection);
37 const locationsAt = parts.lastIndexOf("locations");
38 const projectsAt = parts.lastIndexOf("projects");
39 return {
40 project:
41 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : "",
42 location:
43 locationsAt >= 0 && parts[locationsAt + 1]
44 ? parts[locationsAt + 1]!
45 : DEFAULT_LOCATION,
46 id:
47 collectionAt >= 0 && parts[collectionAt + 1]
48 ? parts[collectionAt + 1]!
49 : lastSegment(name),
50 };
51};
52
53export const resourceName = (
54 project: string,
55 location: string,
56 collection: string,
57 id: string,
58) => `projects/${project}/locations/${location}/${collection}/${id}`;
59
60export const parentOf = (project: string, location: string) =>
61 `projects/${project}/locations/${location}`;
62
63export const toResourcePath = (value: string) => {
64 const trimmed = value.trim();
65 const match = trimmed.match(/projects\/[^/]+\/.+/);
66 return match ? match[0]!.replace(/\/+$/, "") : trimmed;
67};
68
69export const toNetworkResource = (project: string, network: string) => {
70 const trimmed = toResourcePath(network);
71 const match = trimmed.match(/projects\/[^/]+\/global\/networks\/[^/]+/);
72 if (match) return match[0]!;
73 if (trimmed.includes("/")) return trimmed;
74 return `projects/${project}/global/networks/${trimmed}`;
75};
76
77export const userLabels = (
78 labels: Record<string, string | undefined> | null | undefined,
79): Record<string, string> => stripInternalLabels(tagRecord(labels));
80
81export const toId = (
82 id: string,
83 explicit: string | undefined,
84 existing: string | undefined,
85 fallback: string,
86) =>
87 Effect.gen(function* () {
88 if (explicit !== undefined) return explicit;
89 if (existing !== undefined) return existing;
90 return rfc1035(
91 yield* createPhysicalName({
92 id,
93 maxLength: MAX_NAME_LENGTH,
94 lowercase: true,
95 }),
96 fallback,
97 );
98 });