@abapify/openai-codegen 0.3.6 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2957 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import SwaggerParser from "@apidevtools/swagger-parser";
3
+ import YAML from "yaml";
4
+ import { createHash } from "node:crypto";
5
+ import { assign, attributeDef, builtinType, call, classDef, comment, enumType, identifierExpr, interfaceDef, literal, methodDef, methodImpl, methodParam, namedTypeRef, print, raw, section, structureType, tableType, typeDef } from "@abapify/abap-ast";
6
+ import { join } from "node:path";
7
+
8
+ //#region src/oas/types.ts
9
+ function isRef(x) {
10
+ return typeof x === "object" && x !== null && typeof x.$ref === "string";
11
+ }
12
+
13
+ //#endregion
14
+ //#region src/oas/load.ts
15
+ const HTTP_METHODS = [
16
+ "get",
17
+ "post",
18
+ "put",
19
+ "patch",
20
+ "delete",
21
+ "head",
22
+ "options",
23
+ "trace"
24
+ ];
25
+ function isRecord$4(x) {
26
+ return typeof x === "object" && x !== null && !Array.isArray(x);
27
+ }
28
+ function asString(x) {
29
+ return typeof x === "string" ? x : void 0;
30
+ }
31
+ function asBool(x, fallback) {
32
+ return typeof x === "boolean" ? x : fallback;
33
+ }
34
+ function asArray(x) {
35
+ return Array.isArray(x) ? x : [];
36
+ }
37
+ function asStringArray(x) {
38
+ return asArray(x).filter((v) => typeof v === "string");
39
+ }
40
+ /**
41
+ * Load and normalize an OpenAPI 3.x spec from a file path, URL, or in-memory
42
+ * object. YAML files (`.yaml` / `.yml`) are read manually with the `yaml`
43
+ * library first so we always hand SwaggerParser a parsed object — this keeps
44
+ * our behaviour identical for local files and in-memory inputs.
45
+ */
46
+ async function loadSpec(input) {
47
+ let raw;
48
+ if (typeof input === "string" && /\.ya?ml$/i.test(input)) {
49
+ const text = await readFile(input, "utf8");
50
+ raw = YAML.parse(text);
51
+ } else if (input instanceof URL) raw = input.toString();
52
+ else raw = input;
53
+ const namedSchemas = extractComponentSchemas(raw);
54
+ let validated;
55
+ try {
56
+ const parserInput = typeof raw === "string" ? raw : structuredClone(raw);
57
+ validated = await SwaggerParser.validate(parserInput);
58
+ } catch (err) {
59
+ throw formatValidationError(err);
60
+ }
61
+ return normalizeSpec(await SwaggerParser.dereference(validated), namedSchemas);
62
+ }
63
+ function formatValidationError(err) {
64
+ if (err instanceof Error) {
65
+ const details = err.details;
66
+ if (Array.isArray(details) && details.length > 0) {
67
+ const top = details.slice(0, 3).map((d, i) => {
68
+ const msg = isRecord$4(d) && typeof d.message === "string" ? d.message : String(d);
69
+ return ` ${i + 1}. ${msg}`;
70
+ }).join("\n");
71
+ return /* @__PURE__ */ new Error(`OpenAPI spec validation failed:\n${top}`);
72
+ }
73
+ return /* @__PURE__ */ new Error(`OpenAPI spec validation failed: ${err.message}`);
74
+ }
75
+ return /* @__PURE__ */ new Error(`OpenAPI spec validation failed: ${String(err)}`);
76
+ }
77
+ function extractComponentSchemas(raw) {
78
+ const out = {};
79
+ if (!isRecord$4(raw)) return out;
80
+ const components = raw.components;
81
+ if (!isRecord$4(components)) return out;
82
+ const schemas = components.schemas;
83
+ if (!isRecord$4(schemas)) return out;
84
+ for (const [name, schema] of Object.entries(schemas)) if (isRecord$4(schema)) out[name] = schema;
85
+ return out;
86
+ }
87
+ /**
88
+ * Normalize a (presumed-dereferenced) OpenAPI document into our internal
89
+ * shape. Callers who already have a dereferenced spec may invoke this
90
+ * directly.
91
+ */
92
+ function normalizeSpec(dereferenced, namedSchemas = {}) {
93
+ if (!isRecord$4(dereferenced)) throw new Error("normalizeSpec: input must be an object");
94
+ const openapiVersion = asString(dereferenced.openapi) ?? "";
95
+ const info = normalizeInfo(dereferenced.info);
96
+ const servers = normalizeServers(dereferenced.servers);
97
+ const securitySchemes = normalizeSecuritySchemes(isRecord$4(dereferenced.components) ? dereferenced.components.securitySchemes : void 0);
98
+ const topLevelSecurity = normalizeSecurityRequirements(dereferenced.security);
99
+ const operations = normalizeOperations(dereferenced.paths, topLevelSecurity);
100
+ const schemas = Object.keys(namedSchemas).length > 0 ? namedSchemas : extractComponentSchemas(dereferenced);
101
+ return Object.freeze({
102
+ openapiVersion,
103
+ info,
104
+ servers,
105
+ operations,
106
+ schemas,
107
+ securitySchemes
108
+ });
109
+ }
110
+ function normalizeInfo(raw) {
111
+ if (!isRecord$4(raw)) return {
112
+ title: "",
113
+ version: ""
114
+ };
115
+ const info = {
116
+ title: asString(raw.title) ?? "",
117
+ version: asString(raw.version) ?? ""
118
+ };
119
+ const description = asString(raw.description);
120
+ if (description !== void 0) info.description = description;
121
+ const tos = asString(raw.termsOfService);
122
+ if (tos !== void 0) info.termsOfService = tos;
123
+ if (isRecord$4(raw.contact)) {
124
+ const contact = {};
125
+ const name = asString(raw.contact.name);
126
+ if (name !== void 0) contact.name = name;
127
+ const url = asString(raw.contact.url);
128
+ if (url !== void 0) contact.url = url;
129
+ const email = asString(raw.contact.email);
130
+ if (email !== void 0) contact.email = email;
131
+ info.contact = contact;
132
+ }
133
+ if (isRecord$4(raw.license)) {
134
+ const license = { name: asString(raw.license.name) ?? "" };
135
+ const url = asString(raw.license.url);
136
+ if (url !== void 0) license.url = url;
137
+ const id = asString(raw.license.identifier);
138
+ if (id !== void 0) license.identifier = id;
139
+ info.license = license;
140
+ }
141
+ return info;
142
+ }
143
+ function normalizeServers(raw) {
144
+ return asArray(raw).filter(isRecord$4).map((s) => {
145
+ const url = asString(s.url) ?? "";
146
+ const variables = {};
147
+ if (isRecord$4(s.variables)) for (const [k, v] of Object.entries(s.variables)) {
148
+ if (!isRecord$4(v)) continue;
149
+ const variable = { default: asString(v.default) ?? "" };
150
+ const enums = asStringArray(v.enum);
151
+ if (enums.length > 0) variable.enum = enums;
152
+ const desc = asString(v.description);
153
+ if (desc !== void 0) variable.description = desc;
154
+ variables[k] = variable;
155
+ }
156
+ const server = {
157
+ url,
158
+ variables
159
+ };
160
+ const desc = asString(s.description);
161
+ if (desc !== void 0) server.description = desc;
162
+ return server;
163
+ });
164
+ }
165
+ function normalizeSecuritySchemes(raw) {
166
+ const out = {};
167
+ if (!isRecord$4(raw)) return out;
168
+ for (const [name, schemeRaw] of Object.entries(raw)) {
169
+ if (!isRecord$4(schemeRaw)) continue;
170
+ const type = asString(schemeRaw.type);
171
+ const description = asString(schemeRaw.description);
172
+ switch (type) {
173
+ case "apiKey": {
174
+ const inLoc = asString(schemeRaw.in);
175
+ if (inLoc !== "header" && inLoc !== "query" && inLoc !== "cookie") continue;
176
+ const scheme = {
177
+ type: "apiKey",
178
+ name: asString(schemeRaw.name) ?? name,
179
+ in: inLoc
180
+ };
181
+ if (description !== void 0) scheme.description = description;
182
+ out[name] = scheme;
183
+ break;
184
+ }
185
+ case "http": {
186
+ const scheme = {
187
+ type: "http",
188
+ scheme: asString(schemeRaw.scheme) ?? "bearer"
189
+ };
190
+ const bf = asString(schemeRaw.bearerFormat);
191
+ if (bf !== void 0) scheme.bearerFormat = bf;
192
+ if (description !== void 0) scheme.description = description;
193
+ out[name] = scheme;
194
+ break;
195
+ }
196
+ case "oauth2": {
197
+ const flowsRaw = isRecord$4(schemeRaw.flows) ? schemeRaw.flows : {};
198
+ const scheme = {
199
+ type: "oauth2",
200
+ flows: {}
201
+ };
202
+ for (const flowName of [
203
+ "implicit",
204
+ "password",
205
+ "clientCredentials",
206
+ "authorizationCode"
207
+ ]) {
208
+ const f = flowsRaw[flowName];
209
+ if (!isRecord$4(f)) continue;
210
+ const flow = { scopes: {} };
211
+ if (isRecord$4(f.scopes)) {
212
+ for (const [k, v] of Object.entries(f.scopes)) if (typeof v === "string") flow.scopes[k] = v;
213
+ }
214
+ const au = asString(f.authorizationUrl);
215
+ if (au !== void 0) flow.authorizationUrl = au;
216
+ const tu = asString(f.tokenUrl);
217
+ if (tu !== void 0) flow.tokenUrl = tu;
218
+ const ru = asString(f.refreshUrl);
219
+ if (ru !== void 0) flow.refreshUrl = ru;
220
+ scheme.flows[flowName] = flow;
221
+ }
222
+ if (description !== void 0) scheme.description = description;
223
+ out[name] = scheme;
224
+ break;
225
+ }
226
+ case "openIdConnect": {
227
+ const url = asString(schemeRaw.openIdConnectUrl);
228
+ if (url === void 0) continue;
229
+ const scheme = {
230
+ type: "openIdConnect",
231
+ openIdConnectUrl: url
232
+ };
233
+ if (description !== void 0) scheme.description = description;
234
+ out[name] = scheme;
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ return out;
240
+ }
241
+ function normalizeSecurityRequirements(raw) {
242
+ return asArray(raw).filter(isRecord$4).map((req) => {
243
+ const out = {};
244
+ for (const [k, v] of Object.entries(req)) out[k] = asStringArray(v);
245
+ return out;
246
+ });
247
+ }
248
+ function normalizeOperations(pathsRaw, topLevelSecurity) {
249
+ const ops = [];
250
+ if (!isRecord$4(pathsRaw)) return ops;
251
+ for (const [path, pathItemRaw] of Object.entries(pathsRaw)) {
252
+ if (!isRecord$4(pathItemRaw)) continue;
253
+ const pathLevelParams = normalizeParameters(pathItemRaw.parameters);
254
+ for (const method of HTTP_METHODS) {
255
+ const opRaw = pathItemRaw[method];
256
+ if (!isRecord$4(opRaw)) continue;
257
+ const parameters = mergeParameters(pathLevelParams, normalizeParameters(opRaw.parameters));
258
+ const op = {
259
+ operationId: asString(opRaw.operationId) ?? synthesizeOperationId(method, path),
260
+ method,
261
+ path,
262
+ tags: asStringArray(opRaw.tags),
263
+ deprecated: asBool(opRaw.deprecated, false),
264
+ parameters,
265
+ responses: normalizeResponses(opRaw.responses),
266
+ security: opRaw.security === void 0 ? topLevelSecurity : normalizeSecurityRequirements(opRaw.security)
267
+ };
268
+ const summary = asString(opRaw.summary);
269
+ if (summary !== void 0) op.summary = summary;
270
+ const description = asString(opRaw.description);
271
+ if (description !== void 0) op.description = description;
272
+ const requestBody = normalizeRequestBody(opRaw.requestBody);
273
+ if (requestBody !== void 0) op.requestBody = requestBody;
274
+ ops.push(op);
275
+ }
276
+ }
277
+ return ops;
278
+ }
279
+ function normalizeParameters(raw) {
280
+ return asArray(raw).filter(isRecord$4).map((p) => {
281
+ const inLoc = asString(p.in);
282
+ if (inLoc !== "path" && inLoc !== "query" && inLoc !== "header" && inLoc !== "cookie") return;
283
+ const name = asString(p.name);
284
+ if (name === void 0) return void 0;
285
+ const param = {
286
+ name,
287
+ in: inLoc,
288
+ required: inLoc === "path" ? true : asBool(p.required, false),
289
+ schema: isRecord$4(p.schema) ? p.schema : {},
290
+ deprecated: asBool(p.deprecated, false)
291
+ };
292
+ const style = asString(p.style);
293
+ if (style !== void 0) param.style = style;
294
+ if (typeof p.explode === "boolean") param.explode = p.explode;
295
+ const desc = asString(p.description);
296
+ if (desc !== void 0) param.description = desc;
297
+ return param;
298
+ }).filter((p) => p !== void 0);
299
+ }
300
+ function mergeParameters(pathLevel, opLevel) {
301
+ const key = (p) => `${p.in}:${p.name}`;
302
+ const merged = /* @__PURE__ */ new Map();
303
+ for (const p of pathLevel) merged.set(key(p), p);
304
+ for (const p of opLevel) merged.set(key(p), p);
305
+ return [...merged.values()];
306
+ }
307
+ function normalizeRequestBody(raw) {
308
+ if (!isRecord$4(raw)) return void 0;
309
+ const content = {};
310
+ if (isRecord$4(raw.content)) for (const [mt, mtRaw] of Object.entries(raw.content)) {
311
+ if (!isRecord$4(mtRaw)) continue;
312
+ content[mt] = { schema: isRecord$4(mtRaw.schema) ? mtRaw.schema : {} };
313
+ }
314
+ const body = {
315
+ required: asBool(raw.required, false),
316
+ content
317
+ };
318
+ const desc = asString(raw.description);
319
+ if (desc !== void 0) body.description = desc;
320
+ return body;
321
+ }
322
+ function normalizeResponses(raw) {
323
+ if (!isRecord$4(raw)) return [];
324
+ const entries = Object.entries(raw).filter((entry) => isRecord$4(entry[1]));
325
+ const has2xx = entries.some(([code]) => /^2\d\d$/.test(code));
326
+ return entries.map(([statusCode, respRaw]) => {
327
+ const isSuccess = /^2\d\d$/.test(statusCode) || statusCode === "default" && !has2xx;
328
+ const isError = /^(4|5)\d\d$/.test(statusCode) || statusCode === "default" && has2xx;
329
+ const content = {};
330
+ if (isRecord$4(respRaw.content)) for (const [mt, mtRaw] of Object.entries(respRaw.content)) {
331
+ if (!isRecord$4(mtRaw)) continue;
332
+ content[mt] = { schema: isRecord$4(mtRaw.schema) ? mtRaw.schema : {} };
333
+ }
334
+ const headers = {};
335
+ if (isRecord$4(respRaw.headers)) for (const [hn, hRaw] of Object.entries(respRaw.headers)) {
336
+ if (!isRecord$4(hRaw)) continue;
337
+ const header = { schema: isRecord$4(hRaw.schema) ? hRaw.schema : {} };
338
+ const desc = asString(hRaw.description);
339
+ if (desc !== void 0) header.description = desc;
340
+ if (typeof hRaw.required === "boolean") header.required = hRaw.required;
341
+ if (typeof hRaw.deprecated === "boolean") header.deprecated = hRaw.deprecated;
342
+ headers[hn] = header;
343
+ }
344
+ const resp = {
345
+ statusCode,
346
+ isSuccess,
347
+ isError,
348
+ content,
349
+ headers
350
+ };
351
+ const desc = asString(respRaw.description);
352
+ if (desc !== void 0) resp.description = desc;
353
+ return resp;
354
+ });
355
+ }
356
+ function synthesizeOperationId(method, path) {
357
+ const out = [];
358
+ let underscore = false;
359
+ let braceDepth = 0;
360
+ for (const ch of path) {
361
+ if (ch === "{") {
362
+ braceDepth += 1;
363
+ continue;
364
+ }
365
+ if (ch === "}") {
366
+ if (braceDepth > 0) braceDepth -= 1;
367
+ continue;
368
+ }
369
+ if (ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch >= "0" && ch <= "9") {
370
+ out.push(ch);
371
+ underscore = false;
372
+ } else if (!underscore) {
373
+ out.push("_");
374
+ underscore = true;
375
+ }
376
+ }
377
+ let start = 0;
378
+ let end = out.length;
379
+ while (start < end && out[start] === "_") start += 1;
380
+ while (end > start && out[end - 1] === "_") end -= 1;
381
+ return `${method}_${out.slice(start, end).join("")}`;
382
+ }
383
+
384
+ //#endregion
385
+ //#region src/types/naming.ts
386
+ const DEFAULT_MAX_LEN = 30;
387
+ /** Split an identifier on camelCase boundaries, non-alnum runs, and digits-prev-letter. */
388
+ function splitWords(raw) {
389
+ if (!raw) return [];
390
+ const cleaned = raw.replaceAll(/[^A-Za-z0-9]+/g, " ").trim();
391
+ if (!cleaned) return [];
392
+ const tokens = cleaned.split(/\s+/);
393
+ const out = [];
394
+ for (const tok of tokens) {
395
+ const spaced = tok.replaceAll(/([A-Z])(?=[A-Z][a-z])/g, "$1 ").replaceAll(/([a-z0-9])(?=[A-Z])/g, "$1 ");
396
+ for (const part of spaced.split(/\s+/)) if (part) out.push(part);
397
+ }
398
+ return out;
399
+ }
400
+ function shortHash(raw, len = 4) {
401
+ return createHash("sha256").update(raw).digest("hex").slice(0, len);
402
+ }
403
+ /** Sanitize a raw identifier into an ABAP-safe name. */
404
+ function sanitizeIdent(raw, kind, opts = {}) {
405
+ const maxLen = opts.maxLen ?? DEFAULT_MAX_LEN;
406
+ const prefix = opts.prefix ?? "";
407
+ const words = splitWords(raw);
408
+ const joiner = "_";
409
+ let base;
410
+ if (kind === "class") base = words.map((w) => w.toUpperCase()).join(joiner);
411
+ else base = words.map((w) => w.toLowerCase()).join(joiner);
412
+ if (!base) base = kind === "class" ? "X" : "x";
413
+ if (/^[0-9]/.test(base)) base = (kind === "class" ? "N_" : "n_") + base;
414
+ let full = prefix + base;
415
+ if (full.length > maxLen) {
416
+ const suffix = "_" + shortHash(raw, 4);
417
+ full = full.slice(0, Math.max(0, maxLen - suffix.length)) + suffix;
418
+ }
419
+ return full;
420
+ }
421
+ /** Create a collision-resolving allocator bound to a mutable name set. */
422
+ function makeNameAllocator(usedNames) {
423
+ return (raw, kind, opts = {}) => {
424
+ const maxLen = opts.maxLen ?? DEFAULT_MAX_LEN;
425
+ const base = sanitizeIdent(raw, kind, opts);
426
+ if (!usedNames.has(base)) {
427
+ usedNames.add(base);
428
+ return base;
429
+ }
430
+ for (let i = 2; i < 1e4; i++) {
431
+ const suffix = "_" + i;
432
+ const candidate = base.slice(0, Math.max(0, maxLen - suffix.length)) + suffix;
433
+ if (!usedNames.has(candidate)) {
434
+ usedNames.add(candidate);
435
+ return candidate;
436
+ }
437
+ }
438
+ throw new Error(`makeNameAllocator: unable to resolve collision for "${raw}"`);
439
+ };
440
+ }
441
+
442
+ //#endregion
443
+ //#region src/types/errors.ts
444
+ var CyclicTypeError = class extends Error {
445
+ constructor(message) {
446
+ super(message);
447
+ this.name = "CyclicTypeError";
448
+ }
449
+ };
450
+ var CollisionError = class extends Error {
451
+ constructor(message) {
452
+ super(message);
453
+ this.name = "CollisionError";
454
+ }
455
+ };
456
+ var UnsupportedSchemaError = class extends Error {
457
+ constructor(message) {
458
+ super(message);
459
+ this.name = "UnsupportedSchemaError";
460
+ }
461
+ };
462
+
463
+ //#endregion
464
+ //#region src/types/plan.ts
465
+ function isRecord$3(x) {
466
+ return typeof x === "object" && x !== null && !Array.isArray(x);
467
+ }
468
+ /** Extract component schema name from a $ref like "#/components/schemas/Pet". */
469
+ function componentNameFromRef$2(ref) {
470
+ const m = /^#\/components\/schemas\/(.+)$/.exec(ref);
471
+ return m ? m[1] : void 0;
472
+ }
473
+ /** Decide if a (non-ref) schema should get its own plan entry when encountered inline. */
474
+ function isNonTrivialInline(schema) {
475
+ if (isRef(schema)) return false;
476
+ const type = schema.type;
477
+ const props = schema.properties;
478
+ if (type === "object" || isRecord$3(props)) return isRecord$3(props);
479
+ for (const c of [
480
+ "allOf",
481
+ "oneOf",
482
+ "anyOf"
483
+ ]) if (Array.isArray(schema[c])) return true;
484
+ return false;
485
+ }
486
+ /** Walk a single schema (rooted at `rootId`) to collect:
487
+ * - direct dependencies on other entries (by id)
488
+ * - inline entries that must be allocated.
489
+ *
490
+ * Descends into properties / items / combinators / additionalProperties.
491
+ */
492
+ function collectFromSchema(rootId, rootSchema, plannedIds, enqueueInline, schemaToId) {
493
+ const deps = /* @__PURE__ */ new Set();
494
+ const visit = (schema, path, atRoot) => {
495
+ if (!isRecord$3(schema)) return;
496
+ if (isRef(schema)) {
497
+ const name = componentNameFromRef$2(schema.$ref);
498
+ if (name) {
499
+ const depId = `components.schemas.${name}`;
500
+ deps.add(depId);
501
+ }
502
+ return;
503
+ }
504
+ if (!atRoot) {
505
+ const hitId = schemaToId.get(schema);
506
+ if (hitId && hitId !== rootId) {
507
+ deps.add(hitId);
508
+ return;
509
+ }
510
+ if (hitId === rootId) {
511
+ deps.add(rootId);
512
+ return;
513
+ }
514
+ }
515
+ if (!atRoot && isNonTrivialInline(schema)) {
516
+ const id = `${rootId}.${path.join(".")}`;
517
+ if (!plannedIds.has(id)) {
518
+ enqueueInline(id, schema);
519
+ schemaToId.set(schema, id);
520
+ }
521
+ deps.add(id);
522
+ return;
523
+ }
524
+ const rec = schema;
525
+ if (isRecord$3(rec.properties)) for (const [name, sub] of Object.entries(rec.properties)) visit(sub, [
526
+ ...path,
527
+ "properties",
528
+ name
529
+ ], false);
530
+ if (rec.items !== void 0) visit(rec.items, [...path, "items"], false);
531
+ if (isRecord$3(rec.additionalProperties)) visit(rec.additionalProperties, [...path, "additionalProperties"], false);
532
+ for (const c of [
533
+ "allOf",
534
+ "anyOf",
535
+ "oneOf"
536
+ ]) {
537
+ const arr = rec[c];
538
+ if (Array.isArray(arr)) arr.forEach((sub, idx) => {
539
+ visit(sub, [
540
+ ...path,
541
+ c,
542
+ String(idx)
543
+ ], false);
544
+ });
545
+ }
546
+ };
547
+ visit(rootSchema, [], true);
548
+ return [...deps];
549
+ }
550
+ /** Plan the ABAP type universe for a normalized spec. */
551
+ function planTypes(spec, opts) {
552
+ const alloc = makeNameAllocator(/* @__PURE__ */ new Set());
553
+ const entries = [];
554
+ const byId = /* @__PURE__ */ new Map();
555
+ const plannedIds = /* @__PURE__ */ new Set();
556
+ const schemaToId = /* @__PURE__ */ new Map();
557
+ const queue = [];
558
+ const enqueue = (id, schema, source) => {
559
+ if (plannedIds.has(id)) return;
560
+ plannedIds.add(id);
561
+ schemaToId.set(schema, id);
562
+ queue.push({
563
+ id,
564
+ schema,
565
+ source
566
+ });
567
+ };
568
+ for (const [name, schema] of Object.entries(spec.schemas)) enqueue(`components.schemas.${name}`, schema, "component");
569
+ while (queue.length > 0) {
570
+ const item = queue.shift();
571
+ const abapName = alloc(inferRawName(item.id, item.source), "type", { prefix: "ty_" + (opts.typePrefix ?? "") });
572
+ const entry = {
573
+ id: item.id,
574
+ abapName,
575
+ schema: item.schema,
576
+ dependencies: [],
577
+ source: item.source
578
+ };
579
+ entries.push(entry);
580
+ byId.set(item.id, entry);
581
+ entry.dependencies = collectFromSchema(item.id, item.schema, plannedIds, (id, schema) => {
582
+ enqueue(id, schema, "inline");
583
+ }, schemaToId);
584
+ }
585
+ if (entries.some((e) => hasOpenAdditional(e.schema))) {
586
+ const kvId = "aux.kv";
587
+ const kvEntry = {
588
+ id: kvId,
589
+ abapName: alloc("kv", "type", { prefix: "ty_" + (opts.typePrefix ?? "") }),
590
+ schema: {
591
+ type: "object",
592
+ properties: {
593
+ key: { type: "string" },
594
+ value: { type: "string" }
595
+ }
596
+ },
597
+ dependencies: [],
598
+ source: "auxiliary"
599
+ };
600
+ entries.push(kvEntry);
601
+ byId.set(kvId, kvEntry);
602
+ }
603
+ const sorted = topoSort(entries);
604
+ return {
605
+ entries: Object.freeze(sorted.map((e) => Object.freeze({
606
+ ...e,
607
+ dependencies: Object.freeze([...e.dependencies])
608
+ }))),
609
+ byId: new Map(sorted.map((e) => [e.id, {
610
+ ...e,
611
+ dependencies: Object.freeze([...e.dependencies])
612
+ }]))
613
+ };
614
+ }
615
+ function hasOpenAdditional(schema) {
616
+ const ap = schema.additionalProperties;
617
+ if (ap === true) return true;
618
+ if (isRecord$3(ap)) return true;
619
+ return false;
620
+ }
621
+ function inferRawName(id, source) {
622
+ if (source === "component") {
623
+ const m = /^components\.schemas\.(.+)$/.exec(id);
624
+ return m ? m[1] : id;
625
+ }
626
+ if (source === "auxiliary") {
627
+ const m = /^aux\.(.+)$/.exec(id);
628
+ return m ? m[1] : id;
629
+ }
630
+ return id.split(".").filter((p) => p && p !== "properties" && p !== "items").slice(-2).join("_") || id;
631
+ }
632
+ function topoSort(entries) {
633
+ const byId = new Map(entries.map((e) => [e.id, e]));
634
+ const out = [];
635
+ const state = /* @__PURE__ */ new Map();
636
+ for (const e of entries) state.set(e.id, "white");
637
+ const visit = (id, stack) => {
638
+ const s = state.get(id);
639
+ if (s === "black") return;
640
+ const entry = byId.get(id);
641
+ if (!entry) return;
642
+ if (s === "gray") {
643
+ const cycleStart = stack.indexOf(id);
644
+ const cycle = cycleStart >= 0 ? stack.slice(cycleStart) : [id];
645
+ if (cycle.length === 1) {
646
+ entry.selfReferential = true;
647
+ return;
648
+ }
649
+ for (const pid of cycle) {
650
+ const ent = byId.get(pid);
651
+ if (ent) ent.selfReferential = true;
652
+ }
653
+ return;
654
+ }
655
+ state.set(id, "gray");
656
+ stack.push(id);
657
+ for (const dep of entry.dependencies) {
658
+ if (dep === id) {
659
+ entry.selfReferential = true;
660
+ continue;
661
+ }
662
+ if (!byId.has(dep)) continue;
663
+ if (state.get(dep) === "gray") {
664
+ const cycleStart = stack.indexOf(dep);
665
+ const cycle = cycleStart >= 0 ? stack.slice(cycleStart) : [dep];
666
+ for (const pid of [...cycle, id]) {
667
+ const ent = byId.get(pid);
668
+ if (ent) ent.selfReferential = true;
669
+ }
670
+ continue;
671
+ }
672
+ visit(dep, stack);
673
+ }
674
+ stack.pop();
675
+ state.set(id, "black");
676
+ out.push(entry);
677
+ };
678
+ for (const e of entries) visit(e.id, []);
679
+ if (out.length !== entries.length) throw new CyclicTypeError(`planTypes: unable to topologically sort ${entries.length} entries (got ${out.length}).`);
680
+ return out;
681
+ }
682
+
683
+ //#endregion
684
+ //#region src/types/map.ts
685
+ function isRecord$2(x) {
686
+ return typeof x === "object" && x !== null && !Array.isArray(x);
687
+ }
688
+ /** Map a JSON Schema primitive (type+format) to an ABAP builtin type name. */
689
+ function mapPrimitive(schema) {
690
+ const type = schema.type;
691
+ const format = schema.format;
692
+ if (type === "boolean") return "abap_bool";
693
+ if (type === "integer") return format === "int64" ? "int8" : "i";
694
+ if (type === "number") return format === "float" || format === "double" ? "f" : "decfloat34";
695
+ if (type === "string") switch (format) {
696
+ case "date": return "d";
697
+ case "date-time": return "timestampl";
698
+ case "uuid": return "sysuuid_x16";
699
+ case "byte":
700
+ case "binary": return "xstring";
701
+ default: return "string";
702
+ }
703
+ return "string";
704
+ }
705
+ function refToEntry(ref, plan) {
706
+ const m = /^#\/components\/schemas\/(.+)$/.exec(ref);
707
+ if (!m) return void 0;
708
+ return plan.byId.get(`components.schemas.${m[1]}`);
709
+ }
710
+ function pluckType$2(schema) {
711
+ const t = schema.type;
712
+ if (typeof t === "string") return t;
713
+ if (Array.isArray(t)) {
714
+ const nonNull = t.filter((x) => x !== "null");
715
+ if (nonNull.length >= 1) return nonNull[0];
716
+ }
717
+ }
718
+ function isNullable$1(schema) {
719
+ const rec = schema;
720
+ if (rec.nullable === true) return true;
721
+ const t = rec.type;
722
+ if (Array.isArray(t) && t.includes("null")) return true;
723
+ return false;
724
+ }
725
+ /** Resolve a schema to an ABAP TypeRef. */
726
+ function mapSchemaToTypeRef(schema, plan, ctx = {}) {
727
+ if (isRef(schema)) {
728
+ const entry = refToEntry(schema.$ref, plan);
729
+ if (!entry) throw new UnsupportedSchemaError(`mapSchemaToTypeRef: unresolved $ref "${schema.$ref}"`);
730
+ return namedTypeRef({ name: entry.abapName });
731
+ }
732
+ const type = pluckType$2(schema);
733
+ if (type === "array") {
734
+ const items = schema.items;
735
+ const rowSchema = isRecord$2(items) ? items : {};
736
+ return tableType({
737
+ rowType: mapSchemaToTypeRef(rowSchema, plan, ctx),
738
+ tableKind: "standard"
739
+ });
740
+ }
741
+ if (type === "object" || isRecord$2(schema.properties)) {
742
+ const matched = findPlannedEntryBySchema$1(schema, plan, ctx.entryId);
743
+ if (matched) return namedTypeRef({ name: matched.abapName });
744
+ const fields = buildStructureFields$1(schema, plan, ctx);
745
+ if (fields.length === 0) return builtinType({ name: "string" });
746
+ return structureType({ fields });
747
+ }
748
+ if (type === void 0) {
749
+ for (const c of [
750
+ "allOf",
751
+ "oneOf",
752
+ "anyOf"
753
+ ]) if (Array.isArray(schema[c])) {
754
+ const matched = findPlannedEntryBySchema$1(schema, plan, ctx.entryId);
755
+ if (matched) return namedTypeRef({ name: matched.abapName });
756
+ return builtinType({ name: "string" });
757
+ }
758
+ return builtinType({ name: "string" });
759
+ }
760
+ return builtinType({ name: mapPrimitive(schema) });
761
+ }
762
+ /** If a schema is identical-by-reference to a planned entry, return it. */
763
+ function findPlannedEntryBySchema$1(schema, plan, excludeId) {
764
+ for (const entry of plan.entries) {
765
+ if (entry.id === excludeId) continue;
766
+ if (entry.schema === schema) return entry;
767
+ }
768
+ }
769
+ function buildEnumMembers(values) {
770
+ const out = [];
771
+ for (const v of values) if (typeof v === "string") {
772
+ const name = sanitizeIdent(v, "type");
773
+ out.push({
774
+ name,
775
+ value: v
776
+ });
777
+ } else if (typeof v === "number") out.push({
778
+ name: sanitizeIdent(`v_${v}`, "type"),
779
+ value: v
780
+ });
781
+ return out;
782
+ }
783
+ /** Build the list of structure fields for an object-like schema, including
784
+ * combinators, nullable flags, and additionalProperties escape hatch. */
785
+ function buildStructureFields$1(schema, plan, ctx) {
786
+ const fields = [];
787
+ const seen = /* @__PURE__ */ new Map();
788
+ const addField = (name, type, src) => {
789
+ const sanitized = sanitizeIdent(name, "param");
790
+ const prev = seen.get(sanitized);
791
+ if (prev) {
792
+ if (prev === type || shallowTypeRefEqual(prev, type)) return;
793
+ throw new CollisionError(`buildStructureFields: duplicate field "${sanitized}" with conflicting types`);
794
+ }
795
+ seen.set(sanitized, type);
796
+ fields.push({
797
+ name: sanitized,
798
+ type
799
+ });
800
+ if (isNullable$1(src)) {
801
+ const flagName = sanitizeIdent(`${name}_is_null`, "param");
802
+ if (!seen.has(flagName)) {
803
+ const flagType = builtinType({ name: "abap_bool" });
804
+ seen.set(flagName, flagType);
805
+ fields.push({
806
+ name: flagName,
807
+ type: flagType
808
+ });
809
+ }
810
+ }
811
+ };
812
+ const allOf = schema.allOf;
813
+ if (Array.isArray(allOf)) for (const sub of allOf) {
814
+ if (!isRecord$2(sub)) continue;
815
+ const subSchema = sub;
816
+ const subProps = isRef(subSchema) ? (() => {
817
+ const entry = refToEntry(subSchema.$ref, plan);
818
+ if (!entry) return void 0;
819
+ return entry.schema.properties;
820
+ })() : subSchema.properties;
821
+ if (isRecord$2(subProps)) for (const [name, propSchema] of Object.entries(subProps)) {
822
+ if (!isRecord$2(propSchema)) continue;
823
+ addField(name, mapSchemaToTypeRef(propSchema, plan, ctx), propSchema);
824
+ }
825
+ }
826
+ const props = schema.properties;
827
+ if (isRecord$2(props)) for (const [name, propSchema] of Object.entries(props)) {
828
+ if (!isRecord$2(propSchema)) continue;
829
+ addField(name, mapSchemaToTypeRef(propSchema, plan, ctx), propSchema);
830
+ }
831
+ for (const combinator of ["oneOf", "anyOf"]) {
832
+ const arr = schema[combinator];
833
+ if (!Array.isArray(arr)) continue;
834
+ const discriminator = schema.discriminator;
835
+ if (isRecord$2(discriminator) && typeof discriminator.propertyName === "string") {
836
+ addFieldRaw(fields, seen, "kind", builtinType({ name: "string" }));
837
+ arr.forEach((sub, idx) => {
838
+ if (!isRecord$2(sub)) return;
839
+ const variantName = variantFieldName(sub, idx);
840
+ addFieldRaw(fields, seen, variantName, mapSchemaToTypeRef(sub, plan, ctx));
841
+ });
842
+ } else {
843
+ addFieldRaw(fields, seen, "variant_kind", builtinType({ name: "string" }));
844
+ arr.forEach((sub, idx) => {
845
+ if (!isRecord$2(sub)) return;
846
+ const variantName = variantFieldName(sub, idx);
847
+ addFieldRaw(fields, seen, variantName, mapSchemaToTypeRef(sub, plan, ctx));
848
+ addFieldRaw(fields, seen, `${variantName}_is_set`, builtinType({ name: "abap_bool" }));
849
+ });
850
+ }
851
+ }
852
+ const ap = schema.additionalProperties;
853
+ if (ap === true || isRecord$2(ap)) {
854
+ const kvEntry = plan.byId.get("aux.kv");
855
+ if (kvEntry) addFieldRaw(fields, seen, "_extra", tableType({
856
+ rowType: namedTypeRef({ name: kvEntry.abapName }),
857
+ tableKind: "hashed",
858
+ uniqueness: "unique",
859
+ keyFields: ["key"]
860
+ }));
861
+ }
862
+ return fields;
863
+ }
864
+ function addFieldRaw(fields, seen, rawName, type) {
865
+ const name = sanitizeIdent(rawName, "param");
866
+ if (seen.has(name)) return;
867
+ seen.set(name, type);
868
+ fields.push({
869
+ name,
870
+ type
871
+ });
872
+ }
873
+ function shallowTypeRefEqual(a, b) {
874
+ if (a.kind !== b.kind) return false;
875
+ if (a.kind === "BuiltinType" && b.kind === "BuiltinType") return a.name === b.name;
876
+ if (a.kind === "NamedTypeRef" && b.kind === "NamedTypeRef") return a.name === b.name;
877
+ return false;
878
+ }
879
+ function variantFieldName(sub, idx) {
880
+ if (isRef(sub)) {
881
+ const m = /^#\/components\/schemas\/(.+)$/.exec(sub.$ref);
882
+ if (m) return m[1];
883
+ }
884
+ const title = sub.title;
885
+ if (typeof title === "string" && title) return title;
886
+ return `variant_${idx}`;
887
+ }
888
+ /** Build a TypeDef node for a single plan entry. */
889
+ function mapSchemaToTypeDef(entry, plan) {
890
+ const schema = entry.schema;
891
+ const ctx = { entryId: entry.id };
892
+ const enumValues = schema.enum;
893
+ const schemaType = pluckType$2(schema);
894
+ if (Array.isArray(enumValues) && (schemaType === "string" || schemaType === void 0)) {
895
+ const members = buildEnumMembers(enumValues);
896
+ if (members.length > 0) return { typeDef: typeDef({
897
+ name: entry.abapName,
898
+ type: enumType({
899
+ baseType: builtinType({ name: "string" }),
900
+ members
901
+ })
902
+ }) };
903
+ }
904
+ if (schemaType === "array") {
905
+ const items = schema.items;
906
+ const rowSchema = isRecord$2(items) ? items : {};
907
+ return { typeDef: typeDef({
908
+ name: entry.abapName,
909
+ type: tableType({
910
+ rowType: mapSchemaToTypeRef(rowSchema, plan, ctx),
911
+ tableKind: "standard"
912
+ })
913
+ }) };
914
+ }
915
+ if (schemaType === "object" || isRecord$2(schema.properties) || Array.isArray(schema.allOf) || Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) {
916
+ const fields = buildStructureFields$1(schema, plan, ctx);
917
+ const struct = fields.length > 0 ? structureType({ fields }) : structureType({ fields: [{
918
+ name: "_placeholder",
919
+ type: builtinType({ name: "string" })
920
+ }] });
921
+ const discriminator = schema.discriminator;
922
+ const leadingComment = Array.isArray(schema.oneOf) && isRecord$2(discriminator) ? comment({
923
+ text: ` Tagged union: only one variant field is populated at a time (kind discriminator).`,
924
+ style: "star"
925
+ }) : void 0;
926
+ return {
927
+ typeDef: typeDef({
928
+ name: entry.abapName,
929
+ type: struct
930
+ }),
931
+ leadingComment
932
+ };
933
+ }
934
+ return { typeDef: typeDef({
935
+ name: entry.abapName,
936
+ type: builtinType({ name: mapPrimitive(schema) })
937
+ }) };
938
+ }
939
+
940
+ //#endregion
941
+ //#region src/emit/naming.ts
942
+ var NamesConfigError = class extends Error {
943
+ constructor(message) {
944
+ super(message);
945
+ this.name = "NamesConfigError";
946
+ }
947
+ };
948
+ const GLOBAL_NAME_RE = /^[ZY][A-Z0-9_]*$/;
949
+ const LOCAL_NAME_RE = /^[a-z][a-z0-9_]*$/;
950
+ const MAX_LEN = 30;
951
+ const DEFAULT_LOCAL_HTTP = "lcl_http";
952
+ const DEFAULT_LOCAL_RESPONSE = "lcl_response";
953
+ const DEFAULT_LOCAL_JSON = "json";
954
+ const DEFAULT_LOCAL_JSON_PARSER = "lcl_json_parser";
955
+ function validateGlobal(field, value) {
956
+ if (value.length === 0) throw new NamesConfigError(`Invalid ABAP global name for "${field}": value is empty.`);
957
+ if (value.length > MAX_LEN) throw new NamesConfigError(`Invalid ABAP global name for "${field}": "${value}" exceeds ${MAX_LEN} characters.`);
958
+ if (!GLOBAL_NAME_RE.test(value)) throw new NamesConfigError(`Invalid ABAP global name for "${field}": "${value}" must start with Z or Y and contain only uppercase letters, digits, and underscores.`);
959
+ }
960
+ function validateLocal(field, value) {
961
+ if (value.length === 0) throw new NamesConfigError(`Invalid ABAP local class name for "${field}": value is empty.`);
962
+ if (value.length > MAX_LEN) throw new NamesConfigError(`Invalid ABAP local class name for "${field}": "${value}" exceeds ${MAX_LEN} characters.`);
963
+ if (!LOCAL_NAME_RE.test(value)) throw new NamesConfigError(`Invalid ABAP local class name for "${field}": "${value}" must start with a lowercase letter and contain only lowercase letters, digits, and underscores.`);
964
+ }
965
+ function deriveFromBase(base) {
966
+ const trimmed = base.trim();
967
+ if (trimmed.length === 0) throw new NamesConfigError("Invalid \"base\" name: value is empty or whitespace.");
968
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(trimmed)) throw new NamesConfigError(`Invalid "base" name: "${base}" must start with a letter and contain only letters, digits, and underscores.`);
969
+ const upper = trimmed.toUpperCase();
970
+ return {
971
+ typesInterface: `ZIF_${upper}_TYPES`,
972
+ operationsInterface: `ZIF_${upper}`,
973
+ implementationClass: `ZCL_${upper}`,
974
+ exceptionClass: `ZCX_${upper}_ERROR`
975
+ };
976
+ }
977
+ function resolveNames(config) {
978
+ const cfg = config ?? {};
979
+ const hasBase = typeof cfg.base === "string" && cfg.base.trim().length > 0;
980
+ const hasAnyGlobalOverride = cfg.typesInterface !== void 0 || cfg.operationsInterface !== void 0 || cfg.implementationClass !== void 0 || cfg.exceptionClass !== void 0;
981
+ if (!hasBase && !hasAnyGlobalOverride) throw new NamesConfigError("Names configuration requires at least \"base\" or one of the individual overrides (typesInterface, operationsInterface, implementationClass, exceptionClass).");
982
+ const derived = hasBase ? deriveFromBase(cfg.base) : {
983
+ typesInterface: "",
984
+ operationsInterface: "",
985
+ implementationClass: "",
986
+ exceptionClass: ""
987
+ };
988
+ const typesInterface = (cfg.typesInterface ?? derived.typesInterface).trim();
989
+ const operationsInterface = (cfg.operationsInterface ?? derived.operationsInterface).trim();
990
+ const implementationClass = (cfg.implementationClass ?? derived.implementationClass).trim();
991
+ const exceptionClass = (cfg.exceptionClass ?? derived.exceptionClass).trim();
992
+ if (!typesInterface) throw new NamesConfigError("Cannot resolve \"typesInterface\": provide --base or --types-interface.");
993
+ if (!operationsInterface) throw new NamesConfigError("Cannot resolve \"operationsInterface\": provide --base or --operations-interface.");
994
+ if (!implementationClass) throw new NamesConfigError("Cannot resolve \"implementationClass\": provide --base or --class-name.");
995
+ if (!exceptionClass) throw new NamesConfigError("Cannot resolve \"exceptionClass\": provide --base or --exception-class.");
996
+ const resolved = {
997
+ typesInterface,
998
+ operationsInterface,
999
+ implementationClass,
1000
+ exceptionClass,
1001
+ localHttpClass: cfg.localHttpClass ?? DEFAULT_LOCAL_HTTP,
1002
+ localResponseClass: cfg.localResponseClass ?? DEFAULT_LOCAL_RESPONSE,
1003
+ localJsonClass: cfg.localJsonClass ?? DEFAULT_LOCAL_JSON,
1004
+ localJsonParserClass: cfg.localJsonParserClass ?? DEFAULT_LOCAL_JSON_PARSER
1005
+ };
1006
+ validateGlobal("typesInterface", resolved.typesInterface);
1007
+ validateGlobal("operationsInterface", resolved.operationsInterface);
1008
+ validateGlobal("implementationClass", resolved.implementationClass);
1009
+ validateGlobal("exceptionClass", resolved.exceptionClass);
1010
+ validateLocal("localHttpClass", resolved.localHttpClass);
1011
+ validateLocal("localResponseClass", resolved.localResponseClass);
1012
+ validateLocal("localJsonClass", resolved.localJsonClass);
1013
+ validateLocal("localJsonParserClass", resolved.localJsonParserClass);
1014
+ return resolved;
1015
+ }
1016
+
1017
+ //#endregion
1018
+ //#region src/emit/types-interface.ts
1019
+ /**
1020
+ * Layer-1 types interface emitter.
1021
+ *
1022
+ * Builds a single global ABAP interface (e.g. `ZIF_PETSTORE_TYPES`) that
1023
+ * declares every OpenAPI component schema as a `TYPES` entry, in
1024
+ * topological order. Naming follows the v2 convention: no `ty_` prefix —
1025
+ * the OpenAPI schema name is snake_cased directly.
1026
+ *
1027
+ * See packages/openai-codegen/AGENTS.md style references. This module
1028
+ * intentionally bypasses the existing `ty_`-prefixed `mapSchemaToTypeDef`
1029
+ * in `../types/map` and reconstructs TypeDef nodes from scratch so it can
1030
+ * honour the v2 rules (bare names, per-schema map types, cyclic back-ref
1031
+ * detection, controllable null flags, polymorphism union merge).
1032
+ */
1033
+ /** Raised when polymorphism variants disagree on the type of a named field. */
1034
+ var PolymorphismConflictError = class extends Error {
1035
+ constructor(message) {
1036
+ super(message);
1037
+ this.name = "PolymorphismConflictError";
1038
+ }
1039
+ };
1040
+ function emitTypesInterface(spec, plan, opts) {
1041
+ if (!opts.name) throw new Error("emitTypesInterface: opts.name is required");
1042
+ const ctx = buildContext(spec, plan, opts);
1043
+ const typeDefs = [];
1044
+ for (const entry of plan.entries) {
1045
+ if (entry.source === "auxiliary") continue;
1046
+ const aux = ctx.mapAux.get(entry.id);
1047
+ if (aux) {
1048
+ typeDefs.push(aux.entryTypeDef);
1049
+ typeDefs.push(aux.mapTypeDef);
1050
+ }
1051
+ typeDefs.push(buildEntryTypeDef(entry, ctx));
1052
+ }
1053
+ return {
1054
+ interface: interfaceDef({
1055
+ name: opts.name,
1056
+ members: typeDefs,
1057
+ abapDoc: opts.interfaceAbapDoc
1058
+ }),
1059
+ typeDefs
1060
+ };
1061
+ }
1062
+ function isRecord$1(x) {
1063
+ return typeof x === "object" && x !== null && !Array.isArray(x);
1064
+ }
1065
+ function pluckType$1(schema) {
1066
+ const t = schema.type;
1067
+ if (typeof t === "string") return t;
1068
+ if (Array.isArray(t)) {
1069
+ const nonNull = t.filter((x) => x !== "null");
1070
+ if (nonNull.length >= 1) return nonNull[0];
1071
+ }
1072
+ }
1073
+ function isNullable(schema) {
1074
+ const rec = schema;
1075
+ if (rec.nullable === true) return true;
1076
+ const t = rec.type;
1077
+ if (Array.isArray(t) && t.includes("null")) return true;
1078
+ return false;
1079
+ }
1080
+ function componentNameFromRef$1(ref) {
1081
+ const m = /^#\/components\/schemas\/(.+)$/.exec(ref);
1082
+ return m ? m[1] : void 0;
1083
+ }
1084
+ function originalNameFromId(id) {
1085
+ const m = /^components\.schemas\.(.+)$/.exec(id);
1086
+ return m ? m[1] : void 0;
1087
+ }
1088
+ /** Normalise a raw name for v2: replace `$` with `_`, then snake_case ≤ 30. */
1089
+ function v2Name(raw, allocator) {
1090
+ return allocator(raw.replaceAll(/\$/g, "_"), "type");
1091
+ }
1092
+ function buildContext(spec, plan, opts) {
1093
+ const alloc = makeNameAllocator(/* @__PURE__ */ new Set());
1094
+ const nameOf = /* @__PURE__ */ new Map();
1095
+ const originalNameOf = /* @__PURE__ */ new Map();
1096
+ const positionOf = /* @__PURE__ */ new Map();
1097
+ plan.entries.forEach((e, idx) => positionOf.set(e.id, idx));
1098
+ for (const entry of plan.entries) {
1099
+ if (entry.source === "auxiliary") continue;
1100
+ const v2 = v2Name(deriveRawName(entry), alloc);
1101
+ nameOf.set(entry.id, v2);
1102
+ const orig = originalNameFromId(entry.id);
1103
+ if (orig !== void 0) originalNameOf.set(entry.id, orig);
1104
+ }
1105
+ const mapAux = /* @__PURE__ */ new Map();
1106
+ const backRefs = /* @__PURE__ */ new Map();
1107
+ for (const entry of plan.entries) backRefs.set(entry.id, []);
1108
+ const ctxStub = {
1109
+ spec,
1110
+ plan,
1111
+ opts,
1112
+ nameOf,
1113
+ positionOf,
1114
+ originalNameOf,
1115
+ mapAux,
1116
+ backRefs
1117
+ };
1118
+ for (const entry of plan.entries) {
1119
+ if (entry.source === "auxiliary") continue;
1120
+ const ap = entry.schema.additionalProperties;
1121
+ if (ap === void 0 || ap === false) continue;
1122
+ const base = nameOf.get(entry.id);
1123
+ const entryName = v2Name(`${base}_map_entry`, alloc);
1124
+ const mapName = v2Name(`${base}_map`, alloc);
1125
+ const valueRef = asFieldTypeRef(resolveTypeRef(isRecord$1(ap) ? ap : { type: "string" }, entry.id, ctxStub));
1126
+ const entryTypeDef = typeDef({
1127
+ name: entryName,
1128
+ type: structureType({ fields: [{
1129
+ name: "key",
1130
+ type: builtinType({ name: "string" })
1131
+ }, {
1132
+ name: "value",
1133
+ type: valueRef
1134
+ }] }),
1135
+ abapDoc: [`@openapi-map-entry ${originalNameFromId(entry.id) ?? base}`]
1136
+ });
1137
+ const mapTypeDef = typeDef({
1138
+ name: mapName,
1139
+ type: tableType({
1140
+ rowType: namedTypeRef({ name: entryName }),
1141
+ tableKind: "standard"
1142
+ }),
1143
+ abapDoc: [`@openapi-map ${originalNameFromId(entry.id) ?? base}`]
1144
+ });
1145
+ mapAux.set(entry.id, {
1146
+ entryName,
1147
+ mapName,
1148
+ entryTypeDef,
1149
+ mapTypeDef
1150
+ });
1151
+ }
1152
+ return ctxStub;
1153
+ }
1154
+ function deriveRawName(entry) {
1155
+ const stripped = entry.abapName.replace(/^ty_/, "");
1156
+ const orig = originalNameFromId(entry.id);
1157
+ if (orig !== void 0) return orig;
1158
+ return stripped || entry.id;
1159
+ }
1160
+ function buildEntryTypeDef(entry, ctx) {
1161
+ const name = ctx.nameOf.get(entry.id);
1162
+ const schema = entry.schema;
1163
+ const leadingDoc = [];
1164
+ const original = ctx.originalNameOf.get(entry.id);
1165
+ if (original !== void 0) leadingDoc.push(`@openapi-schema ${original}`);
1166
+ else if (entry.source === "inline") leadingDoc.push(`@openapi-path ${entry.id}`);
1167
+ const description = schema.description;
1168
+ if (typeof description === "string" && description.trim()) for (const line of description.split(/\r?\n/)) leadingDoc.push(`@openapi-description ${line}`);
1169
+ const enumValues = schema.enum;
1170
+ const schemaType = pluckType$1(schema);
1171
+ if (Array.isArray(enumValues) && (schemaType === "string" || schemaType === void 0)) {
1172
+ const members = [];
1173
+ const seen = /* @__PURE__ */ new Set();
1174
+ for (const v of enumValues) if (typeof v === "string") {
1175
+ let nm = sanitizeIdent(v, "type");
1176
+ let i = 2;
1177
+ while (seen.has(nm)) nm = `${nm}_${i++}`;
1178
+ seen.add(nm);
1179
+ members.push({
1180
+ name: nm,
1181
+ value: v
1182
+ });
1183
+ } else if (typeof v === "number") {
1184
+ let nm = sanitizeIdent(`v_${v}`, "type");
1185
+ let i = 2;
1186
+ while (seen.has(nm)) nm = `${nm}_${i++}`;
1187
+ seen.add(nm);
1188
+ members.push({
1189
+ name: nm,
1190
+ value: v
1191
+ });
1192
+ }
1193
+ if (members.length > 0) return typeDef({
1194
+ name,
1195
+ type: enumType({
1196
+ baseType: builtinType({ name: "string" }),
1197
+ members
1198
+ }),
1199
+ abapDoc: leadingDoc.length > 0 ? leadingDoc : void 0
1200
+ });
1201
+ }
1202
+ if (schemaType === "array") {
1203
+ const items = schema.items;
1204
+ const rowSchema = isRecord$1(items) ? items : {};
1205
+ return typeDef({
1206
+ name,
1207
+ type: tableType({
1208
+ rowType: resolveTypeRef(rowSchema, entry.id, ctx),
1209
+ tableKind: "standard"
1210
+ }),
1211
+ abapDoc: finaliseDoc(leadingDoc, ctx.backRefs.get(entry.id) ?? [])
1212
+ });
1213
+ }
1214
+ if (schemaType === "object" || isRecord$1(schema.properties) || Array.isArray(schema.allOf) || Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf) || ctx.mapAux.has(entry.id)) {
1215
+ const fields = buildStructureFields(entry, ctx);
1216
+ const finalFields = fields.length > 0 ? fields : [{
1217
+ name: "_placeholder",
1218
+ type: builtinType({ name: "string" })
1219
+ }];
1220
+ return typeDef({
1221
+ name,
1222
+ type: structureType({ fields: finalFields }),
1223
+ abapDoc: finaliseDoc(leadingDoc, ctx.backRefs.get(entry.id) ?? [])
1224
+ });
1225
+ }
1226
+ return typeDef({
1227
+ name,
1228
+ type: builtinType({ name: mapPrimitiveLocal(schema) }),
1229
+ abapDoc: finaliseDoc(leadingDoc, ctx.backRefs.get(entry.id) ?? [])
1230
+ });
1231
+ }
1232
+ function finaliseDoc(base, backRefs) {
1233
+ const out = [...base];
1234
+ for (const br of backRefs) out.push(br);
1235
+ return out.length > 0 ? out : void 0;
1236
+ }
1237
+ function buildStructureFields(entry, ctx) {
1238
+ const schema = entry.schema;
1239
+ const fields = [];
1240
+ const seen = /* @__PURE__ */ new Map();
1241
+ const addField = (rawName, rawType, src) => {
1242
+ const type = asFieldTypeRef(rawType);
1243
+ const fieldName = sanitizeIdent(rawName.replaceAll(/\$/g, "_"), "param");
1244
+ const prev = seen.get(fieldName);
1245
+ if (prev !== void 0) {
1246
+ if (!typeRefEqual(prev, type)) throw new PolymorphismConflictError(`buildStructureFields: conflicting types for field "${fieldName}" in "${entry.id}"`);
1247
+ return;
1248
+ }
1249
+ seen.set(fieldName, type);
1250
+ fields.push({
1251
+ name: fieldName,
1252
+ type
1253
+ });
1254
+ if (ctx.opts.emitNullFlags && isNullable(src)) {
1255
+ const flagName = sanitizeIdent(`${rawName}_is_null`, "param");
1256
+ if (!seen.has(flagName)) {
1257
+ const flagType = builtinType({ name: "abap_bool" });
1258
+ seen.set(flagName, flagType);
1259
+ fields.push({
1260
+ name: flagName,
1261
+ type: flagType
1262
+ });
1263
+ }
1264
+ }
1265
+ };
1266
+ const mergeProperties = (src) => {
1267
+ const props = src.properties;
1268
+ if (isRecord$1(props)) for (const [propName, propSchema] of Object.entries(props)) {
1269
+ if (!isRecord$1(propSchema)) continue;
1270
+ const ref = resolveTypeRef(propSchema, entry.id, ctx, propName);
1271
+ addField(propName, ref, propSchema);
1272
+ }
1273
+ };
1274
+ const allOf = schema.allOf;
1275
+ if (Array.isArray(allOf)) for (const sub of allOf) {
1276
+ if (!isRecord$1(sub)) continue;
1277
+ if (isRef(sub)) {
1278
+ const refEntry = resolveRefEntry(sub.$ref, ctx);
1279
+ if (refEntry !== void 0) mergeProperties(refEntry.schema);
1280
+ } else mergeProperties(sub);
1281
+ }
1282
+ mergeProperties(schema);
1283
+ for (const combinator of ["oneOf", "anyOf"]) {
1284
+ const arr = schema[combinator];
1285
+ if (!Array.isArray(arr)) continue;
1286
+ for (const sub of arr) {
1287
+ if (!isRecord$1(sub)) continue;
1288
+ if (isRef(sub)) {
1289
+ const refEntry = resolveRefEntry(sub.$ref, ctx);
1290
+ if (refEntry !== void 0) mergeProperties(refEntry.schema);
1291
+ } else mergeProperties(sub);
1292
+ }
1293
+ }
1294
+ const aux = ctx.mapAux.get(entry.id);
1295
+ if (aux) {
1296
+ const fieldName = sanitizeIdent("entries", "param");
1297
+ if (!seen.has(fieldName)) {
1298
+ const ref = namedTypeRef({ name: aux.mapName });
1299
+ seen.set(fieldName, ref);
1300
+ fields.push({
1301
+ name: fieldName,
1302
+ type: ref
1303
+ });
1304
+ }
1305
+ }
1306
+ return fields;
1307
+ }
1308
+ function resolveRefEntry(ref, ctx) {
1309
+ const name = componentNameFromRef$1(ref);
1310
+ if (!name) return void 0;
1311
+ return ctx.plan.byId.get(`components.schemas.${name}`);
1312
+ }
1313
+ function typeRefEqual(a, b) {
1314
+ if (a.kind !== b.kind) return false;
1315
+ if (a.kind === "BuiltinType" && b.kind === "BuiltinType") return a.name === b.name;
1316
+ if (a.kind === "NamedTypeRef" && b.kind === "NamedTypeRef") return a.name === b.name;
1317
+ if (a.kind === "TableType" && b.kind === "TableType") return typeRefEqual(a.rowType, b.rowType);
1318
+ return false;
1319
+ }
1320
+ /**
1321
+ * Convert a TypeRef that may be a TableType into a form that can appear as a
1322
+ * structure-field type. The underlying ABAP printer only accepts BuiltinType
1323
+ * or NamedTypeRef inline; we synthesise a NamedTypeRef whose "name" is the
1324
+ * full inline table expression so the printer emits it verbatim.
1325
+ */
1326
+ function asFieldTypeRef(t) {
1327
+ if (t.kind !== "TableType") return t;
1328
+ const row = t.rowType;
1329
+ let rowExpr;
1330
+ if (row.kind === "BuiltinType") rowExpr = row.name;
1331
+ else if (row.kind === "NamedTypeRef") rowExpr = row.name;
1332
+ else rowExpr = "string";
1333
+ return namedTypeRef({ name: `STANDARD TABLE OF ${rowExpr} WITH EMPTY KEY` });
1334
+ }
1335
+ function resolveTypeRef(schema, ownerEntryId, ctx, fieldName) {
1336
+ if (isRef(schema)) {
1337
+ const target = resolveRefEntry(schema.$ref, ctx);
1338
+ if (target === void 0) return builtinType({ name: "string" });
1339
+ const ownerPos = ctx.positionOf.get(ownerEntryId) ?? -1;
1340
+ const targetPos = ctx.positionOf.get(target.id) ?? -1;
1341
+ const isBackEdge = targetPos >= 0 && ownerPos >= 0 && targetPos >= ownerPos;
1342
+ if (target.id === ownerEntryId || isBackEdge) {
1343
+ const targetOriginal = ctx.originalNameOf.get(target.id) ?? ctx.nameOf.get(target.id) ?? target.id;
1344
+ const backRefs = ctx.backRefs.get(ownerEntryId);
1345
+ if (backRefs !== void 0) {
1346
+ const label = fieldName ? `${fieldName}:${targetOriginal}` : targetOriginal;
1347
+ backRefs.push(`@openapi-ref ${label}`);
1348
+ }
1349
+ return namedTypeRef({ name: "REF TO data" });
1350
+ }
1351
+ return namedTypeRef({ name: ctx.nameOf.get(target.id) ?? target.abapName });
1352
+ }
1353
+ const type = pluckType$1(schema);
1354
+ if (type === "array") {
1355
+ const items = schema.items;
1356
+ const rowSchema = isRecord$1(items) ? items : {};
1357
+ return tableType({
1358
+ rowType: resolveTypeRef(rowSchema, ownerEntryId, ctx, fieldName),
1359
+ tableKind: "standard"
1360
+ });
1361
+ }
1362
+ const props = schema.properties;
1363
+ if (type === "object" || isRecord$1(props)) {
1364
+ const matched = findPlannedEntryBySchema(schema, ctx.plan, ownerEntryId);
1365
+ if (matched !== void 0) {
1366
+ const name = ctx.nameOf.get(matched.id);
1367
+ if (name !== void 0) return namedTypeRef({ name });
1368
+ }
1369
+ return builtinType({ name: "string" });
1370
+ }
1371
+ if (type === void 0) {
1372
+ for (const c of [
1373
+ "allOf",
1374
+ "oneOf",
1375
+ "anyOf"
1376
+ ]) if (Array.isArray(schema[c])) {
1377
+ const matched = findPlannedEntryBySchema(schema, ctx.plan, ownerEntryId);
1378
+ if (matched !== void 0) {
1379
+ const name = ctx.nameOf.get(matched.id);
1380
+ if (name !== void 0) return namedTypeRef({ name });
1381
+ }
1382
+ return builtinType({ name: "string" });
1383
+ }
1384
+ return builtinType({ name: "string" });
1385
+ }
1386
+ return builtinType({ name: mapPrimitiveLocal(schema) });
1387
+ }
1388
+ function findPlannedEntryBySchema(schema, plan, excludeId) {
1389
+ for (const entry of plan.entries) {
1390
+ if (entry.id === excludeId) continue;
1391
+ if (entry.schema === schema) return entry;
1392
+ }
1393
+ }
1394
+ function mapPrimitiveLocal(schema) {
1395
+ const type = pluckType$1(schema);
1396
+ const format = schema.format;
1397
+ if (type === "boolean") return "abap_bool";
1398
+ if (type === "integer") return format === "int64" ? "int8" : "i";
1399
+ if (type === "number") return format === "float" || format === "double" ? "f" : "decfloat34";
1400
+ if (type === "string") switch (format) {
1401
+ case "date": return "d";
1402
+ case "date-time": return "timestampl";
1403
+ case "uuid": return "sysuuid_x16";
1404
+ case "byte":
1405
+ case "binary": return "xstring";
1406
+ default: return "string";
1407
+ }
1408
+ return "string";
1409
+ }
1410
+
1411
+ //#endregion
1412
+ //#region src/emit/operations-interface.ts
1413
+ /**
1414
+ * Layer 2 emitter — OPERATIONS INTERFACE (`ZIF_<base>`).
1415
+ *
1416
+ * Produces a single global ABAP interface whose methods mirror the OpenAPI
1417
+ * `operations` list 1:1. All type references point into the types interface
1418
+ * emitted by the Wave 1 Layer-1 emitter (`ZIF_<base>_TYPES`).
1419
+ *
1420
+ * The design is deliberately conservative:
1421
+ * - RETURNING values carry a bare, natural name (no `rv_` / `rt_` prefix).
1422
+ * - IMPORTING parameters use the OpenAPI parameter name, snake-cased.
1423
+ * - Body parameters are always called `body`.
1424
+ * - Every method RAISES the single generated exception class.
1425
+ * - Cookie parameters are skipped with an `"! @openapi-todo` note.
1426
+ *
1427
+ * The emitter also returns a parallel `operations` array describing each
1428
+ * method's mapping. The Wave 2 implementation-class emitter consumes this
1429
+ * array to build per-operation bodies.
1430
+ */
1431
+ function isRecord(x) {
1432
+ return typeof x === "object" && x !== null && !Array.isArray(x);
1433
+ }
1434
+ function pluckType(schema) {
1435
+ if (!schema) return void 0;
1436
+ const t = schema.type;
1437
+ if (typeof t === "string") return t;
1438
+ if (Array.isArray(t)) {
1439
+ const nonNull = t.filter((x) => x !== "null");
1440
+ if (nonNull.length >= 1) return nonNull[0];
1441
+ }
1442
+ }
1443
+ function isPlainObjectSchema(schema) {
1444
+ return pluckType(schema) === "object" || isRecord(schema.properties);
1445
+ }
1446
+ function componentNameFromRef(ref) {
1447
+ const m = /^#\/components\/schemas\/(.+)$/.exec(ref);
1448
+ return m ? m[1] : void 0;
1449
+ }
1450
+ function buildComponentIndex(spec) {
1451
+ const byPropSig = /* @__PURE__ */ new Map();
1452
+ for (const [name, schema] of Object.entries(spec.schemas)) {
1453
+ if (!isRecord(schema)) continue;
1454
+ const props = schema.properties;
1455
+ if (!isRecord(props)) continue;
1456
+ const keys = Object.keys(props).sort((a, b) => a.localeCompare(b)).join(",");
1457
+ if (keys && !byPropSig.has(keys)) byPropSig.set(keys, name);
1458
+ }
1459
+ return { byPropSig };
1460
+ }
1461
+ /** Identify which component schema a (possibly dereferenced) schema refers to. */
1462
+ function matchComponent(schema, index) {
1463
+ if (!schema) return void 0;
1464
+ if (isRef(schema)) return componentNameFromRef(schema.$ref);
1465
+ if (!isPlainObjectSchema(schema)) return void 0;
1466
+ const props = schema.properties;
1467
+ if (!isRecord(props)) return void 0;
1468
+ const sig = Object.keys(props).sort((a, b) => a.localeCompare(b)).join(",");
1469
+ if (!sig) return void 0;
1470
+ return index.byPropSig.get(sig);
1471
+ }
1472
+ function typeInterfaceMember(typesInterfaceName, componentName) {
1473
+ return `${typesInterfaceName.toLowerCase()}=>${sanitizeIdent(componentName, "type")}`;
1474
+ }
1475
+ /** Render a schema as the printable form used inline after `TYPE`. */
1476
+ function renderSchemaTypeSource(schema, index, typesInterfaceName) {
1477
+ if (!schema) return "string";
1478
+ if (isRef(schema)) {
1479
+ const name = componentNameFromRef(schema.$ref);
1480
+ return name ? typeInterfaceMember(typesInterfaceName, name) : "string";
1481
+ }
1482
+ if (pluckType(schema) === "array") {
1483
+ const items = schema.items;
1484
+ return `STANDARD TABLE OF ${renderSchemaTypeSource(isRecord(items) ? items : {}, index, typesInterfaceName)}`;
1485
+ }
1486
+ if (isPlainObjectSchema(schema)) {
1487
+ const comp = matchComponent(schema, index);
1488
+ return comp ? typeInterfaceMember(typesInterfaceName, comp) : "string";
1489
+ }
1490
+ return mapPrimitive(schema);
1491
+ }
1492
+ /**
1493
+ * Build an ABAP `TypeRef` for a schema usable as an inline type reference.
1494
+ *
1495
+ * For array shapes we ALWAYS emit a named alias (`<ops-intf>=><name>_list`)
1496
+ * into the operations interface itself. Cloud ABAP does NOT accept inline
1497
+ * `STANDARD TABLE OF ...` clauses inside a METHODS parameter list (the SAP
1498
+ * ADT save endpoint returns "An error occured during the save operation"),
1499
+ * so every array-typed parameter or return must be a named type reference.
1500
+ * The aliases are collected via {@link registerTableAlias} and emitted as
1501
+ * TYPES declarations at the head of the interface in {@link emitOperationsInterface}.
1502
+ */
1503
+ function schemaToTypeRef(schema, index, typesInterfaceName, opsInterfaceName, aliases) {
1504
+ if (!schema) return builtinType({ name: "string" });
1505
+ if (isRef(schema)) {
1506
+ const name = componentNameFromRef(schema.$ref);
1507
+ if (name) return namedTypeRef({ name: typeInterfaceMember(typesInterfaceName, name) });
1508
+ return builtinType({ name: "string" });
1509
+ }
1510
+ if (pluckType(schema) === "array") {
1511
+ const items = schema.items;
1512
+ const aliasName = registerTableAlias(aliases, renderSchemaTypeSource(isRecord(items) ? items : {}, index, typesInterfaceName));
1513
+ return namedTypeRef({ name: `${opsInterfaceName.toLowerCase()}=>${aliasName}` });
1514
+ }
1515
+ if (isPlainObjectSchema(schema)) {
1516
+ const comp = matchComponent(schema, index);
1517
+ if (comp) return namedTypeRef({ name: typeInterfaceMember(typesInterfaceName, comp) });
1518
+ return builtinType({ name: "string" });
1519
+ }
1520
+ return builtinType({ name: mapPrimitive(schema) });
1521
+ }
1522
+ function createTableAliasRegistry() {
1523
+ return {
1524
+ aliases: /* @__PURE__ */ new Map(),
1525
+ order: []
1526
+ };
1527
+ }
1528
+ /** Return the alias name for a given row type, registering it if new. */
1529
+ function registerTableAlias(registry, rowType) {
1530
+ const existing = registry.aliases.get(rowType);
1531
+ if (existing !== void 0) return existing;
1532
+ const alias = deriveTableAliasName(rowType, registry.aliases);
1533
+ registry.aliases.set(rowType, alias);
1534
+ registry.order.push(alias);
1535
+ return alias;
1536
+ }
1537
+ /**
1538
+ * Derive an alias name from the row-type source.
1539
+ * Strip any interface prefix (`zif_foo_types=>`) and keep only the schema
1540
+ * name, then append `_list`. Collisions get a `_2`, `_3` … suffix.
1541
+ */
1542
+ function deriveTableAliasName(rowType, existing) {
1543
+ const taken = new Set(existing.values());
1544
+ const base = `${(rowType.includes("=>") ? rowType.split("=>").pop() ?? rowType : rowType).replaceAll(/[^a-z0-9_]/gi, "_").toLowerCase()}_list`;
1545
+ if (!taken.has(base)) return base;
1546
+ let i = 2;
1547
+ while (taken.has(`${base}_${i}`)) i += 1;
1548
+ return `${base}_${i}`;
1549
+ }
1550
+ /** Build TypeDef nodes for every registered table alias. */
1551
+ function buildAliasTypeDefs(registry) {
1552
+ const inverse = /* @__PURE__ */ new Map();
1553
+ for (const [rowType, alias] of registry.aliases) inverse.set(alias, rowType);
1554
+ return registry.order.map((alias) => {
1555
+ const rowType = inverse.get(alias) ?? "string";
1556
+ return typeDef({
1557
+ name: alias,
1558
+ type: tableType({
1559
+ rowType: namedTypeRef({ name: rowType }),
1560
+ tableKind: "standard"
1561
+ })
1562
+ });
1563
+ });
1564
+ }
1565
+ function pickSuccess(op) {
1566
+ const successes = op.responses.filter((r) => r.isSuccess);
1567
+ if (successes.length === 0) return void 0;
1568
+ let chosen;
1569
+ chosen = successes.find((r) => r.statusCode === "200");
1570
+ if (!chosen) chosen = successes.filter((r) => /^2\d\d$/.test(r.statusCode)).sort((a, b) => Number(a.statusCode) - Number(b.statusCode))[0] ?? successes[0];
1571
+ const status = chosen.statusCode === "default" ? "default" : Number(chosen.statusCode);
1572
+ const content = chosen.content;
1573
+ const schema = (content["application/json"] ?? content["application/octet-stream"] ?? content[Object.keys(content)[0] ?? ""])?.schema;
1574
+ const hasSchema = schema !== void 0 && Object.keys(schema).length > 0;
1575
+ return {
1576
+ response: chosen,
1577
+ status,
1578
+ schema: hasSchema ? schema : void 0
1579
+ };
1580
+ }
1581
+ function pickRequestBodySchema$1(op) {
1582
+ const rb = op.requestBody;
1583
+ if (!rb) return void 0;
1584
+ const c = rb.content;
1585
+ const schema = (c["application/json"] ?? c["application/octet-stream"] ?? c[Object.keys(c)[0] ?? ""])?.schema;
1586
+ if (!schema) return void 0;
1587
+ return Object.keys(schema).length > 0 ? schema : void 0;
1588
+ }
1589
+ function deriveReturningName(schema, index) {
1590
+ if (!schema) return "result";
1591
+ if (isRef(schema)) {
1592
+ const name = componentNameFromRef(schema.$ref);
1593
+ return name ? sanitizeIdent(name, "param") : "result";
1594
+ }
1595
+ if (pluckType(schema) === "array") {
1596
+ const items = schema.items;
1597
+ if (isRecord(items)) {
1598
+ const inner = deriveReturningName(items, index);
1599
+ if (inner === "result") return "results";
1600
+ return pluralize(inner);
1601
+ }
1602
+ return "results";
1603
+ }
1604
+ if (isPlainObjectSchema(schema)) {
1605
+ const comp = matchComponent(schema, index);
1606
+ if (comp) return sanitizeIdent(comp, "param");
1607
+ return "result";
1608
+ }
1609
+ return "result";
1610
+ }
1611
+ function pluralize(name) {
1612
+ if (!name) return "results";
1613
+ if (/(s|x|z|ch|sh)$/i.test(name)) return `${name}_list`;
1614
+ if (/y$/i.test(name) && !/[aeiou]y$/i.test(name)) return `${name.slice(0, -1)}ies`;
1615
+ return `${name}s`;
1616
+ }
1617
+ function orderParams(params) {
1618
+ const path = params.filter((p) => p.in === "path");
1619
+ const query = params.filter((p) => p.in === "query");
1620
+ const header = params.filter((p) => p.in === "header");
1621
+ const cookie = params.filter((p) => p.in === "cookie");
1622
+ return {
1623
+ emitted: [
1624
+ ...path,
1625
+ ...query,
1626
+ ...header
1627
+ ],
1628
+ cookieSkipped: cookie
1629
+ };
1630
+ }
1631
+ function buildMethodAbapDoc(op, cookieSkipped) {
1632
+ const lines = [];
1633
+ lines.push(`@openapi-operation ${op.operationId}`);
1634
+ lines.push(`@openapi-path ${op.method.toUpperCase()} ${op.path}`);
1635
+ if (op.summary) {
1636
+ for (const l of op.summary.split(/\r?\n/)) if (l.trim().length > 0) lines.push(l.trim());
1637
+ }
1638
+ if (op.deprecated) lines.push("@openapi-deprecated");
1639
+ for (const c of cookieSkipped) lines.push(`@openapi-todo cookie param '${c.name}' not mapped`);
1640
+ return lines;
1641
+ }
1642
+ function emitOperationsInterface(spec, _plan, opts) {
1643
+ const compIndex = buildComponentIndex(spec);
1644
+ const allocMethod = makeNameAllocator(/* @__PURE__ */ new Set());
1645
+ const methods = [];
1646
+ const mappings = [];
1647
+ const tableAliases = createTableAliasRegistry();
1648
+ const opsInterfaceName = opts.name;
1649
+ for (const op of spec.operations) {
1650
+ const methodName = allocMethod(op.operationId, "method");
1651
+ const { emitted, cookieSkipped } = orderParams(op.parameters);
1652
+ const allocParam = makeNameAllocator(/* @__PURE__ */ new Set());
1653
+ const abapParams = [];
1654
+ const paramMappings = [];
1655
+ for (const p of emitted) {
1656
+ const abapName = allocParam(p.name, "param");
1657
+ const ref = schemaToTypeRef(p.schema, compIndex, opts.typesInterfaceName, opsInterfaceName, tableAliases);
1658
+ abapParams.push(methodParam({
1659
+ paramKind: "importing",
1660
+ name: abapName,
1661
+ typeRef: ref,
1662
+ optional: !p.required
1663
+ }));
1664
+ paramMappings.push({
1665
+ name: abapName,
1666
+ openapiName: p.name,
1667
+ location: p.in,
1668
+ required: p.required,
1669
+ typeRefSource: renderTypeRefSource(ref),
1670
+ isTable: pluckType(p.schema) === "array"
1671
+ });
1672
+ }
1673
+ const bodySchema = pickRequestBodySchema$1(op);
1674
+ const hasBody = bodySchema !== void 0;
1675
+ if (hasBody) {
1676
+ const bodyName = allocParam("body", "param");
1677
+ const ref = schemaToTypeRef(bodySchema, compIndex, opts.typesInterfaceName, opsInterfaceName, tableAliases);
1678
+ abapParams.push(methodParam({
1679
+ paramKind: "importing",
1680
+ name: bodyName,
1681
+ typeRef: ref,
1682
+ optional: op.requestBody?.required === false ? true : false
1683
+ }));
1684
+ }
1685
+ const success = pickSuccess(op);
1686
+ let successStatus;
1687
+ let successIsEmptyBody;
1688
+ let returningName;
1689
+ let returningRef;
1690
+ if (!success) {
1691
+ successStatus = 200;
1692
+ successIsEmptyBody = true;
1693
+ returningName = allocParam("success", "param");
1694
+ returningRef = builtinType({ name: "abap_bool" });
1695
+ } else if (success.schema === void 0) {
1696
+ successStatus = success.status;
1697
+ successIsEmptyBody = true;
1698
+ returningName = allocParam("success", "param");
1699
+ returningRef = builtinType({ name: "abap_bool" });
1700
+ } else {
1701
+ successStatus = success.status;
1702
+ successIsEmptyBody = false;
1703
+ returningName = allocParam(deriveReturningName(success.schema, compIndex), "param");
1704
+ returningRef = schemaToTypeRef(success.schema, compIndex, opts.typesInterfaceName, opsInterfaceName, tableAliases);
1705
+ }
1706
+ abapParams.push(methodParam({
1707
+ paramKind: "returning",
1708
+ name: returningName,
1709
+ typeRef: returningRef
1710
+ }));
1711
+ const errorResponses = {};
1712
+ for (const r of op.responses) {
1713
+ if (!r.isError) continue;
1714
+ errorResponses[r.statusCode] = r.description ? { description: r.description } : {};
1715
+ }
1716
+ const method = methodDef({
1717
+ name: methodName,
1718
+ visibility: "public",
1719
+ params: abapParams,
1720
+ raising: [namedTypeRef({ name: opts.exceptionClassName.toLowerCase() })],
1721
+ abapDoc: buildMethodAbapDoc(op, cookieSkipped)
1722
+ });
1723
+ methods.push(method);
1724
+ mappings.push({
1725
+ operationId: op.operationId,
1726
+ methodName,
1727
+ method: op.method.toUpperCase(),
1728
+ path: op.path,
1729
+ params: paramMappings,
1730
+ hasBody,
1731
+ successStatus,
1732
+ successIsEmptyBody,
1733
+ errorResponses,
1734
+ returningName
1735
+ });
1736
+ }
1737
+ const aliasTypeDefs = buildAliasTypeDefs(tableAliases);
1738
+ return {
1739
+ interface: interfaceDef({
1740
+ name: opts.name.toLowerCase(),
1741
+ members: [...aliasTypeDefs, ...methods],
1742
+ abapDoc: opts.interfaceAbapDoc
1743
+ }),
1744
+ operations: mappings
1745
+ };
1746
+ }
1747
+ function renderTypeRefSource(ref) {
1748
+ switch (ref.kind) {
1749
+ case "BuiltinType": return ref.name;
1750
+ case "NamedTypeRef": return ref.name;
1751
+ case "TableType":
1752
+ case "StructureType": return ref.kind;
1753
+ }
1754
+ }
1755
+
1756
+ //#endregion
1757
+ //#region src/emit/exception-class.ts
1758
+ /**
1759
+ * Emit the generated ABAP exception class that carries HTTP error detail
1760
+ * (status code, reason phrase, raw body, response headers).
1761
+ *
1762
+ * The class inherits from `cx_static_check` and is FINAL with CREATE PUBLIC.
1763
+ * It exposes four READ-ONLY attributes and a constructor that assigns them
1764
+ * after delegating to `super->constructor( )`.
1765
+ */
1766
+ /** Build the constructor's IMPLEMENTATION body (super call + 4 assignments). */
1767
+ function buildConstructorImpl() {
1768
+ const superCall = call({
1769
+ receiver: identifierExpr({ name: "super" }),
1770
+ method: "constructor",
1771
+ callKind: "instance",
1772
+ args: []
1773
+ });
1774
+ const assignments = [
1775
+ "status",
1776
+ "description",
1777
+ "body",
1778
+ "headers"
1779
+ ].map((f) => assign({
1780
+ target: identifierExpr({ name: `me->${f}` }),
1781
+ value: identifierExpr({ name: f })
1782
+ }));
1783
+ return methodImpl({
1784
+ name: "constructor",
1785
+ body: [superCall, ...assignments]
1786
+ });
1787
+ }
1788
+ function emitExceptionClass(opts) {
1789
+ if (!opts.name) throw new Error("emitExceptionClass: required field \"name\" is missing");
1790
+ const kvType = typeDef({
1791
+ name: "kv",
1792
+ type: structureType({ fields: [{
1793
+ name: "k",
1794
+ type: builtinType({ name: "string" })
1795
+ }, {
1796
+ name: "v",
1797
+ type: builtinType({ name: "string" })
1798
+ }] })
1799
+ });
1800
+ const kvsType = typeDef({
1801
+ name: "kvs",
1802
+ type: tableType({
1803
+ rowType: namedTypeRef({ name: "kv" }),
1804
+ tableKind: "standard"
1805
+ })
1806
+ });
1807
+ const statusAttr = attributeDef({
1808
+ name: "status",
1809
+ type: builtinType({ name: "i" }),
1810
+ visibility: "public",
1811
+ readOnly: true
1812
+ });
1813
+ const descriptionAttr = attributeDef({
1814
+ name: "description",
1815
+ type: builtinType({ name: "string" }),
1816
+ visibility: "public",
1817
+ readOnly: true
1818
+ });
1819
+ const bodyAttr = attributeDef({
1820
+ name: "body",
1821
+ type: builtinType({ name: "xstring" }),
1822
+ visibility: "public",
1823
+ readOnly: true
1824
+ });
1825
+ const headersAttr = attributeDef({
1826
+ name: "headers",
1827
+ type: namedTypeRef({ name: "kvs" }),
1828
+ visibility: "public",
1829
+ readOnly: true
1830
+ });
1831
+ const ctor = methodDef({
1832
+ name: "constructor",
1833
+ visibility: "public",
1834
+ params: [
1835
+ methodParam({
1836
+ paramKind: "importing",
1837
+ name: "status",
1838
+ typeRef: builtinType({ name: "i" })
1839
+ }),
1840
+ methodParam({
1841
+ paramKind: "importing",
1842
+ name: "description",
1843
+ typeRef: builtinType({ name: "string" }),
1844
+ optional: true
1845
+ }),
1846
+ methodParam({
1847
+ paramKind: "importing",
1848
+ name: "body",
1849
+ typeRef: builtinType({ name: "xstring" }),
1850
+ optional: true
1851
+ }),
1852
+ methodParam({
1853
+ paramKind: "importing",
1854
+ name: "headers",
1855
+ typeRef: namedTypeRef({ name: "kvs" }),
1856
+ optional: true
1857
+ })
1858
+ ]
1859
+ });
1860
+ const publicSection = section({
1861
+ visibility: "public",
1862
+ members: [
1863
+ kvType,
1864
+ kvsType,
1865
+ statusAttr,
1866
+ descriptionAttr,
1867
+ bodyAttr,
1868
+ headersAttr,
1869
+ ctor
1870
+ ]
1871
+ });
1872
+ return { class: classDef({
1873
+ name: opts.name,
1874
+ superclass: "cx_static_check",
1875
+ isFinal: true,
1876
+ sections: [publicSection],
1877
+ implementations: [buildConstructorImpl()],
1878
+ abapDoc: opts.interfaceAbapDoc ? [...opts.interfaceAbapDoc] : void 0
1879
+ }) };
1880
+ }
1881
+
1882
+ //#endregion
1883
+ //#region src/emit/templates/locals-def.abap.ts
1884
+ function buildLocalsDef(names) {
1885
+ const { localJsonClass: JSON_CLS, localJsonParserClass: JSON_PARSER, localResponseClass: RESPONSE, localHttpClass: HTTP } = names;
1886
+ names.exceptionClass;
1887
+ return `* Generated by @abapify/openai-codegen - local class declarations.
1888
+ * ZERO Z-dependencies: these locals wrap SAP kernel APIs only.
1889
+ * Per-operation methods map HTTP status codes to the custom exception.
1890
+
1891
+ CLASS ${HTTP} DEFINITION DEFERRED.
1892
+ CLASS ${RESPONSE} DEFINITION DEFERRED.
1893
+ CLASS ${JSON_PARSER} DEFINITION DEFERRED.
1894
+ CLASS ${JSON_CLS} DEFINITION DEFERRED.
1895
+
1896
+ CLASS ${HTTP} DEFINITION FINAL CREATE PUBLIC.
1897
+ PUBLIC SECTION.
1898
+ TYPES: BEGIN OF kv,
1899
+ name TYPE string,
1900
+ value TYPE string,
1901
+ END OF kv.
1902
+ TYPES kvs TYPE STANDARD TABLE OF kv WITH DEFAULT KEY.
1903
+
1904
+ METHODS constructor
1905
+ IMPORTING destination TYPE string
1906
+ server TYPE string OPTIONAL.
1907
+
1908
+ METHODS fetch
1909
+ IMPORTING method TYPE string
1910
+ path TYPE string
1911
+ query TYPE kvs OPTIONAL
1912
+ headers TYPE kvs OPTIONAL
1913
+ body TYPE string OPTIONAL
1914
+ binary TYPE xstring OPTIONAL
1915
+ RETURNING VALUE(response) TYPE REF TO ${RESPONSE}
1916
+ RAISING cx_web_http_client_error
1917
+ cx_http_dest_provider_error.
1918
+
1919
+ PRIVATE SECTION.
1920
+ DATA destination TYPE string.
1921
+ DATA server TYPE string.
1922
+
1923
+ METHODS build_query_string
1924
+ IMPORTING query TYPE kvs
1925
+ RETURNING VALUE(result) TYPE string.
1926
+ ENDCLASS.
1927
+
1928
+ CLASS ${RESPONSE} DEFINITION FINAL CREATE PRIVATE FRIENDS ${HTTP}.
1929
+ PUBLIC SECTION.
1930
+ METHODS status RETURNING VALUE(result) TYPE i.
1931
+ METHODS body RETURNING VALUE(result) TYPE xstring.
1932
+ METHODS text RETURNING VALUE(result) TYPE string.
1933
+ METHODS header
1934
+ IMPORTING name TYPE string
1935
+ RETURNING VALUE(result) TYPE string.
1936
+ METHODS headers RETURNING VALUE(result) TYPE ${HTTP}=>kvs.
1937
+
1938
+ PRIVATE SECTION.
1939
+ METHODS constructor
1940
+ IMPORTING status_code TYPE i
1941
+ body_bytes TYPE xstring
1942
+ body_text TYPE string
1943
+ header_list TYPE ${HTTP}=>kvs.
1944
+
1945
+ DATA status_code TYPE i.
1946
+ DATA body_bytes TYPE xstring.
1947
+ DATA body_text TYPE string.
1948
+ DATA header_list TYPE ${HTTP}=>kvs.
1949
+ ENDCLASS.
1950
+
1951
+ CLASS ${JSON_PARSER} DEFINITION FINAL CREATE PRIVATE FRIENDS ${JSON_CLS}.
1952
+ PUBLIC SECTION.
1953
+ METHODS to
1954
+ IMPORTING target TYPE REF TO data.
1955
+
1956
+ PRIVATE SECTION.
1957
+ METHODS constructor
1958
+ IMPORTING json_text TYPE string.
1959
+
1960
+ DATA json_text TYPE string.
1961
+ ENDCLASS.
1962
+
1963
+ CLASS ${JSON_CLS} DEFINITION FINAL CREATE PRIVATE.
1964
+ PUBLIC SECTION.
1965
+ CLASS-METHODS stringify
1966
+ IMPORTING data TYPE any
1967
+ RETURNING VALUE(result) TYPE string.
1968
+
1969
+ CLASS-METHODS render
1970
+ IMPORTING data TYPE any
1971
+ RETURNING VALUE(result) TYPE xstring.
1972
+
1973
+ CLASS-METHODS parse
1974
+ IMPORTING source TYPE any
1975
+ RETURNING VALUE(result) TYPE REF TO ${JSON_PARSER}.
1976
+ ENDCLASS.
1977
+ `;
1978
+ }
1979
+
1980
+ //#endregion
1981
+ //#region src/emit/templates/locals-imp.abap.ts
1982
+ function buildLocalsImp(names) {
1983
+ const { localJsonClass: JSON_CLS, localJsonParserClass: JSON_PARSER, localResponseClass: RESPONSE, localHttpClass: HTTP } = names;
1984
+ return `* Generated by @abapify/openai-codegen - local class implementations.
1985
+ * ZERO Z-dependencies: uses SAP kernel APIs only (Steampunk whitelist).
1986
+
1987
+ CLASS ${HTTP} IMPLEMENTATION.
1988
+
1989
+ METHOD constructor.
1990
+ me->destination = destination.
1991
+ me->server = server.
1992
+ ENDMETHOD.
1993
+
1994
+ METHOD build_query_string.
1995
+ DATA parts TYPE string_table.
1996
+ LOOP AT query ASSIGNING FIELD-SYMBOL(<q>).
1997
+ DATA(enc_n) = cl_web_http_utility=>escape_url( unescaped = <q>-name ).
1998
+ DATA(enc_v) = cl_web_http_utility=>escape_url( unescaped = <q>-value ).
1999
+ APPEND |{ enc_n }={ enc_v }| TO parts.
2000
+ ENDLOOP.
2001
+ result = concat_lines_of( table = parts sep = \`&\` ).
2002
+ ENDMETHOD.
2003
+
2004
+ METHOD fetch.
2005
+ " Prefix with the configured server base path when set,
2006
+ " so OpenAPI specs with a non-root server still resolve correctly.
2007
+ DATA full_path TYPE string.
2008
+ full_path = COND string(
2009
+ WHEN server IS INITIAL THEN path
2010
+ ELSE |{ server }{ path }| ).
2011
+ IF query IS NOT INITIAL.
2012
+ full_path = |{ full_path }?{ build_query_string( query ) }|.
2013
+ ENDIF.
2014
+
2015
+ DATA dest TYPE REF TO if_http_destination.
2016
+ dest = cl_http_destination_provider=>create_by_destination(
2017
+ i_destination = CONV #( destination ) ).
2018
+
2019
+ DATA client TYPE REF TO if_web_http_client.
2020
+ client = cl_web_http_client_manager=>create_by_http_destination(
2021
+ i_destination = dest ).
2022
+
2023
+ " All IO against client runs inside TRY ... CLEANUP so the HTTP
2024
+ " connection is closed even when execute() / binary read / codepage
2025
+ " conversion raise a kernel exception.
2026
+ TRY.
2027
+ DATA request TYPE REF TO if_web_http_request.
2028
+ request = client->get_http_request( ).
2029
+ request->set_uri_path( i_uri_path = full_path ).
2030
+
2031
+ DATA verb TYPE if_web_http_client=>method.
2032
+ verb = SWITCH if_web_http_client=>method( method
2033
+ WHEN 'GET' THEN if_web_http_client=>get
2034
+ WHEN 'POST' THEN if_web_http_client=>post
2035
+ WHEN 'PUT' THEN if_web_http_client=>put
2036
+ WHEN 'DELETE' THEN if_web_http_client=>delete
2037
+ WHEN 'PATCH' THEN if_web_http_client=>patch
2038
+ WHEN 'HEAD' THEN if_web_http_client=>head
2039
+ WHEN 'OPTIONS' THEN if_web_http_client=>options
2040
+ ELSE if_web_http_client=>get ).
2041
+
2042
+ LOOP AT headers ASSIGNING FIELD-SYMBOL(<h>).
2043
+ request->set_header_field( i_name = <h>-name
2044
+ i_value = <h>-value ).
2045
+ ENDLOOP.
2046
+
2047
+ IF binary IS NOT INITIAL.
2048
+ request->set_binary( i_data = binary ).
2049
+ ELSEIF body IS NOT INITIAL.
2050
+ request->set_text( i_text = body ).
2051
+ ENDIF.
2052
+
2053
+ DATA resp TYPE REF TO if_web_http_response.
2054
+ resp = client->execute( i_method = verb ).
2055
+
2056
+ DATA status_code TYPE i.
2057
+ status_code = resp->get_status( )-code.
2058
+
2059
+ DATA body_bytes TYPE xstring.
2060
+ body_bytes = resp->get_binary( ).
2061
+
2062
+ DATA body_text TYPE string.
2063
+ IF body_bytes IS NOT INITIAL.
2064
+ body_text = cl_abap_conv_codepage=>create_in( )->convert(
2065
+ source = body_bytes ).
2066
+ ENDIF.
2067
+
2068
+ DATA raw_headers TYPE if_web_http_request=>name_value_pairs.
2069
+ raw_headers = resp->get_header_fields( ).
2070
+
2071
+ DATA header_list TYPE kvs.
2072
+ LOOP AT raw_headers ASSIGNING FIELD-SYMBOL(<rh>).
2073
+ APPEND VALUE #( name = <rh>-name
2074
+ value = <rh>-value ) TO header_list.
2075
+ ENDLOOP.
2076
+
2077
+ response = NEW ${RESPONSE}(
2078
+ status_code = status_code
2079
+ body_bytes = body_bytes
2080
+ body_text = body_text
2081
+ header_list = header_list ).
2082
+
2083
+ client->close( ).
2084
+ CLEANUP.
2085
+ client->close( ).
2086
+ ENDTRY.
2087
+ ENDMETHOD.
2088
+
2089
+ ENDCLASS.
2090
+
2091
+ CLASS ${RESPONSE} IMPLEMENTATION.
2092
+
2093
+ METHOD constructor.
2094
+ me->status_code = status_code.
2095
+ me->body_bytes = body_bytes.
2096
+ me->body_text = body_text.
2097
+ me->header_list = header_list.
2098
+ ENDMETHOD.
2099
+
2100
+ METHOD status.
2101
+ result = status_code.
2102
+ ENDMETHOD.
2103
+
2104
+ METHOD body.
2105
+ result = body_bytes.
2106
+ ENDMETHOD.
2107
+
2108
+ METHOD text.
2109
+ result = body_text.
2110
+ ENDMETHOD.
2111
+
2112
+ METHOD headers.
2113
+ result = header_list.
2114
+ ENDMETHOD.
2115
+
2116
+ METHOD header.
2117
+ DATA needle TYPE string.
2118
+ needle = to_lower( name ).
2119
+ LOOP AT header_list ASSIGNING FIELD-SYMBOL(<h>).
2120
+ IF to_lower( <h>-name ) = needle.
2121
+ result = <h>-value.
2122
+ RETURN.
2123
+ ENDIF.
2124
+ ENDLOOP.
2125
+ ENDMETHOD.
2126
+
2127
+ ENDCLASS.
2128
+
2129
+ CLASS ${JSON_PARSER} IMPLEMENTATION.
2130
+
2131
+ METHOD constructor.
2132
+ me->json_text = json_text.
2133
+ ENDMETHOD.
2134
+
2135
+ METHOD to.
2136
+ FIELD-SYMBOLS <t> TYPE any.
2137
+ ASSIGN target->* TO <t>.
2138
+ /ui2/cl_json=>deserialize(
2139
+ EXPORTING json = json_text
2140
+ pretty_name = /ui2/cl_json=>pretty_mode-camel_case
2141
+ CHANGING data = <t> ).
2142
+ ENDMETHOD.
2143
+
2144
+ ENDCLASS.
2145
+
2146
+ CLASS ${JSON_CLS} IMPLEMENTATION.
2147
+
2148
+ METHOD stringify.
2149
+ result = /ui2/cl_json=>serialize(
2150
+ data = data
2151
+ pretty_name = /ui2/cl_json=>pretty_mode-camel_case
2152
+ compress = abap_true ).
2153
+ ENDMETHOD.
2154
+
2155
+ METHOD render.
2156
+ DATA text TYPE string.
2157
+ text = stringify( data ).
2158
+ result = cl_abap_conv_codepage=>create_out( )->convert(
2159
+ source = text ).
2160
+ ENDMETHOD.
2161
+
2162
+ METHOD parse.
2163
+ DATA text TYPE string.
2164
+ FIELD-SYMBOLS <s> TYPE any.
2165
+ ASSIGN source TO <s>.
2166
+ " RTTI dispatch — static type inspection is used instead of the
2167
+ " legacy DESCRIBE statement, which is not allowed on ABAP cloud.
2168
+ DATA type_info TYPE REF TO cl_abap_typedescr.
2169
+ type_info = cl_abap_typedescr=>describe_by_data( <s> ).
2170
+ IF type_info->type_kind = cl_abap_typedescr=>typekind_xstring.
2171
+ DATA bytes TYPE xstring.
2172
+ bytes = <s>.
2173
+ text = cl_abap_conv_codepage=>create_in( )->convert(
2174
+ source = bytes ).
2175
+ ELSE.
2176
+ text = <s>.
2177
+ ENDIF.
2178
+ result = NEW ${JSON_PARSER}( json_text = text ).
2179
+ ENDMETHOD.
2180
+
2181
+ ENDCLASS.
2182
+ `;
2183
+ }
2184
+
2185
+ //#endregion
2186
+ //#region src/emit/local-classes.ts
2187
+ function emitLocalClasses(names) {
2188
+ const templateNames = {
2189
+ exceptionClass: names.exceptionClass,
2190
+ localJsonClass: names.localJsonClass,
2191
+ localJsonParserClass: names.localJsonParserClass,
2192
+ localResponseClass: names.localResponseClass,
2193
+ localHttpClass: names.localHttpClass
2194
+ };
2195
+ return {
2196
+ localsDef: buildLocalsDef(templateNames),
2197
+ localsImp: buildLocalsImp(templateNames)
2198
+ };
2199
+ }
2200
+
2201
+ //#endregion
2202
+ //#region src/emit/response-mapper.ts
2203
+ /**
2204
+ * Response-mapper for openai-codegen v2.
2205
+ *
2206
+ * Produces the `CASE reply->status( ) ... ENDCASE.` block that lives inside
2207
+ * each generated implementation method, directly mapping the operation's
2208
+ * OpenAPI `responses` object onto ABAP branches.
2209
+ *
2210
+ * NOTE: `@abapify/abap-ast` does not currently model `CASE`/`WHEN` as a
2211
+ * dedicated statement node. To keep this change narrowly scoped (and to
2212
+ * avoid racing other agents on abap-ast), the emitted block is returned as
2213
+ * a single `raw({ source })` statement. The AST wrapper is only a transport
2214
+ * for the final ABAP source; callers append it to a method body exactly
2215
+ * like any other statement.
2216
+ */
2217
+ /**
2218
+ * Escape an arbitrary string so it can be embedded inside an ABAP single-
2219
+ * quoted literal. Single quotes are doubled; newlines collapse to spaces.
2220
+ */
2221
+ function escapeAbapLiteral(input) {
2222
+ return input.replaceAll(/[\r\n]+/g, " ").replaceAll("'", "''");
2223
+ }
2224
+ /**
2225
+ * Pick which response drives the success branch.
2226
+ *
2227
+ * Preference order:
2228
+ * 1. Lowest numeric 2xx status code.
2229
+ * 2. `default` if no 2xx exists (treated as success — WHEN OTHERS).
2230
+ * 3. `undefined` — caller falls back to synthesising a generic WHEN.
2231
+ */
2232
+ function pickSuccessStatus(responses) {
2233
+ const twoXX = responses.filter((r) => /^2\d\d$/.test(r.statusCode)).map((r) => r.statusCode).sort((a, b) => Number(a) - Number(b));
2234
+ if (twoXX.length > 0) return {
2235
+ statusCode: twoXX[0],
2236
+ whenLabel: twoXX[0]
2237
+ };
2238
+ const def = responses.find((r) => r.statusCode === "default");
2239
+ if (def !== void 0 && !def.isError) return {
2240
+ statusCode: "default",
2241
+ whenLabel: "OTHERS"
2242
+ };
2243
+ }
2244
+ function renderSuccessBody(opts) {
2245
+ const { successBody } = opts;
2246
+ const responseVar = opts.responseVarName ?? "response";
2247
+ switch (successBody.kind) {
2248
+ case "json": return `${opts.localJsonClassName}=>parse( ${responseVar}->body( ) )->to( REF #( ${successBody.returningName} ) ).`;
2249
+ case "empty": return `${successBody.returningName} = abap_true.`;
2250
+ case "binary": return `${successBody.returningName} = ${responseVar}->body( ).`;
2251
+ }
2252
+ }
2253
+ function renderErrorBranch(statusCode, description, opts) {
2254
+ const responseVar = opts.responseVarName ?? "response";
2255
+ const lines = [];
2256
+ lines.push(` WHEN ${statusCode}.`);
2257
+ lines.push(` RAISE EXCEPTION NEW ${opts.exceptionClassName}(`);
2258
+ lines.push(` status = ${statusCode}`);
2259
+ if (description !== void 0 && description.length > 0) lines.push(` description = '${escapeAbapLiteral(description)}'`);
2260
+ lines.push(` body = ${responseVar}->body( ) ).`);
2261
+ return lines;
2262
+ }
2263
+ function renderOthersBranch(description, opts) {
2264
+ const responseVar = opts.responseVarName ?? "response";
2265
+ const lines = [];
2266
+ lines.push(` WHEN OTHERS.`);
2267
+ lines.push(` RAISE EXCEPTION NEW ${opts.exceptionClassName}(`);
2268
+ lines.push(` status = ${responseVar}->status( )`);
2269
+ if (description !== void 0 && description.length > 0) lines.push(` description = '${escapeAbapLiteral(description)}'`);
2270
+ lines.push(` body = ${responseVar}->body( ) ).`);
2271
+ return lines;
2272
+ }
2273
+ function mapResponseHandling(operation, opts) {
2274
+ const responseVar = opts.responseVarName ?? "response";
2275
+ const lines = [];
2276
+ lines.push(`CASE ${responseVar}->status( ).`);
2277
+ const success = pickSuccessStatus(operation.responses);
2278
+ const successConsumedOthers = success !== void 0 && success.whenLabel === "OTHERS";
2279
+ if (success !== void 0 && !successConsumedOthers) {
2280
+ lines.push(` WHEN ${success.whenLabel}.`);
2281
+ lines.push(` ${renderSuccessBody(opts)}`);
2282
+ }
2283
+ for (const resp of operation.responses) {
2284
+ if (!resp.isError) continue;
2285
+ if (resp.statusCode === "default") continue;
2286
+ if (!/^\d+$/.test(resp.statusCode)) continue;
2287
+ lines.push(...renderErrorBranch(resp.statusCode, resp.description, opts));
2288
+ }
2289
+ if (successConsumedOthers) {
2290
+ lines.push(` WHEN OTHERS.`);
2291
+ lines.push(` ${renderSuccessBody(opts)}`);
2292
+ } else {
2293
+ const defaultResp = operation.responses.find((r) => r.statusCode === "default");
2294
+ const description = defaultResp !== void 0 && defaultResp.isError ? defaultResp.description : void 0;
2295
+ lines.push(...renderOthersBranch(description, opts));
2296
+ }
2297
+ lines.push(`ENDCASE.`);
2298
+ const source = lines.join("\n");
2299
+ return {
2300
+ statement: raw({ source }),
2301
+ source
2302
+ };
2303
+ }
2304
+
2305
+ //#endregion
2306
+ //#region src/format/validate.ts
2307
+ /**
2308
+ * ABAP identifier validation shared by all format writers.
2309
+ *
2310
+ * Rules (SAP repository object naming):
2311
+ * - Must start with 'Z' or 'Y' (customer namespace)
2312
+ * - Allowed characters: A-Z, 0-9, '_'
2313
+ * - Max 30 characters
2314
+ * - Must be uppercase
2315
+ */
2316
+ function assertValidClassName(name) {
2317
+ if (typeof name !== "string" || name.length === 0) throw new Error("className must be a non-empty string");
2318
+ if (name.length > 30) throw new Error(`Invalid ABAP class name '${name}': exceeds 30 characters`);
2319
+ if (!/^[ZY][A-Z0-9_]*$/.test(name)) throw new Error(`Invalid ABAP class name '${name}': must be uppercase, start with Z or Y, and contain only A-Z, 0-9, _`);
2320
+ }
2321
+
2322
+ //#endregion
2323
+ //#region src/format/abapgit.ts
2324
+ /**
2325
+ * abapGit format writer.
2326
+ *
2327
+ * Produces the canonical layout consumed by the abapGit import flow:
2328
+ *
2329
+ * <outDir>/
2330
+ * package.devc.xml
2331
+ * src/<name>.clas.abap
2332
+ * src/<name>.clas.xml
2333
+ * src/<name>.clas.testclasses.abap (optional)
2334
+ * src/<name>.clas.locals_def.abap (optional)
2335
+ *
2336
+ * File names are lowercase (abapGit convention). The `.clas.xml` envelope is
2337
+ * parameterised on CLSNAME / DESCRIPT / LANGU only — STATE/CLSCCINCL/FIXPT/
2338
+ * UNICODE are fixed to the defaults emitted by the SAP serializer for a
2339
+ * freshly-generated global class (mirrors samples/petstore-abapgit/).
2340
+ */
2341
+ const PACKAGE_DEVC_XML = `<?xml version="1.0" encoding="utf-8"?>
2342
+ <asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
2343
+ <asx:values>
2344
+ <DEVC>
2345
+ <CTEXT>Generated by @abapify/openai-codegen</CTEXT>
2346
+ </DEVC>
2347
+ </asx:values>
2348
+ </asx:abap>
2349
+ `;
2350
+ function buildClasXml(className, description, language) {
2351
+ return `<?xml version="1.0" encoding="utf-8"?>
2352
+ <abapGit version="v1.0.0" serializer="LCL_OBJECT_CLAS" serializer_version="v1.0.0">
2353
+ <asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
2354
+ <asx:values>
2355
+ <VSEOCLASS>
2356
+ <CLSNAME>${className}</CLSNAME>
2357
+ <LANGU>${language}</LANGU>
2358
+ <DESCRIPT>${escapeXml(description)}</DESCRIPT>
2359
+ <STATE>1</STATE>
2360
+ <CLSCCINCL>X</CLSCCINCL>
2361
+ <FIXPT>X</FIXPT>
2362
+ <UNICODE>X</UNICODE>
2363
+ <ABAP_LANGUAGE_VERSION>5</ABAP_LANGUAGE_VERSION>
2364
+ </VSEOCLASS>
2365
+ </asx:values>
2366
+ </asx:abap>
2367
+ </abapGit>
2368
+ `;
2369
+ }
2370
+ function escapeXml(value) {
2371
+ return value.replaceAll(/&/g, "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
2372
+ }
2373
+ async function writeAbapgitLayout(artifact, outDir) {
2374
+ assertValidClassName(artifact.className);
2375
+ const language = artifact.language ?? "E";
2376
+ const description = artifact.description ?? artifact.className;
2377
+ const base = artifact.className.toLowerCase();
2378
+ const srcDir = join(outDir, "src");
2379
+ await mkdir(srcDir, { recursive: true });
2380
+ const written = [];
2381
+ const pkgPath = join(outDir, "package.devc.xml");
2382
+ await writeFile(pkgPath, PACKAGE_DEVC_XML, "utf-8");
2383
+ written.push("package.devc.xml");
2384
+ const mainAbap = join(srcDir, `${base}.clas.abap`);
2385
+ await writeFile(mainAbap, artifact.mainSource, "utf-8");
2386
+ written.push(`src/${base}.clas.abap`);
2387
+ const clasXml = join(srcDir, `${base}.clas.xml`);
2388
+ await writeFile(clasXml, buildClasXml(artifact.className, description, language), "utf-8");
2389
+ written.push(`src/${base}.clas.xml`);
2390
+ if (artifact.testSource !== void 0) {
2391
+ const p = join(srcDir, `${base}.clas.testclasses.abap`);
2392
+ await writeFile(p, artifact.testSource, "utf-8");
2393
+ written.push(`src/${base}.clas.testclasses.abap`);
2394
+ }
2395
+ if (artifact.localsDefSource !== void 0) {
2396
+ const p = join(srcDir, `${base}.clas.locals_def.abap`);
2397
+ await writeFile(p, artifact.localsDefSource, "utf-8");
2398
+ written.push(`src/${base}.clas.locals_def.abap`);
2399
+ }
2400
+ if (artifact.localsImpSource !== void 0) {
2401
+ const p = join(srcDir, `${base}.clas.locals_imp.abap`);
2402
+ await writeFile(p, artifact.localsImpSource, "utf-8");
2403
+ written.push(`src/${base}.clas.locals_imp.abap`);
2404
+ }
2405
+ written.sort((a, b) => a.localeCompare(b));
2406
+ return { files: written };
2407
+ }
2408
+ function buildIntfXml(name, description, language) {
2409
+ return `<?xml version="1.0" encoding="utf-8"?>
2410
+ <abapGit version="v1.0.0" serializer="LCL_OBJECT_INTF" serializer_version="v1.0.0">
2411
+ <asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
2412
+ <asx:values>
2413
+ <VSEOINTERF>
2414
+ <CLSNAME>${name}</CLSNAME>
2415
+ <LANGU>${language}</LANGU>
2416
+ <DESCRIPT>${escapeXml(description)}</DESCRIPT>
2417
+ <EXPOSURE>2</EXPOSURE>
2418
+ <STATE>1</STATE>
2419
+ <UNICODE>X</UNICODE>
2420
+ <ABAP_LANGUAGE_VERSION>5</ABAP_LANGUAGE_VERSION>
2421
+ </VSEOINTERF>
2422
+ </asx:values>
2423
+ </asx:abap>
2424
+ </abapGit>
2425
+ `;
2426
+ }
2427
+ async function writeAbapgitInterface(artifact, outDir) {
2428
+ assertValidClassName(artifact.name);
2429
+ const language = artifact.language ?? "E";
2430
+ const description = artifact.description ?? artifact.name;
2431
+ const base = artifact.name.toLowerCase();
2432
+ const srcDir = join(outDir, "src");
2433
+ await mkdir(srcDir, { recursive: true });
2434
+ const written = [];
2435
+ const pkgPath = join(outDir, "package.devc.xml");
2436
+ await writeFile(pkgPath, PACKAGE_DEVC_XML, "utf-8");
2437
+ written.push("package.devc.xml");
2438
+ const intfAbap = join(srcDir, `${base}.intf.abap`);
2439
+ await writeFile(intfAbap, artifact.source, "utf-8");
2440
+ written.push(`src/${base}.intf.abap`);
2441
+ const intfXml = join(srcDir, `${base}.intf.xml`);
2442
+ await writeFile(intfXml, buildIntfXml(artifact.name, description, language), "utf-8");
2443
+ written.push(`src/${base}.intf.xml`);
2444
+ written.sort((a, b) => a.localeCompare(b));
2445
+ return { files: written };
2446
+ }
2447
+
2448
+ //#endregion
2449
+ //#region src/format/gcts.ts
2450
+ /**
2451
+ * gCTS / AFF format writer.
2452
+ *
2453
+ * gCTS (git-enabled CTS) and AFF (SAP/abap-file-formats) share the same
2454
+ * on-disk layout: JSON metadata, per-object subdirectory named after the
2455
+ * lowercased object name, `<name>.<type>.<ext>` naming.
2456
+ *
2457
+ * For a class this produces:
2458
+ *
2459
+ * <outDir>/
2460
+ * package.devc.json
2461
+ * CLAS/<name>/<name>.clas.json
2462
+ * CLAS/<name>/<name>.clas.abap
2463
+ * CLAS/<name>/<name>.clas.testclasses.abap (optional)
2464
+ * CLAS/<name>/<name>.clas.locals_def.abap (optional)
2465
+ * CLAS/<name>/<name>.clas.locals_imp.abap (optional)
2466
+ *
2467
+ * The `CLAS/` object-type subdirectory mirrors the layout used by gCTS
2468
+ * repositories (objects grouped under their type code). See
2469
+ * `packages/adt-plugin-gcts/` for filename conventions.
2470
+ */
2471
+ const PACKAGE_DEVC_JSON = JSON.stringify({
2472
+ header: {
2473
+ formatVersion: "1.0",
2474
+ description: "Generated by @abapify/openai-codegen"
2475
+ },
2476
+ package: { softwareComponent: "LOCAL" }
2477
+ }, null, 2) + "\n";
2478
+ function buildClasJson(className, description, language) {
2479
+ return JSON.stringify({
2480
+ header: {
2481
+ formatVersion: "1.0",
2482
+ description,
2483
+ originalLanguage: language,
2484
+ abapLanguageVersion: "5"
2485
+ },
2486
+ class: {
2487
+ name: className,
2488
+ category: "00",
2489
+ visibility: "public",
2490
+ final: true,
2491
+ abstract: false,
2492
+ fixedPointArithmetic: true,
2493
+ unicodeChecksActive: true
2494
+ }
2495
+ }, null, 2) + "\n";
2496
+ }
2497
+ async function writeGctsLayout(artifact, outDir) {
2498
+ assertValidClassName(artifact.className);
2499
+ const language = artifact.language ?? "E";
2500
+ const description = artifact.description ?? artifact.className;
2501
+ const base = artifact.className.toLowerCase();
2502
+ const objectDir = join(outDir, "CLAS", base);
2503
+ await mkdir(objectDir, { recursive: true });
2504
+ const written = [];
2505
+ const pkgPath = join(outDir, "package.devc.json");
2506
+ await writeFile(pkgPath, PACKAGE_DEVC_JSON, "utf-8");
2507
+ written.push("package.devc.json");
2508
+ const metaPath = join(objectDir, `${base}.clas.json`);
2509
+ await writeFile(metaPath, buildClasJson(artifact.className, description, language), "utf-8");
2510
+ written.push(`CLAS/${base}/${base}.clas.json`);
2511
+ const mainPath = join(objectDir, `${base}.clas.abap`);
2512
+ await writeFile(mainPath, artifact.mainSource, "utf-8");
2513
+ written.push(`CLAS/${base}/${base}.clas.abap`);
2514
+ if (artifact.testSource !== void 0) {
2515
+ const p = join(objectDir, `${base}.clas.testclasses.abap`);
2516
+ await writeFile(p, artifact.testSource, "utf-8");
2517
+ written.push(`CLAS/${base}/${base}.clas.testclasses.abap`);
2518
+ }
2519
+ if (artifact.localsDefSource !== void 0) {
2520
+ const p = join(objectDir, `${base}.clas.locals_def.abap`);
2521
+ await writeFile(p, artifact.localsDefSource, "utf-8");
2522
+ written.push(`CLAS/${base}/${base}.clas.locals_def.abap`);
2523
+ }
2524
+ if (artifact.localsImpSource !== void 0) {
2525
+ const p = join(objectDir, `${base}.clas.locals_imp.abap`);
2526
+ await writeFile(p, artifact.localsImpSource, "utf-8");
2527
+ written.push(`CLAS/${base}/${base}.clas.locals_imp.abap`);
2528
+ }
2529
+ written.sort((a, b) => a.localeCompare(b));
2530
+ return { files: written };
2531
+ }
2532
+ function buildIntfJson(name, description, language) {
2533
+ return JSON.stringify({
2534
+ header: {
2535
+ formatVersion: "1.0",
2536
+ description,
2537
+ originalLanguage: language,
2538
+ abapLanguageVersion: "5"
2539
+ },
2540
+ interface: {
2541
+ name,
2542
+ visibility: "public"
2543
+ }
2544
+ }, null, 2) + "\n";
2545
+ }
2546
+ async function writeGctsInterface(artifact, outDir) {
2547
+ assertValidClassName(artifact.name);
2548
+ const language = artifact.language ?? "E";
2549
+ const description = artifact.description ?? artifact.name;
2550
+ const base = artifact.name.toLowerCase();
2551
+ const objectDir = join(outDir, "INTF", base);
2552
+ await mkdir(objectDir, { recursive: true });
2553
+ const written = [];
2554
+ const pkgPath = join(outDir, "package.devc.json");
2555
+ await writeFile(pkgPath, PACKAGE_DEVC_JSON, "utf-8");
2556
+ written.push("package.devc.json");
2557
+ const metaPath = join(objectDir, `${base}.intf.json`);
2558
+ await writeFile(metaPath, buildIntfJson(artifact.name, description, language), "utf-8");
2559
+ written.push(`INTF/${base}/${base}.intf.json`);
2560
+ const mainPath = join(objectDir, `${base}.intf.abap`);
2561
+ await writeFile(mainPath, artifact.source, "utf-8");
2562
+ written.push(`INTF/${base}/${base}.intf.abap`);
2563
+ written.sort((a, b) => a.localeCompare(b));
2564
+ return { files: written };
2565
+ }
2566
+
2567
+ //#endregion
2568
+ //#region src/format/index.ts
2569
+ /**
2570
+ * Format plugins barrel.
2571
+ *
2572
+ * Exposes per-format writers and a {@link writeLayout} dispatch helper that
2573
+ * selects the writer from an {@link OutputFormat} string. A
2574
+ * {@link writeClientBundle} helper writes the full four-artifact client
2575
+ * (types interface, operations interface, exception class, implementation
2576
+ * class) in one shot.
2577
+ */
2578
+ function isInterfaceArtifact(artifact) {
2579
+ return typeof artifact.name === "string" && typeof artifact.source === "string";
2580
+ }
2581
+ function writeLayout(artifact, format, outDir) {
2582
+ if (isInterfaceArtifact(artifact)) switch (format) {
2583
+ case "abapgit": return writeAbapgitInterface(artifact, outDir);
2584
+ case "gcts": return writeGctsInterface(artifact, outDir);
2585
+ default: throw new Error(`Unknown output format: ${String(format)}`);
2586
+ }
2587
+ switch (format) {
2588
+ case "abapgit": return writeAbapgitLayout(artifact, outDir);
2589
+ case "gcts": return writeGctsLayout(artifact, outDir);
2590
+ default: throw new Error(`Unknown output format: ${String(format)}`);
2591
+ }
2592
+ }
2593
+ async function writeClientBundle(artifacts, format, outDir) {
2594
+ const all = /* @__PURE__ */ new Set();
2595
+ for (const artifact of [
2596
+ artifacts.types,
2597
+ artifacts.operations,
2598
+ artifacts.exception,
2599
+ artifacts.implementation
2600
+ ]) {
2601
+ const r = await writeLayout(artifact, format, outDir);
2602
+ for (const f of r.files) all.add(f);
2603
+ }
2604
+ return { files: [...all].sort((a, b) => a.localeCompare(b)) };
2605
+ }
2606
+
2607
+ //#endregion
2608
+ //#region src/emit/implementation-class.ts
2609
+ /**
2610
+ * Layer 3 emitter — IMPLEMENTATION CLASS (`ZCL_<base>`).
2611
+ *
2612
+ * The generated class is deliberately minimal: it INTERFACES the operations
2613
+ * interface, holds one private reference to the bundled local HTTP client,
2614
+ * and emits one METHOD per operation whose body:
2615
+ *
2616
+ * 1. Calls `client->fetch( ... )` with method / path / query / headers /
2617
+ * body, capturing the result in `DATA(response)`.
2618
+ * 2. Delegates to the response-mapper to emit the `CASE response->status( )`
2619
+ * block that turns each OpenAPI response onto either a RETURNING
2620
+ * assignment or a `RAISE EXCEPTION NEW zcx_...` statement.
2621
+ *
2622
+ * The @abapify/abap-ast printer does not currently model `CASE` / `WHEN`
2623
+ * nodes, and modelling a mixed-indent multi-line method call via the call
2624
+ * expression is awkward. For these two shapes we use `raw({ source })`
2625
+ * statements — the printer emits them verbatim at the current indent.
2626
+ */
2627
+ function emitImplementationClass(spec, operations, opts) {
2628
+ const { names } = opts;
2629
+ const className = names.implementationClass;
2630
+ const ifaceName = names.operationsInterface.toLowerCase();
2631
+ const httpLocal = names.localHttpClass;
2632
+ const jsonLocal = names.localJsonClass;
2633
+ const excLocal = names.exceptionClass.toLowerCase();
2634
+ const defaultServer = opts.defaultServer ?? "/api/v3";
2635
+ const ctorDecl = methodDef({
2636
+ name: "constructor",
2637
+ visibility: "public",
2638
+ params: [methodParam({
2639
+ paramKind: "importing",
2640
+ name: "destination",
2641
+ typeRef: builtinType({ name: "string" })
2642
+ }), methodParam({
2643
+ paramKind: "importing",
2644
+ name: "server",
2645
+ typeRef: builtinType({ name: "string" }),
2646
+ default: literal({
2647
+ literalKind: "string",
2648
+ value: defaultServer
2649
+ })
2650
+ })]
2651
+ });
2652
+ const publicSection = section({
2653
+ visibility: "public",
2654
+ members: [ctorDecl]
2655
+ });
2656
+ const clientAttr = attributeDef({
2657
+ name: "client",
2658
+ type: namedTypeRef({ name: `REF TO ${httpLocal}` }),
2659
+ visibility: "private"
2660
+ });
2661
+ const privateSection = section({
2662
+ visibility: "private",
2663
+ members: [clientAttr]
2664
+ });
2665
+ const ctorImpl = methodImpl({
2666
+ name: "constructor",
2667
+ body: [raw({ source: `client = NEW ${httpLocal}( destination = destination server = server ).` })]
2668
+ });
2669
+ const byId = /* @__PURE__ */ new Map();
2670
+ for (const op of spec.operations) byId.set(op.operationId, op);
2671
+ const operationImpls = operations.map((mapping) => {
2672
+ const op = byId.get(mapping.operationId);
2673
+ if (op === void 0) throw new Error(`emitImplementationClass: operation "${mapping.operationId}" not found in spec`);
2674
+ const bodyStmts = buildOperationBody(op, mapping, {
2675
+ httpLocal,
2676
+ jsonLocal,
2677
+ exceptionClassName: excLocal
2678
+ });
2679
+ return methodImpl({
2680
+ name: `${ifaceName}~${mapping.methodName}`,
2681
+ body: bodyStmts
2682
+ });
2683
+ });
2684
+ return { class: classDef({
2685
+ name: className,
2686
+ interfaces: [ifaceName],
2687
+ sections: [publicSection, privateSection],
2688
+ implementations: [ctorImpl, ...operationImpls]
2689
+ }) };
2690
+ }
2691
+ function buildOperationBody(op, mapping, ctx) {
2692
+ const fetchSource = renderFetchCall(mapping, isBinaryRequestBody(op, pickRequestBodySchema(op)), ctx.jsonLocal);
2693
+ const successBody = decideSuccessBody(op, mapping);
2694
+ const caseResult = mapResponseHandling(op, {
2695
+ exceptionClassName: ctx.exceptionClassName,
2696
+ localJsonClassName: ctx.jsonLocal,
2697
+ successBody
2698
+ });
2699
+ const excName = ctx.exceptionClassName;
2700
+ return [
2701
+ raw({ source: "TRY." }),
2702
+ raw({ source: fetchSource }),
2703
+ caseResult.statement,
2704
+ raw({ source: ` CATCH cx_web_http_client_error cx_http_dest_provider_error INTO DATA(_http_err).` }),
2705
+ raw({ source: ` RAISE EXCEPTION NEW ${excName}(\n status = 0\n description = _http_err->get_text( ) ).` }),
2706
+ raw({ source: "ENDTRY." })
2707
+ ];
2708
+ }
2709
+ function renderFetchCall(mapping, bodyIsBinary, jsonLocalClass) {
2710
+ const args = [];
2711
+ args.push({
2712
+ key: "method",
2713
+ value: `'${mapping.method}'`
2714
+ });
2715
+ args.push({
2716
+ key: "path",
2717
+ value: renderPathValue(mapping)
2718
+ });
2719
+ const queryParams = mapping.params.filter((p) => p.location === "query");
2720
+ if (queryParams.length > 0) {
2721
+ const items = queryParams.map((p) => {
2722
+ const key = escapeSingleQuotes(p.openapiName);
2723
+ if (p.isTable) {
2724
+ const loopVar = `_${p.name}`;
2725
+ return `FOR ${loopVar} IN ${p.name} ( name = '${key}' value = ${loopVar} )`;
2726
+ }
2727
+ return `( name = '${key}' value = ${p.name} )`;
2728
+ }).join(" ");
2729
+ args.push({
2730
+ key: "query",
2731
+ value: `VALUE #( ${items} )`
2732
+ });
2733
+ }
2734
+ const headerParams = mapping.params.filter((p) => p.location === "header");
2735
+ const headerItems = [];
2736
+ if (mapping.hasBody) {
2737
+ if (bodyIsBinary) args.push({
2738
+ key: "binary",
2739
+ value: "body"
2740
+ });
2741
+ else {
2742
+ args.push({
2743
+ key: "body",
2744
+ value: `${jsonLocalClass}=>stringify( body )`
2745
+ });
2746
+ headerItems.push(`( name = 'Content-Type' value = 'application/json' )`);
2747
+ }
2748
+ }
2749
+ for (const h of headerParams) headerItems.push(`( name = '${escapeSingleQuotes(h.openapiName)}' value = ${h.name} )`);
2750
+ if (headerItems.length > 0) args.push({
2751
+ key: "headers",
2752
+ value: `VALUE #( ${headerItems.join(" ")} )`
2753
+ });
2754
+ const head = "DATA(response) = client->fetch(";
2755
+ if (args.length <= 2) return `${head} ${args.map((a) => `${a.key} = ${a.value}`).join(" ")} ).`;
2756
+ const maxKey = Math.max(...args.map((a) => a.key.length));
2757
+ const lines = [head];
2758
+ args.forEach((a, idx) => {
2759
+ const pad = " ".repeat(maxKey - a.key.length);
2760
+ const tail = idx === args.length - 1 ? " )." : "";
2761
+ lines.push(` ${a.key}${pad} = ${a.value}${tail}`);
2762
+ });
2763
+ return lines.join("\n");
2764
+ }
2765
+ /** `'<path>'` when no path params, otherwise a `|...|` string template. */
2766
+ function renderPathValue(mapping) {
2767
+ const pathParams = mapping.params.filter((p) => p.location === "path");
2768
+ if (pathParams.length === 0) return `'${escapeSingleQuotes(mapping.path)}'`;
2769
+ const byOriginal = /* @__PURE__ */ new Map();
2770
+ for (const p of pathParams) byOriginal.set(p.openapiName, p.name);
2771
+ let out = "|";
2772
+ let i = 0;
2773
+ const src = mapping.path;
2774
+ while (i < src.length) {
2775
+ const open = src.indexOf("{", i);
2776
+ if (open === -1) {
2777
+ out += src.slice(i);
2778
+ break;
2779
+ }
2780
+ out += src.slice(i, open);
2781
+ const close = src.indexOf("}", open);
2782
+ if (close === -1) {
2783
+ out += src.slice(open);
2784
+ break;
2785
+ }
2786
+ const placeholder = src.slice(open + 1, close);
2787
+ const abapName = byOriginal.get(placeholder) ?? placeholder;
2788
+ out += `{ ${abapName} }`;
2789
+ i = close + 1;
2790
+ }
2791
+ out += "|";
2792
+ return out;
2793
+ }
2794
+ function decideSuccessBody(op, mapping) {
2795
+ if (mapping.successIsEmptyBody) return {
2796
+ kind: "empty",
2797
+ returningName: mapping.returningName
2798
+ };
2799
+ const picked = pickSuccessResponse(op);
2800
+ const schema = picked ? pickPreferredSchema(picked.content) : void 0;
2801
+ if (schema !== void 0 && isBinarySchema(schema)) return {
2802
+ kind: "binary",
2803
+ returningName: mapping.returningName
2804
+ };
2805
+ return {
2806
+ kind: "json",
2807
+ returningName: mapping.returningName
2808
+ };
2809
+ }
2810
+ function pickSuccessResponse(op) {
2811
+ const twoXX = op.responses.filter((r) => /^2\d\d$/.test(r.statusCode)).sort((a, b) => Number(a.statusCode) - Number(b.statusCode));
2812
+ if (twoXX.length > 0) return twoXX[0];
2813
+ return op.responses.find((r) => r.statusCode === "default");
2814
+ }
2815
+ function pickRequestBodySchema(op) {
2816
+ const rb = op.requestBody;
2817
+ if (!rb) return void 0;
2818
+ return pickPreferredSchema(rb.content);
2819
+ }
2820
+ function pickPreferredSchema(content) {
2821
+ if (!content) return void 0;
2822
+ const json = content["application/json"];
2823
+ if (json !== void 0) return nonEmpty(json.schema);
2824
+ const octet = content["application/octet-stream"];
2825
+ if (octet !== void 0) return nonEmpty(octet.schema);
2826
+ const first = Object.keys(content)[0];
2827
+ if (first !== void 0) return nonEmpty(content[first].schema);
2828
+ }
2829
+ function nonEmpty(schema) {
2830
+ if (schema === void 0) return void 0;
2831
+ return Object.keys(schema).length > 0 ? schema : void 0;
2832
+ }
2833
+ function isBinarySchema(schema) {
2834
+ if (!schema) return false;
2835
+ if (isRef(schema)) return false;
2836
+ const t = schema.type;
2837
+ const f = schema.format;
2838
+ return t === "string" && (f === "binary" || f === "byte");
2839
+ }
2840
+ function isBinaryRequestBody(op, schema) {
2841
+ if (isBinarySchema(schema)) return true;
2842
+ const content = op.requestBody?.content;
2843
+ if (!content) return false;
2844
+ const keys = Object.keys(content);
2845
+ if (keys.length === 1 && keys[0] === "application/octet-stream") return true;
2846
+ return false;
2847
+ }
2848
+ function escapeSingleQuotes(s) {
2849
+ return s.replaceAll("'", "''");
2850
+ }
2851
+
2852
+ //#endregion
2853
+ //#region src/generate.ts
2854
+ /**
2855
+ * End-to-end pipeline for the v2 openai-codegen emitter.
2856
+ *
2857
+ * Loads an OpenAPI spec, plans types, runs the Layer-1 / Layer-2 / exception
2858
+ * / implementation / local-class emitters, prints each ABAP node through
2859
+ * `@abapify/abap-ast`, and hands the four artifacts to the configured format
2860
+ * writer (`abapgit` or `gcts`).
2861
+ */
2862
+ function assertSupportedTarget(target) {
2863
+ if (target !== "s4-cloud") throw new Error(`generate: target "${target}" is not supported in v1 (only 's4-cloud').`);
2864
+ }
2865
+ function deriveDefaultServer(spec) {
2866
+ const first = spec.servers[0]?.url;
2867
+ if (!first || first.length === 0) return "/";
2868
+ try {
2869
+ const u = new URL(first);
2870
+ return u.pathname && u.pathname.length > 0 ? u.pathname : "/";
2871
+ } catch {
2872
+ return first;
2873
+ }
2874
+ }
2875
+ async function generate(options) {
2876
+ const target = options.target ?? "s4-cloud";
2877
+ let step = "validate-target";
2878
+ try {
2879
+ assertSupportedTarget(target);
2880
+ step = "resolve-names";
2881
+ const names = resolveNames(options.names);
2882
+ step = "load-spec";
2883
+ const spec = await loadSpec(options.input);
2884
+ step = "plan-types";
2885
+ const plan = planTypes(spec, { typePrefix: "" });
2886
+ step = "emit-types-interface";
2887
+ const typesIF = emitTypesInterface(spec, plan, {
2888
+ name: names.typesInterface,
2889
+ emitNullFlags: options.emitNullFlags ?? false,
2890
+ interfaceAbapDoc: [`Generated types for ${names.implementationClass}.`]
2891
+ });
2892
+ step = "emit-operations-interface";
2893
+ const opsIF = emitOperationsInterface(spec, plan, {
2894
+ name: names.operationsInterface,
2895
+ typesInterfaceName: names.typesInterface,
2896
+ exceptionClassName: names.exceptionClass
2897
+ });
2898
+ step = "emit-exception-class";
2899
+ const zcx = emitExceptionClass({ name: names.exceptionClass });
2900
+ step = "emit-implementation-class";
2901
+ const implResult = emitImplementationClass(spec, opsIF.operations, {
2902
+ names,
2903
+ defaultServer: options.defaultServer ?? deriveDefaultServer(spec)
2904
+ });
2905
+ step = "emit-local-classes";
2906
+ const locals = emitLocalClasses(names);
2907
+ step = "print";
2908
+ const typesSource = print(typesIF.interface);
2909
+ const opsSource = print(opsIF.interface);
2910
+ const zcxSource = print(zcx.class);
2911
+ const implSource = print(implResult.class);
2912
+ step = "assemble-artifacts";
2913
+ const types = {
2914
+ name: names.typesInterface,
2915
+ source: typesSource,
2916
+ description: options.description ?? `Types for ${names.implementationClass}`
2917
+ };
2918
+ const operationsArtifact = {
2919
+ name: names.operationsInterface,
2920
+ source: opsSource,
2921
+ description: options.description ?? `Operations for ${names.implementationClass}`
2922
+ };
2923
+ const exception = {
2924
+ className: names.exceptionClass,
2925
+ mainSource: zcxSource,
2926
+ description: options.description ?? `Error exception for ${names.implementationClass}`
2927
+ };
2928
+ const implementation = {
2929
+ className: names.implementationClass,
2930
+ mainSource: implSource,
2931
+ description: options.description ?? names.implementationClass,
2932
+ localsDefSource: locals.localsDef,
2933
+ localsImpSource: locals.localsImp
2934
+ };
2935
+ step = "write";
2936
+ return {
2937
+ files: (await writeClientBundle({
2938
+ types,
2939
+ operations: operationsArtifact,
2940
+ exception,
2941
+ implementation
2942
+ }, options.format, options.outDir)).files,
2943
+ resolvedNames: names,
2944
+ typeCount: plan.entries.length,
2945
+ operationCount: opsIF.operations.length
2946
+ };
2947
+ } catch (err) {
2948
+ const message = err instanceof Error ? err.message : String(err);
2949
+ const wrapped = /* @__PURE__ */ new Error(`openai-codegen generate failed at step "${step}": ${message}`);
2950
+ if (err instanceof Error && err.stack) wrapped.stack = err.stack;
2951
+ throw wrapped;
2952
+ }
2953
+ }
2954
+
2955
+ //#endregion
2956
+ export { makeNameAllocator as C, isRef as D, normalizeSpec as E, UnsupportedSchemaError as S, loadSpec as T, mapSchemaToTypeDef as _, writeGctsLayout as a, CollisionError as b, mapResponseHandling as c, emitOperationsInterface as d, PolymorphismConflictError as f, mapPrimitive as g, resolveNames as h, writeGctsInterface as i, emitLocalClasses as l, NamesConfigError as m, writeClientBundle as n, writeAbapgitInterface as o, emitTypesInterface as p, writeLayout as r, writeAbapgitLayout as s, generate as t, emitExceptionClass as u, mapSchemaToTypeRef as v, sanitizeIdent as w, CyclicTypeError as x, planTypes as y };
2957
+ //# sourceMappingURL=generate-BobARM00.mjs.map