@abapify/openai-codegen 0.3.5 → 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.
- package/dist/cli.d.mts +44 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +116 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/generate-BobARM00.mjs +2957 -0
- package/dist/generate-BobARM00.mjs.map +1 -0
- package/dist/index.d.mts +496 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +224 -0
- package/dist/index.mjs.map +1 -0
- package/dist/naming-9hJCZKjv.d.mts +47 -0
- package/dist/naming-9hJCZKjv.d.mts.map +1 -0
- package/package.json +4 -3
|
@@ -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, "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
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
|