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 locationParent,
13 normalizeLocation,
14 parseResourceName,
15 replaceOnIdentity,
16 resourceNameOf,
17 retryTransient,
18 toPhysicalId,
19 waitUntilGone,
20 toRestrictedDisplayName,
21} from "./internal.ts";
22
23export type AdaptiveMtDatasetProps = {
24 /**
25 * Dataset id (the `{dataset}` segment of
26 * `projects/{project}/locations/{location}/adaptiveMtDatasets/{dataset}`).
27 * If omitted, a unique name is generated from the stack, stage, and
28 * logical id. Immutable — changing it replaces the dataset.
29 */
30 datasetId?: string;
31 /**
32 * Location of the dataset (`us-central1`, `europe-west1`, …). Adaptive
33 * MT is regional. Immutable — changing it replaces the dataset.
34 * @default "us-central1"
35 */
36 location?: string;
37 /**
38 * User-facing display name. There is no update API — changing display name
39 * replaces the dataset.
40 */
41 displayName?: string;
42 /**
43 * BCP-47 source language code, for example `"en"`. Immutable —
44 * changing it replaces the dataset.
45 */
46 sourceLanguageCode: string;
47 /**
48 * BCP-47 target language code, for example `"es"`. Immutable —
49 * changing it replaces the dataset.
50 */
51 targetLanguageCode: string;
52};
53
54export type AdaptiveMtDataset = Resource<
55 "GCP.Translate.AdaptiveMtDataset",
56 AdaptiveMtDatasetProps,
57 {
58 /** Full resource name `projects/{project}/locations/{location}/adaptiveMtDatasets/{dataset}`. */
59 name: string;
60 /** Dataset id (last path segment). */
61 datasetId: string;
62 /** Project id. */
63 project: string;
64 /** Location id. */
65 location: string;
66 /** Display name. */
67 displayName: string | undefined;
68 /** BCP-47 source language code. */
69 sourceLanguageCode: string | undefined;
70 /** BCP-47 target language code. */
71 targetLanguageCode: string | undefined;
72 /** Number of imported sentence pairs. */
73 exampleCount: number | undefined;
74 /** RFC3339 creation timestamp. */
75 createTime: string | undefined;
76 /** RFC3339 last-update timestamp. */
77 updateTime: string | undefined;
78 },
79 never,
80 Providers
81>;
82
83/**
84 * An Adaptive MT dataset of source/target sentence pairs used to
85 * customize Cloud Translation.
86 *
87 * Without labels, ownership rests on the deterministic id: `read` reports a
88 * resource it finds without prior state as unowned (adopt it with `--adopt`).
89 * Adaptive MT datasets are location-scoped and have no labels field. Language
90 * pair, location, and display name are immutable — there is no patch RPC.
91 *
92 * ### Creating a Dataset
93 * **Example:** English to Spanish
94 * ```typescript
95 * const dataset = yield* GCP.Translate.AdaptiveMtDataset("EnEs", {
96 * sourceLanguageCode: "en",
97 * targetLanguageCode: "es",
98 * });
99 * ```
100 *
101 * **Example:** Explicit id and location
102 * ```typescript
103 * const dataset = yield* GCP.Translate.AdaptiveMtDataset("EnEs", {
104 * datasetId: "en-es-adaptive",
105 * location: "us-central1",
106 * sourceLanguageCode: "en",
107 * targetLanguageCode: "es",
108 * displayName: "enes",
109 * });
110 * ```
111 *
112 * @resource
113 * @category Translate
114 */
115export const AdaptiveMtDataset = Resource<AdaptiveMtDataset>(
116 "GCP.Translate.AdaptiveMtDataset",
117);
118
119const resourceName = (project: string, location: string, datasetId: string) =>
120 resourceNameOf(
121 locationParent(project, location),
122 "adaptiveMtDatasets",
123 datasetId,
124 );
125
126const toAttrs = (dataset: translate.AdaptiveMtDataset, project: string) => {
127 const name = dataset.name ?? "";
128 const parsed = parseResourceName(name, "adaptiveMtDatasets");
129 return {
130 name,
131 datasetId: parsed.id,
132 project: parsed.project || project,
133 location: parsed.location,
134 displayName: dataset.displayName,
135 sourceLanguageCode: dataset.sourceLanguageCode,
136 targetLanguageCode: dataset.targetLanguageCode,
137 exampleCount: dataset.exampleCount,
138 createTime: dataset.createTime,
139 updateTime: dataset.updateTime,
140 };
141};
142
143const getByName = (name: string) =>
144 name.length === 0
145 ? Effect.succeed(undefined)
146 : translate
147 .getProjectsLocationsAdaptiveMtDatasets({ name })
148 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
149
150export const AdaptiveMtDatasetProvider = () =>
151 Provider.succeed(AdaptiveMtDataset, {
152 stables: ["name", "datasetId", "project", "location", "createTime"],
153
154 diff: Effect.fn(function* ({ news, olds, output }) {
155 if (!isResolved(news)) return undefined;
156 const extra =
157 (olds?.sourceLanguageCode !== undefined &&
158 news.sourceLanguageCode !== olds.sourceLanguageCode) ||
159 (output?.sourceLanguageCode !== undefined &&
160 news.sourceLanguageCode !== output.sourceLanguageCode) ||
161 (olds?.targetLanguageCode !== undefined &&
162 news.targetLanguageCode !== olds.targetLanguageCode) ||
163 (output?.targetLanguageCode !== undefined &&
164 news.targetLanguageCode !== output.targetLanguageCode) ||
165 (news.displayName !== undefined &&
166 (olds?.displayName ?? output?.displayName) !== undefined &&
167 news.displayName !== (olds?.displayName ?? output?.displayName));
168 return replaceOnIdentity({
169 previousId: olds?.datasetId ?? output?.datasetId,
170 nextId: news.datasetId,
171 previousLocation: olds?.location ?? output?.location,
172 nextLocation: news.location ?? olds?.location ?? output?.location,
173 extra,
174 });
175 }),
176
177 read: Effect.fn(function* ({ olds, output }) {
178 const env = yield* GcpEnvironment.current;
179 const location = normalizeLocation(olds?.location ?? output?.location);
180 const datasetId = olds?.datasetId ?? output?.datasetId;
181 const name =
182 output?.name ??
183 (datasetId ? resourceName(env.project, location, datasetId) : "");
184 const existing = yield* getByName(name);
185 if (existing === undefined) return undefined;
186 const attrs = toAttrs(existing, env.project);
187 // No labels field: without prior state it may not be ours.
188 return output !== undefined ? attrs : Unowned(attrs);
189 }),
190
191 reconcile: Effect.fn(function* ({ id, news, output }) {
192 const env = yield* GcpEnvironment.current;
193 const location = normalizeLocation(
194 news.location ?? output?.location ?? DEFAULT_LOCATION,
195 );
196 const parent = locationParent(env.project, location);
197 const datasetId = yield* toPhysicalId(
198 id,
199 news.datasetId,
200 output?.datasetId,
201 );
202 const name = resourceName(env.project, location, datasetId);
203 const displayName = toRestrictedDisplayName(
204 news.displayName ?? datasetId,
205 );
206 const hinted = output?.name ?? name;
207
208 let current = yield* getByName(hinted);
209
210 if (current === undefined) {
211 const created = yield* retryTransient(
212 translate.createProjectsLocationsAdaptiveMtDatasets({
213 parent,
214 body: {
215 name,
216 displayName,
217 sourceLanguageCode: news.sourceLanguageCode,
218 targetLanguageCode: news.targetLanguageCode,
219 },
220 }),
221 ).pipe(Effect.catchTag("Conflict", () => getByName(name)));
222 current = created ?? undefined;
223 }
224
225 if (current === undefined) {
226 return yield* new ResourceNotResolved({ name: hinted || name });
227 }
228
229 return toAttrs(current, env.project);
230 }),
231
232 delete: Effect.fn(function* ({ output }) {
233 if (!output.name) return;
234 yield* retryTransient(
235 translate.deleteProjectsLocationsAdaptiveMtDatasets({
236 name: output.name,
237 }),
238 ).pipe(Effect.catchTag("NotFound", () => Effect.void));
239 yield* waitUntilGone(getByName(output.name), output.name);
240 }),
241 });