1import * as translate from "@distilled.cloud/gcp/translate_v3";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import {
10 DEFAULT_LOCATION,
11 ResourceNotResolved,
12 expandParent,
13 normalizeLocation,
14 parseResourceName,
15 replaceOnIdentity,
16 resourceNameOf,
17 retryTransient,
18 sameJson,
19 sameText,
20 waitUntilGone,
21} from "./internal.ts";
22
23export type GlossaryTermProps = {
24 /**
25 * Glossary term text.
26 */
27 text?: string;
28 /**
29 * BCP-47 language code for this term, for example `"en"`.
30 */
31 languageCode?: string;
32};
33
34export type GlossaryTermsPairProps = {
35 /**
36 * Source term matched in input text (unidirectional glossaries).
37 */
38 sourceTerm?: GlossaryTermProps;
39 /**
40 * Replacement term written into the translation.
41 */
42 targetTerm?: GlossaryTermProps;
43};
44
45export type GlossaryTermsSetProps = {
46 /**
47 * Equivalent terms that may replace each other (equivalent-term
48 * glossaries).
49 */
50 terms?: GlossaryTermProps[];
51};
52
53export type GlossariesGlossaryEntryProps = {
54 /**
55 * Parent glossary resource name
56 * `projects/{project}/locations/{location}/glossaries/{glossary}`
57 * or the glossary id (combined with `location`). Immutable — changing
58 * it replaces the entry.
59 */
60 parent: string;
61 /**
62 * Location used when `parent` is a bare glossary id. Immutable —
63 * changing it replaces the entry.
64 * @default "us-central1"
65 */
66 location?: string;
67 /**
68 * Server-assigned integer entry id (the `{entry}` segment of
69 * `.../glossaries/{glossary}/glossaryEntries/{entry}`). Leave blank on
70 * create. Immutable — changing it replaces the entry.
71 */
72 glossaryEntryId?: string;
73 /**
74 * Human-readable description.
75 */
76 description?: string;
77 /**
78 * Unidirectional source/target term pair. Mutually exclusive with
79 * `termsSet`.
80 */
81 termsPair?: GlossaryTermsPairProps;
82 /**
83 * Equivalent term set. Mutually exclusive with `termsPair`.
84 */
85 termsSet?: GlossaryTermsSetProps;
86};
87
88export type GlossariesGlossaryEntry = Resource<
89 "GCP.Translate.GlossariesGlossaryEntry",
90 GlossariesGlossaryEntryProps,
91 {
92 /** Full resource name `projects/{project}/locations/{location}/glossaries/{glossary}/glossaryEntries/{entry}`. */
93 name: string;
94 /** Entry id (last path segment). */
95 glossaryEntryId: string;
96 /** Parent glossary resource name. */
97 parent: string;
98 /** Project id. */
99 project: string;
100 /** Location id. */
101 location: string;
102 /** Description. */
103 description: string | undefined;
104 /** Unidirectional term pair, if set. */
105 termsPair: GlossaryTermsPairProps | undefined;
106 /** Equivalent term set, if set. */
107 termsSet: GlossaryTermsSetProps | undefined;
108 },
109 never,
110 Providers
111>;
112
113/**
114 * A single Cloud Translation glossary entry under a parent glossary.
115 *
116 * Without labels, ownership rests on the deterministic id: `read` reports a
117 * resource it finds without prior state as unowned (adopt it with `--adopt`).
118 * Entries are nested under a glossary and have no labels field. Term pair /
119 * term set and description update in place via patch. Parent glossary and
120 * entry id are identity — changing them replaces the entry.
121 *
122 * ### Creating an Entry
123 * **Example:** Unidirectional term pair
124 * ```typescript
125 * const entry = yield* GCP.Translate.GlossariesGlossaryEntry("Hello", {
126 * parent: glossary.name,
127 * termsPair: {
128 * sourceTerm: { languageCode: "en", text: "hello" },
129 * targetTerm: { languageCode: "es", text: "hola" },
130 * },
131 * });
132 * ```
133 *
134 * **Example:** Description
135 * ```typescript
136 * const entry = yield* GCP.Translate.GlossariesGlossaryEntry("Hello", {
137 * parent: glossary.name,
138 * description: "greeting",
139 * termsPair: {
140 * sourceTerm: { languageCode: "en", text: "hello" },
141 * targetTerm: { languageCode: "es", text: "hola" },
142 * },
143 * });
144 * ```
145 *
146 * ### Updating an Entry
147 * **Example:** Change the target term
148 * ```typescript
149 * // Same logical id as before; only the changed props differ.
150 * const entry = yield* GCP.Translate.GlossariesGlossaryEntry("Hello", {
151 * parent: glossary.name,
152 * termsPair: {
153 * sourceTerm: { languageCode: "en", text: "hello" },
154 * targetTerm: { languageCode: "es", text: "buenas" },
155 * },
156 * });
157 * ```
158 *
159 * @resource
160 * @category Translate
161 */
162export const GlossariesGlossaryEntry = Resource<GlossariesGlossaryEntry>(
163 "GCP.Translate.GlossariesGlossaryEntry",
164);
165
166const toTerm = (
167 term: translate.GlossaryTerm | undefined,
168): GlossaryTermProps | undefined => {
169 if (term === undefined) return undefined;
170 return {
171 text: term.text,
172 languageCode: term.languageCode,
173 };
174};
175
176const toTermsPair = (
177 pair: translate.GlossaryTermsPair | undefined,
178): GlossaryTermsPairProps | undefined => {
179 if (pair === undefined) return undefined;
180 return {
181 sourceTerm: toTerm(pair.sourceTerm),
182 targetTerm: toTerm(pair.targetTerm),
183 };
184};
185
186const toTermsSet = (
187 set: translate.GlossaryTermsSet | undefined,
188): GlossaryTermsSetProps | undefined => {
189 if (set === undefined) return undefined;
190 return {
191 terms: (set.terms ?? []).map((term) => ({
192 text: term.text,
193 languageCode: term.languageCode,
194 })),
195 };
196};
197
198const toAttrs = (entry: translate.GlossaryEntry, project: string) => {
199 const name = entry.name ?? "";
200 const parsed = parseResourceName(name, "glossaryEntries");
201 return {
202 name,
203 glossaryEntryId: parsed.id,
204 parent: parsed.parent,
205 project: parsed.project || project,
206 location: parsed.location,
207 description: entry.description,
208 termsPair: toTermsPair(entry.termsPair),
209 termsSet: toTermsSet(entry.termsSet),
210 };
211};
212
213const getByName = (name: string) =>
214 name.length === 0
215 ? Effect.succeed(undefined)
216 : translate
217 .getProjectsLocationsGlossariesGlossaryEntries({ name })
218 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
219
220const glossaryParentOf = (project: string, location: string, parent: string) =>
221 expandParent(parent, project, location, "glossaries");
222
223export const GlossariesGlossaryEntryProvider = () =>
224 Provider.succeed(GlossariesGlossaryEntry, {
225 stables: ["name", "glossaryEntryId", "parent", "project", "location"],
226
227 diff: Effect.fn(function* ({ news, olds, output }) {
228 if (!isResolved(news)) return undefined;
229 const previousParent = olds?.parent ?? output?.parent;
230 const nextParent = news.parent;
231 return replaceOnIdentity({
232 previousId: olds?.glossaryEntryId ?? output?.glossaryEntryId,
233 nextId: news.glossaryEntryId,
234 previousLocation: olds?.location ?? output?.location,
235 nextLocation: news.location ?? olds?.location ?? output?.location,
236 previousParent,
237 nextParent,
238 });
239 }),
240
241 read: Effect.fn(function* ({ olds, output }) {
242 const env = yield* GcpEnvironment.current;
243 const location = normalizeLocation(olds?.location ?? output?.location);
244 const parent = glossaryParentOf(
245 env.project,
246 location,
247 olds?.parent ?? output?.parent ?? "",
248 );
249 const glossaryEntryId = olds?.glossaryEntryId ?? output?.glossaryEntryId;
250 const name =
251 output?.name ??
252 (glossaryEntryId
253 ? resourceNameOf(parent, "glossaryEntries", glossaryEntryId)
254 : "");
255 // Server-assigned id: only a recorded entry can be observed.
256 const existing = yield* getByName(name);
257 if (existing === undefined) return undefined;
258 const attrs = toAttrs(existing, env.project);
259 return output !== undefined ? attrs : Unowned(attrs);
260 }),
261
262 reconcile: Effect.fn(function* ({ news, output }) {
263 const env = yield* GcpEnvironment.current;
264 const location = normalizeLocation(
265 news.location ?? output?.location ?? DEFAULT_LOCATION,
266 );
267 const parent = glossaryParentOf(env.project, location, news.parent);
268 if (
269 parent.length === 0 ||
270 parent.endsWith("/glossaries") ||
271 parent.includes("/locations/global/")
272 ) {
273 return yield* new ResourceNotResolved({
274 name: parent || news.parent,
275 });
276 }
277 const glossaryEntryId = news.glossaryEntryId ?? output?.glossaryEntryId;
278 const name = glossaryEntryId
279 ? resourceNameOf(parent, "glossaryEntries", glossaryEntryId)
280 : (output?.name ?? "");
281 const description = news.description;
282 const hinted = output?.name ?? name;
283 const body: translate.GlossaryEntry = {
284 ...(name.length > 0 ? { name } : {}),
285 description,
286 termsPair: news.termsPair,
287 termsSet: news.termsSet,
288 };
289
290 let current = yield* getByName(hinted);
291
292 if (current === undefined) {
293 const created = yield* retryTransient(
294 translate.createProjectsLocationsGlossariesGlossaryEntries({
295 parent,
296 body,
297 }),
298 ).pipe(Effect.catchTag("Conflict", () => getByName(name)));
299 current = created ?? undefined;
300 }
301
302 if (current === undefined) {
303 return yield* new ResourceNotResolved({ name: hinted || name });
304 }
305
306 const currentName = current.name ?? hinted;
307 const descriptionChanged = !sameText(current.description, description);
308 const pairChanged = !sameJson(current.termsPair, news.termsPair);
309 const setChanged = !sameJson(current.termsSet, news.termsSet);
310 if (descriptionChanged || pairChanged || setChanged) {
311 current = yield* retryTransient(
312 translate.patchProjectsLocationsGlossariesGlossaryEntries({
313 name: currentName,
314 body: {
315 name: currentName,
316 description,
317 termsPair: news.termsPair,
318 termsSet: news.termsSet,
319 },
320 }),
321 );
322 }
323
324 return toAttrs(current, env.project);
325 }),
326
327 delete: Effect.fn(function* ({ output }) {
328 if (!output.name) return;
329 yield* retryTransient(
330 translate.deleteProjectsLocationsGlossariesGlossaryEntries({
331 name: output.name,
332 }),
333 ).pipe(Effect.catchTag("NotFound", () => Effect.void));
334 yield* waitUntilGone(getByName(output.name), output.name);
335 }),
336 });