@redocly/cli 2.47.0 → 2.49.0
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/lib/chunks/{QRD3XQNU.js → 2U54VUOE.js} +1 -1
- package/lib/chunks/3JJEI7CJ.js +456 -0
- package/lib/chunks/4I2OVMBJ.js +11 -0
- package/lib/chunks/{REBHK2JR.js → 4JDAOKRJ.js} +1 -1
- package/lib/chunks/5JIGUBC3.js +7 -0
- package/lib/chunks/{QVV2EKMT.js → 5SAFIITS.js} +1 -1
- package/lib/chunks/6KCIFTIG.js +57 -0
- package/lib/chunks/{5NR4DJW4.js → 6VSHPQXO.js} +1 -1
- package/lib/chunks/{W2NJCZOS.js → 74WJ7S7J.js} +1 -1
- package/lib/chunks/{QL4V2A4A.js → 7R5PBTLG.js} +1 -1
- package/lib/chunks/BT3ELZVX.js +42 -0
- package/lib/chunks/C5TR4MMN.js +19 -0
- package/lib/chunks/CCR5RYCX.js +20 -0
- package/lib/chunks/DCN3GSUA.js +7 -0
- package/lib/chunks/{TEQA5OM7.js → E5I67ZCZ.js} +1 -1
- package/lib/chunks/FUMCRNTK.js +78 -0
- package/lib/chunks/GNMJA2V2.js +16 -0
- package/lib/chunks/GRJG3TOI.js +14 -0
- package/lib/chunks/HA22GUKQ.js +7 -0
- package/lib/chunks/HJ36YGCI.js +184 -0
- package/lib/chunks/HJYFWIVE.js +9 -0
- package/lib/chunks/HL5FOKC4.js +16 -0
- package/lib/chunks/{SL6AH3N2.js → JBNPGEIJ.js} +1 -1
- package/lib/chunks/KHEMMNFU.js +26 -0
- package/lib/chunks/{SN2EC2ZR.js → KK2STV73.js} +1 -1
- package/lib/chunks/KK77ANZH.js +7 -0
- package/lib/chunks/L3TZNNJJ.js +13 -0
- package/lib/chunks/M6KLAAY4.js +7 -0
- package/lib/chunks/MHITBEGC.js +90 -0
- package/lib/chunks/NB54XRRU.js +10 -0
- package/lib/chunks/NSIQWNYL.js +7 -0
- package/lib/chunks/{YQW463Z7.js → NTPLYWAB.js} +1 -1
- package/lib/chunks/{5PXRDPHV.js → NUNOXB7Z.js} +1 -1
- package/lib/chunks/NWMTC4AN.js +7 -0
- package/lib/chunks/NYLAOZPO.js +22 -0
- package/lib/chunks/O4R6BSAS.js +20 -0
- package/lib/chunks/OKITK6EF.js +30 -0
- package/lib/chunks/OUCZOBEX.js +22 -0
- package/lib/chunks/PFYPTG4G.js +7 -0
- package/lib/chunks/QZH7EL26.js +7 -0
- package/lib/chunks/R4XFBOJU.js +9 -0
- package/lib/chunks/REFX2EKS.js +13 -0
- package/lib/chunks/{S4MX7JPH.js → RXOEH2LN.js} +1 -1
- package/lib/chunks/SGBB4RNM.js +11 -0
- package/lib/chunks/SQVG3CEM.js +8 -0
- package/lib/chunks/{RIFZSMUA.js → T5EPIBMA.js} +1 -1
- package/lib/chunks/TLQZAJQU.js +18 -0
- package/lib/chunks/TSB6PODJ.js +13 -0
- package/lib/chunks/TSRK76GA.js +19 -0
- package/lib/chunks/{34J2ZJDK.js → U2H2LXZE.js} +1 -1
- package/lib/chunks/UKSQ7B5H.js +2268 -0
- package/lib/chunks/UMQN5HTL.js +42 -0
- package/lib/chunks/V2LJ4UGM.js +7 -0
- package/lib/chunks/VBPP2WUF.js +7 -0
- package/lib/chunks/WPGKH5NI.js +177 -0
- package/lib/chunks/WXVC3KOI.js +67 -0
- package/lib/chunks/{KEUH3PST.js → YI34NB7D.js} +1 -1
- package/lib/chunks/YOJGIGE3.js +7 -0
- package/lib/chunks/{TO2KJ36N.js → YP5GHFYE.js} +1 -1
- package/lib/chunks/YXXYE4G2.js +7 -0
- package/lib/chunks/ZBL4IVEW.js +11 -0
- package/lib/chunks/ZMCRMR2V.js +5400 -0
- package/lib/chunks/ZOUQBK53.js +14 -0
- package/lib/chunks/ZXKOHMC6.js +177 -0
- package/lib/eject-assets/AGENTS.md +137 -0
- package/lib/eject-assets/generators/cli/docs.ts +224 -0
- package/lib/eject-assets/generators/cli/engine-source.ts +21 -0
- package/lib/eject-assets/generators/cli/index.ts +90 -0
- package/lib/eject-assets/generators/cli/render.ts +348 -0
- package/lib/eject-assets/generators/go/client.ts +57 -0
- package/lib/eject-assets/generators/go/descriptor.ts +41 -0
- package/lib/eject-assets/generators/go/index.ts +336 -0
- package/lib/eject-assets/generators/go/models.ts +179 -0
- package/lib/eject-assets/generators/go/naming.ts +49 -0
- package/lib/eject-assets/generators/go/operations.ts +296 -0
- package/lib/eject-assets/generators/go/pagination.ts +194 -0
- package/lib/eject-assets/generators/go/types.ts +60 -0
- package/lib/eject-assets/generators/mock/faker.ts +214 -0
- package/lib/eject-assets/generators/mock/index.ts +35 -0
- package/lib/eject-assets/generators/mock/render.ts +282 -0
- package/lib/eject-assets/generators/mock/sample.ts +316 -0
- package/lib/eject-assets/generators/mock/values.ts +63 -0
- package/lib/eject-assets/generators/php/client.ts +63 -0
- package/lib/eject-assets/generators/php/descriptor.ts +58 -0
- package/lib/eject-assets/generators/php/index.ts +237 -0
- package/lib/eject-assets/generators/php/models.ts +275 -0
- package/lib/eject-assets/generators/php/naming.ts +51 -0
- package/lib/eject-assets/generators/php/operations.ts +232 -0
- package/lib/eject-assets/generators/php/pagination.ts +133 -0
- package/lib/eject-assets/generators/php/types.ts +148 -0
- package/lib/eject-assets/generators/python/client.ts +123 -0
- package/lib/eject-assets/generators/python/descriptor.ts +52 -0
- package/lib/eject-assets/generators/python/index.ts +251 -0
- package/lib/eject-assets/generators/python/models.ts +242 -0
- package/lib/eject-assets/generators/python/naming.ts +46 -0
- package/lib/eject-assets/generators/python/operations.ts +147 -0
- package/lib/eject-assets/generators/python/pagination.ts +128 -0
- package/lib/eject-assets/generators/python/types.ts +54 -0
- package/lib/eject-assets/generators/swr/index.ts +37 -0
- package/lib/eject-assets/generators/swr/render.ts +78 -0
- package/lib/eject-assets/generators/tanstack-query/index.ts +48 -0
- package/lib/eject-assets/generators/tanstack-query/render.ts +346 -0
- package/lib/eject-assets/generators/transformers/index.ts +46 -0
- package/lib/eject-assets/generators/transformers/render.ts +506 -0
- package/lib/eject-assets/generators/typescript/banner.ts +35 -0
- package/lib/eject-assets/generators/typescript/client.ts +254 -0
- package/lib/eject-assets/generators/typescript/descriptor.ts +137 -0
- package/lib/eject-assets/generators/typescript/index.ts +101 -0
- package/lib/eject-assets/generators/typescript/inline-runtime.ts +135 -0
- package/lib/eject-assets/generators/typescript/operation-signature.ts +62 -0
- package/lib/eject-assets/generators/typescript/operation-types.ts +17 -0
- package/lib/eject-assets/generators/typescript/operations.ts +524 -0
- package/lib/eject-assets/generators/typescript/response-headers.ts +74 -0
- package/lib/eject-assets/generators/typescript/type-guards.ts +159 -0
- package/lib/eject-assets/generators/typescript/types.ts +172 -0
- package/lib/eject-assets/generators/zod/index.ts +32 -0
- package/lib/eject-assets/generators/zod/schemas.ts +456 -0
- package/lib/eject-assets/skills/cli-generator/SKILL.md +121 -0
- package/lib/eject-assets/skills/client-generators/SKILL.md +142 -0
- package/lib/eject-assets/skills/go-generator/SKILL.md +96 -0
- package/lib/eject-assets/skills/mock-generator/SKILL.md +50 -0
- package/lib/eject-assets/skills/php-generator/SKILL.md +112 -0
- package/lib/eject-assets/skills/python-generator/SKILL.md +110 -0
- package/lib/eject-assets/skills/swr-generator/SKILL.md +50 -0
- package/lib/eject-assets/skills/tanstack-query-generator/SKILL.md +55 -0
- package/lib/eject-assets/skills/transformers-generator/SKILL.md +47 -0
- package/lib/eject-assets/skills/typescript-generator/SKILL.md +95 -0
- package/lib/eject-assets/skills/zod-generator/SKILL.md +54 -0
- package/lib/index.js +271 -193
- package/package.json +1 -1
- package/lib/chunks/B6HS7CVY.js +0 -184
- package/lib/chunks/PIJLRHNN.js +0 -8
- package/lib/chunks/Y5HLCNL5.js +0 -2542
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Ejected from @redocly/client-generator@0.4.1 — the built-in "php" generator.
|
|
2
|
+
// This file is yours: edit freely; the generated client stays machine-owned and is
|
|
3
|
+
// rebuilt by `redocly generate-client`. Newer generator versions merge in with
|
|
4
|
+
// `redocly eject-generator php --update`.
|
|
5
|
+
// The `operations` stage: one typed request method per operation, plus the
|
|
6
|
+
// argument planning and request prologue it shares with the pagination wrappers.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ApiModel,
|
|
10
|
+
type DateType,
|
|
11
|
+
isMultipartBody,
|
|
12
|
+
jsonSuccessSchema,
|
|
13
|
+
type OperationModel,
|
|
14
|
+
sseResponse,
|
|
15
|
+
uniqueIdentifiers,
|
|
16
|
+
} from '@redocly/client-generator';
|
|
17
|
+
import type { PhpPrinter } from '@redocly/client-generator/printers/php';
|
|
18
|
+
|
|
19
|
+
import { envelopeHeaderSpecs } from './descriptor.ts';
|
|
20
|
+
import { hydration, serialization } from './models.ts';
|
|
21
|
+
import { PHP, phpString } from './naming.ts';
|
|
22
|
+
import { phpElementType, phpNullable, phpType } from './types.ts';
|
|
23
|
+
|
|
24
|
+
const MUTATING = new Set(['post', 'put', 'patch']);
|
|
25
|
+
|
|
26
|
+
type MethodArgs = {
|
|
27
|
+
pathArgs: Array<{ php: string; wire: string; type: string }>;
|
|
28
|
+
/** `value` is the expression to send: a date object formats itself, everything else is the variable. */
|
|
29
|
+
queryArgs: Array<{ php: string; wire: string; type: string; value: string }>;
|
|
30
|
+
signature: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The argument names a request method declares beside its parameters. A parameter named
|
|
35
|
+
* after one of them takes a suffixed variable instead, so the slot keeps its meaning.
|
|
36
|
+
*/
|
|
37
|
+
const SIGNATURE_ARG_SLOTS = ['body', 'headers', 'idempotencyKey'];
|
|
38
|
+
|
|
39
|
+
export function methodArgs(
|
|
40
|
+
op: OperationModel,
|
|
41
|
+
model: ApiModel,
|
|
42
|
+
includeBody: boolean,
|
|
43
|
+
dateType: DateType
|
|
44
|
+
): MethodArgs {
|
|
45
|
+
// Each parameter is its own argument, so path and query names share one namespace with
|
|
46
|
+
// the slots this signature declares itself (`$body`, `$headers`, `$idempotencyKey`).
|
|
47
|
+
// A repeat moves aside (`$id`, `$id_2`): PHP rejects a redefined parameter outright, and
|
|
48
|
+
// a description may legally use one name in two locations.
|
|
49
|
+
const names = uniqueIdentifiers(
|
|
50
|
+
[...op.pathParams, ...op.queryParams].map((param) => param.name),
|
|
51
|
+
{ style: 'camel', reserved: PHP, taken: SIGNATURE_ARG_SLOTS }
|
|
52
|
+
);
|
|
53
|
+
const pathArgs = op.pathParams.map((param, index) => ({
|
|
54
|
+
php: names[index],
|
|
55
|
+
wire: param.name,
|
|
56
|
+
type: phpType(param.schema, model, dateType),
|
|
57
|
+
}));
|
|
58
|
+
const queryArgs = op.queryParams.map((param, index) => {
|
|
59
|
+
const php = names[op.pathParams.length + index];
|
|
60
|
+
return {
|
|
61
|
+
php,
|
|
62
|
+
wire: param.name,
|
|
63
|
+
type: phpType(param.schema, model, dateType),
|
|
64
|
+
value: serialization(param.schema, `${'$'}${php}`, model, dateType) ?? `${'$'}${php}`,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const signature = [
|
|
68
|
+
...pathArgs.map(({ php, type }) => `${type} ${'$'}${php}`),
|
|
69
|
+
...(includeBody && op.requestBody
|
|
70
|
+
? [
|
|
71
|
+
`${isMultipartBody(op) ? 'array' : phpType(op.requestBody.schema, model, dateType)} ${'$'}body`,
|
|
72
|
+
]
|
|
73
|
+
: []),
|
|
74
|
+
...queryArgs.map(({ php, type }) => {
|
|
75
|
+
const nullable = phpNullable(type);
|
|
76
|
+
return `${nullable} ${'$'}${php} = null`;
|
|
77
|
+
}),
|
|
78
|
+
'?array $headers = null',
|
|
79
|
+
...(includeBody && MUTATING.has(op.method.toLowerCase())
|
|
80
|
+
? ['?string $idempotencyKey = null']
|
|
81
|
+
: []),
|
|
82
|
+
];
|
|
83
|
+
return { pathArgs, queryArgs, signature };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The shared prologue: resolve auth, build query/url, merge headers. */
|
|
87
|
+
function writeRequestSetup(printer: PhpPrinter, op: OperationModel, args: MethodArgs): void {
|
|
88
|
+
printer.line(`$op = OPERATIONS[${phpString(op.specName ?? op.name)}];`);
|
|
89
|
+
printer.line(
|
|
90
|
+
"[$authHeaders, $query, $cookies] = resolveAuth($op['security'] ?? [], $this->config->auth);"
|
|
91
|
+
);
|
|
92
|
+
for (const { php, wire, value } of args.queryArgs) {
|
|
93
|
+
printer.block(
|
|
94
|
+
`if (${'$'}${php} !== null) {`,
|
|
95
|
+
() => {
|
|
96
|
+
printer.line(`$query[${phpString(wire)}] = ${value};`);
|
|
97
|
+
},
|
|
98
|
+
'}'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const pathDict = args.pathArgs
|
|
102
|
+
.map(({ php, wire }) => `${phpString(wire)} => ${'$'}${php}`)
|
|
103
|
+
.join(', ');
|
|
104
|
+
printer.line(`$url = buildUrl($this->config->serverUrl, $op['path'], [${pathDict}]);`);
|
|
105
|
+
printer.line('$requestHeaders = array_merge($authHeaders, $headers ?? []);');
|
|
106
|
+
printer.block(
|
|
107
|
+
'if ($cookies !== []) {',
|
|
108
|
+
() => {
|
|
109
|
+
printer.line("$requestHeaders['Cookie'] = implode('; ', $cookies);");
|
|
110
|
+
},
|
|
111
|
+
'}'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function writePhpMethod(
|
|
116
|
+
printer: PhpPrinter,
|
|
117
|
+
op: OperationModel,
|
|
118
|
+
ident: string,
|
|
119
|
+
model: ApiModel,
|
|
120
|
+
dateType: DateType,
|
|
121
|
+
envelope = false
|
|
122
|
+
): void {
|
|
123
|
+
const args = methodArgs(op, model, true, dateType);
|
|
124
|
+
const sse = sseResponse(op);
|
|
125
|
+
const success = jsonSuccessSchema(op);
|
|
126
|
+
// Non-JSON success bodies (PDFs, images, octet streams) return the raw body string.
|
|
127
|
+
const rawBody =
|
|
128
|
+
sse === undefined &&
|
|
129
|
+
success === undefined &&
|
|
130
|
+
op.successResponses.some((response) => response.contentType !== '');
|
|
131
|
+
const returnType = envelope
|
|
132
|
+
? 'Envelope'
|
|
133
|
+
: sse !== undefined
|
|
134
|
+
? '\\Generator'
|
|
135
|
+
: success !== undefined
|
|
136
|
+
? phpType(success, model, dateType)
|
|
137
|
+
: rawBody
|
|
138
|
+
? 'string'
|
|
139
|
+
: 'void';
|
|
140
|
+
const name = envelope ? `${ident}WithHeaders` : ident;
|
|
141
|
+
const element = envelope ? undefined : phpElementType(success, model, dateType);
|
|
142
|
+
printer.doc(
|
|
143
|
+
name,
|
|
144
|
+
envelope
|
|
145
|
+
? `Like ${ident}(), returning an Envelope with the declared response headers.`
|
|
146
|
+
: (op.summary ?? `${op.method.toUpperCase()} ${op.path}`),
|
|
147
|
+
element === undefined ? [] : [`@return ${element}[]`]
|
|
148
|
+
);
|
|
149
|
+
printer.line(`public function ${name}(${args.signature.join(', ')}): ${returnType}`);
|
|
150
|
+
printer.block(
|
|
151
|
+
'{',
|
|
152
|
+
() => {
|
|
153
|
+
writeRequestSetup(printer, op, args);
|
|
154
|
+
if (sse !== undefined) {
|
|
155
|
+
const jsonData = sse.schema !== undefined && sse.schema.kind !== 'unknown';
|
|
156
|
+
printer.line('$url = appendQuery($url, $query);');
|
|
157
|
+
printer.block(
|
|
158
|
+
'$open = function (array $extraHeaders) use ($url, $requestHeaders): \\CurlHandle {',
|
|
159
|
+
() => {
|
|
160
|
+
printer.line('$handle = curl_init($url);');
|
|
161
|
+
printer.line('$lines = [];');
|
|
162
|
+
printer.block(
|
|
163
|
+
'foreach (array_merge($requestHeaders, $extraHeaders) as $name => $value) {',
|
|
164
|
+
() => {
|
|
165
|
+
printer.line("$lines[] = $name . ': ' . $value;");
|
|
166
|
+
},
|
|
167
|
+
'}'
|
|
168
|
+
);
|
|
169
|
+
printer.line('curl_setopt($handle, CURLOPT_HTTPHEADER, $lines);');
|
|
170
|
+
printer.line('return $handle;');
|
|
171
|
+
},
|
|
172
|
+
'};'
|
|
173
|
+
);
|
|
174
|
+
printer.line(`yield from iterSse($open, ${jsonData ? 'true' : 'false'});`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const request = [
|
|
178
|
+
`'operationId' => $op['id']`,
|
|
179
|
+
`'method' => $op['method']`,
|
|
180
|
+
`'url' => $url`,
|
|
181
|
+
`'headers' => $requestHeaders`,
|
|
182
|
+
`'query' => $query`,
|
|
183
|
+
];
|
|
184
|
+
if (op.requestBody && isMultipartBody(op)) {
|
|
185
|
+
printer.line('[$contentType, $encoded] = toMultipart($body);');
|
|
186
|
+
request.push(`'body' => $encoded`, `'contentType' => $contentType`);
|
|
187
|
+
} else if (op.requestBody) {
|
|
188
|
+
const wire = serialization(op.requestBody.schema, '$body', model, dateType) ?? '$body';
|
|
189
|
+
printer.line(`$payload = json_encode(${wire});`);
|
|
190
|
+
request.push(
|
|
191
|
+
`'body' => $payload`,
|
|
192
|
+
`'contentType' => ${phpString(op.requestBody.contentType)}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (MUTATING.has(op.method.toLowerCase()) && op.requestBody) {
|
|
196
|
+
request.push(`'idempotencyKey' => $idempotencyKey`);
|
|
197
|
+
}
|
|
198
|
+
printer.line(`$response = send($this->config, [${request.join(', ')}]);`);
|
|
199
|
+
printer.block(
|
|
200
|
+
"if ($response['status'] >= 400) {",
|
|
201
|
+
() => {
|
|
202
|
+
printer.line('throw apiErrorFrom($response);');
|
|
203
|
+
},
|
|
204
|
+
'}'
|
|
205
|
+
);
|
|
206
|
+
const decoded = rawBody
|
|
207
|
+
? "$response['body']"
|
|
208
|
+
: ((success === undefined
|
|
209
|
+
? undefined
|
|
210
|
+
: hydration(success, 'decodeJson($response)', model, dateType)) ??
|
|
211
|
+
'decodeJson($response)');
|
|
212
|
+
if (envelope) {
|
|
213
|
+
printer.line(`$data = ${decoded};`);
|
|
214
|
+
printer.line(
|
|
215
|
+
`return new Envelope(data: $data, headers: readEnvelopeHeaders($response, ${envelopeHeaderSpecs(op, model)}), status: $response['status']);`
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (rawBody) {
|
|
220
|
+
printer.line("return $response['body'];");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (returnType === 'void') {
|
|
224
|
+
printer.line('decodeJson($response);');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
printer.line(`return ${decoded};`);
|
|
228
|
+
},
|
|
229
|
+
'}'
|
|
230
|
+
);
|
|
231
|
+
printer.blank();
|
|
232
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Ejected from @redocly/client-generator@0.4.1 — the built-in "php" generator.
|
|
2
|
+
// This file is yours: edit freely; the generated client stays machine-owned and is
|
|
3
|
+
// rebuilt by `redocly generate-client`. Newer generator versions merge in with
|
|
4
|
+
// `redocly eject-generator php --update`.
|
|
5
|
+
// The `pagination` stage: the `<op>Pages()` / `<op>Items()` generator methods
|
|
6
|
+
// over the runtime's iterPages.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ApiModel,
|
|
10
|
+
type DateType,
|
|
11
|
+
jsonSuccessSchema,
|
|
12
|
+
type OperationModel,
|
|
13
|
+
} from '@redocly/client-generator';
|
|
14
|
+
import type { PhpPrinter } from '@redocly/client-generator/printers/php';
|
|
15
|
+
|
|
16
|
+
import { phpString } from './naming.ts';
|
|
17
|
+
import { methodArgs } from './operations.ts';
|
|
18
|
+
import { phpType } from './types.ts';
|
|
19
|
+
|
|
20
|
+
/** `<op>Pages()` / `<op>Items()` generators over the runtime's iterPages. */
|
|
21
|
+
export function writePhpPaginationWrappers(
|
|
22
|
+
printer: PhpPrinter,
|
|
23
|
+
op: OperationModel,
|
|
24
|
+
ident: string,
|
|
25
|
+
model: ApiModel,
|
|
26
|
+
dateType: DateType,
|
|
27
|
+
pageHydration: string | undefined,
|
|
28
|
+
itemHydration: string | undefined,
|
|
29
|
+
itemsPointer: string | undefined,
|
|
30
|
+
itemYield: string
|
|
31
|
+
): void {
|
|
32
|
+
const args = methodArgs(op, model, false, dateType);
|
|
33
|
+
const name = ident;
|
|
34
|
+
|
|
35
|
+
const writeCall = () => {
|
|
36
|
+
printer.line(`$op = OPERATIONS[${phpString(op.specName ?? op.name)}];`);
|
|
37
|
+
printer.line('$base = [];');
|
|
38
|
+
for (const { php, wire, value } of args.queryArgs) {
|
|
39
|
+
printer.block(
|
|
40
|
+
`if (${'$'}${php} !== null) {`,
|
|
41
|
+
() => {
|
|
42
|
+
printer.line(`$base[${phpString(wire)}] = ${value};`);
|
|
43
|
+
},
|
|
44
|
+
'}'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const pathDict = args.pathArgs
|
|
48
|
+
.map(({ php, wire }) => `${phpString(wire)} => ${'$'}${php}`)
|
|
49
|
+
.join(', ');
|
|
50
|
+
printer.block(
|
|
51
|
+
'$call = function (array $params) use ($op, $headers): array {',
|
|
52
|
+
() => {
|
|
53
|
+
printer.line(
|
|
54
|
+
"[$authHeaders, $authQuery, $cookies] = resolveAuth($op['security'] ?? [], $this->config->auth);"
|
|
55
|
+
);
|
|
56
|
+
printer.line(`$url = buildUrl($this->config->serverUrl, $op['path'], [${pathDict}]);`);
|
|
57
|
+
printer.line('$requestHeaders = array_merge($authHeaders, $headers ?? []);');
|
|
58
|
+
printer.block(
|
|
59
|
+
'if ($cookies !== []) {',
|
|
60
|
+
() => {
|
|
61
|
+
printer.line("$requestHeaders['Cookie'] = implode('; ', $cookies);");
|
|
62
|
+
},
|
|
63
|
+
'}'
|
|
64
|
+
);
|
|
65
|
+
printer.line(
|
|
66
|
+
"$response = send($this->config, ['operationId' => $op['id'], 'method' => $op['method'], 'url' => $url, 'headers' => $requestHeaders, 'query' => array_merge($params, $authQuery)]);"
|
|
67
|
+
);
|
|
68
|
+
printer.block(
|
|
69
|
+
"if ($response['status'] >= 400) {",
|
|
70
|
+
() => {
|
|
71
|
+
printer.line('throw apiErrorFrom($response);');
|
|
72
|
+
},
|
|
73
|
+
'}'
|
|
74
|
+
);
|
|
75
|
+
printer.line('return [decodeJson($response), $response];');
|
|
76
|
+
},
|
|
77
|
+
'};'
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const pageType = phpType(jsonSuccessSchema(op) ?? { kind: 'unknown' }, model, dateType);
|
|
82
|
+
const pageYield = pageType === 'mixed' ? 'mixed' : pageType;
|
|
83
|
+
printer.line('/**');
|
|
84
|
+
printer.line(` * ${name} response pages, following the pagination rule automatically.`);
|
|
85
|
+
printer.line(' *');
|
|
86
|
+
printer.line(` * @return \\Generator<int, ${pageYield}>`);
|
|
87
|
+
printer.line(' */');
|
|
88
|
+
printer.line(`public function ${name}Pages(${args.signature.join(', ')}): \\Generator`);
|
|
89
|
+
printer.block(
|
|
90
|
+
'{',
|
|
91
|
+
() => {
|
|
92
|
+
writeCall();
|
|
93
|
+
printer.block(
|
|
94
|
+
"foreach (iterPages($call, $op['pagination'], $base) as $page) {",
|
|
95
|
+
() => {
|
|
96
|
+
printer.line(`yield ${pageHydration ?? '$page'};`);
|
|
97
|
+
},
|
|
98
|
+
'}'
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
'}'
|
|
102
|
+
);
|
|
103
|
+
printer.blank();
|
|
104
|
+
|
|
105
|
+
printer.line('/**');
|
|
106
|
+
printer.line(` * The items of every ${name} page.`);
|
|
107
|
+
printer.line(' *');
|
|
108
|
+
printer.line(` * @return \\Generator<int, ${itemYield}>`);
|
|
109
|
+
printer.line(' */');
|
|
110
|
+
printer.line(`public function ${name}Items(${args.signature.join(', ')}): \\Generator`);
|
|
111
|
+
printer.block(
|
|
112
|
+
'{',
|
|
113
|
+
() => {
|
|
114
|
+
writeCall();
|
|
115
|
+
printer.block(
|
|
116
|
+
"foreach (iterPages($call, $op['pagination'], $base) as $page) {",
|
|
117
|
+
() => {
|
|
118
|
+
printer.line(`$items = resolvePointer($page, ${phpString(itemsPointer ?? '')});`);
|
|
119
|
+
printer.block(
|
|
120
|
+
'foreach (is_array($items) ? $items : [] as $item) {',
|
|
121
|
+
() => {
|
|
122
|
+
printer.line(`yield ${itemHydration ?? '$item'};`);
|
|
123
|
+
},
|
|
124
|
+
'}'
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
'}'
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
'}'
|
|
131
|
+
);
|
|
132
|
+
printer.blank();
|
|
133
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Ejected from @redocly/client-generator@0.4.1 — the built-in "php" generator.
|
|
2
|
+
// This file is yours: edit freely; the generated client stays machine-owned and is
|
|
3
|
+
// rebuilt by `redocly generate-client`. Newer generator versions merge in with
|
|
4
|
+
// `redocly eject-generator php --update`.
|
|
5
|
+
// The `types` stage: the PHP type declaration for a schema, its nullable and
|
|
6
|
+
// union forms, and the element type PHP's own syntax erases.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ApiModel,
|
|
10
|
+
type DateType,
|
|
11
|
+
deref,
|
|
12
|
+
enumValues,
|
|
13
|
+
flattenAllOf,
|
|
14
|
+
isNullable,
|
|
15
|
+
type SchemaModel,
|
|
16
|
+
unwrapNullable,
|
|
17
|
+
} from '@redocly/client-generator';
|
|
18
|
+
|
|
19
|
+
import { className } from './naming.ts';
|
|
20
|
+
|
|
21
|
+
/** What a named schema renders as: a class, a native enum, or nothing (alias). */
|
|
22
|
+
export function classify(name: string, model: ApiModel): 'class' | 'enum' | 'other' {
|
|
23
|
+
const named = model.schemas.find((candidate) => candidate.name === name);
|
|
24
|
+
if (named === undefined) return 'other';
|
|
25
|
+
const schema = named.schema;
|
|
26
|
+
const asEnum = enumValues(schema);
|
|
27
|
+
if (asEnum !== undefined && (asEnum.scalar === 'string' || asEnum.scalar === 'integer')) {
|
|
28
|
+
return 'enum';
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
(schema.kind === 'object' || schema.kind === 'intersection') &&
|
|
32
|
+
flattenAllOf(schema, model) !== undefined
|
|
33
|
+
) {
|
|
34
|
+
return 'class';
|
|
35
|
+
}
|
|
36
|
+
return 'other';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The PHP type declaration for a schema (arrays and unions widen to array/mixed). */
|
|
40
|
+
export function phpType(
|
|
41
|
+
schema: SchemaModel,
|
|
42
|
+
model: ApiModel,
|
|
43
|
+
dateType: DateType = 'string'
|
|
44
|
+
): string {
|
|
45
|
+
if (isNullable(schema)) {
|
|
46
|
+
const inner = phpType(unwrapNullable(schema), model, dateType);
|
|
47
|
+
return phpNullable(inner);
|
|
48
|
+
}
|
|
49
|
+
switch (schema.kind) {
|
|
50
|
+
case 'scalar':
|
|
51
|
+
// Under `dateType: Date`, date and date-time become DateTimeImmutable — PHP's
|
|
52
|
+
// immutable date object parses and formats both wire shapes.
|
|
53
|
+
if (dateType === 'Date' && schema.scalar === 'string' && isDateFormat(schema)) {
|
|
54
|
+
return '\\DateTimeImmutable';
|
|
55
|
+
}
|
|
56
|
+
return { string: 'string', integer: 'int', number: 'float', boolean: 'bool' }[schema.scalar];
|
|
57
|
+
case 'array':
|
|
58
|
+
case 'record':
|
|
59
|
+
return 'array';
|
|
60
|
+
case 'ref': {
|
|
61
|
+
const kind = classify(schema.name, model);
|
|
62
|
+
if (kind === 'class' || kind === 'enum') return className(schema.name);
|
|
63
|
+
const target = deref(schema, model);
|
|
64
|
+
return target === undefined ? 'mixed' : phpType(target, model, dateType);
|
|
65
|
+
}
|
|
66
|
+
case 'enum':
|
|
67
|
+
// Anonymous (inline) enums keep the wire scalar; only NAMED enums get types.
|
|
68
|
+
return { string: 'string', integer: 'int', number: 'float', boolean: 'bool' }[schema.scalar];
|
|
69
|
+
case 'literal':
|
|
70
|
+
return typeof schema.value === 'string'
|
|
71
|
+
? 'string'
|
|
72
|
+
: typeof schema.value === 'boolean'
|
|
73
|
+
? 'bool'
|
|
74
|
+
: 'float';
|
|
75
|
+
case 'omit':
|
|
76
|
+
// PHP has no Omit; the base class is the honest annotation.
|
|
77
|
+
return className(schema.base);
|
|
78
|
+
case 'union':
|
|
79
|
+
return phpUnionType(schema.members, model, dateType);
|
|
80
|
+
case 'null':
|
|
81
|
+
case 'object':
|
|
82
|
+
case 'intersection':
|
|
83
|
+
case 'unknown':
|
|
84
|
+
return 'mixed';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `date` or `date-time` — the two formats `dateType: Date` turns into objects. */
|
|
89
|
+
export function isDateFormat(schema: SchemaModel): boolean {
|
|
90
|
+
const format = schema.metadata?.format;
|
|
91
|
+
return format === 'date' || format === 'date-time';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The nullable form of a PHP type. `?T` for a single type, `A|B|null` for a union — PHP
|
|
96
|
+
* forbids mixing `?` with `|`, and `mixed` already includes null.
|
|
97
|
+
*/
|
|
98
|
+
export function phpNullable(type: string): string {
|
|
99
|
+
if (type === 'mixed' || type.startsWith('?') || type.endsWith('|null')) return type;
|
|
100
|
+
return type.includes('|') ? `${type}|null` : `?${type}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A union as a native PHP 8.1 union type (`int|string`, `PromotionType|array`). Rich list
|
|
105
|
+
* filters are usually unions, and collapsing them to `mixed` throws away the typing that
|
|
106
|
+
* makes the SDK worth generating. `mixed` cannot be a union member, so a member without a
|
|
107
|
+
* PHP type of its own (inline object, intersection, unknown) forces the whole union to
|
|
108
|
+
* `mixed`. Members that map to the same PHP type collapse to one.
|
|
109
|
+
*/
|
|
110
|
+
export function phpUnionType(members: SchemaModel[], model: ApiModel, dateType: DateType): string {
|
|
111
|
+
const rendered: string[] = [];
|
|
112
|
+
for (const member of members) {
|
|
113
|
+
// `null` is handled by the caller's nullability check, never as a member here.
|
|
114
|
+
if (member.kind === 'null') continue;
|
|
115
|
+
const type = phpType(member, model, dateType);
|
|
116
|
+
if (type === 'mixed') return 'mixed';
|
|
117
|
+
// A nullable member inside a union contributes its bare type plus null.
|
|
118
|
+
const bare = type.startsWith('?') ? type.slice(1) : type;
|
|
119
|
+
if (!rendered.includes(bare)) rendered.push(bare);
|
|
120
|
+
if (type.startsWith('?') && !rendered.includes('null')) rendered.push('null');
|
|
121
|
+
}
|
|
122
|
+
if (rendered.length === 0) return 'mixed';
|
|
123
|
+
return rendered.join('|');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The element type behind a PHP type that erases it. `array` and `\Generator` are as
|
|
128
|
+
* specific as PHP's syntax gets, so the docblock carries what they hold — that is what
|
|
129
|
+
* static analysis and readers actually go by.
|
|
130
|
+
*/
|
|
131
|
+
export function phpElementType(
|
|
132
|
+
schema: SchemaModel | undefined,
|
|
133
|
+
model: ApiModel,
|
|
134
|
+
dateType: DateType
|
|
135
|
+
): string | undefined {
|
|
136
|
+
if (schema === undefined) return undefined;
|
|
137
|
+
const bare = unwrapNullable(schema);
|
|
138
|
+
if (bare.kind === 'ref') {
|
|
139
|
+
const target = deref(bare, model);
|
|
140
|
+
// A named schema that IS an array (a collection alias) keeps its element type.
|
|
141
|
+
return classify(bare.name, model) === 'other'
|
|
142
|
+
? phpElementType(target, model, dateType)
|
|
143
|
+
: undefined;
|
|
144
|
+
}
|
|
145
|
+
if (bare.kind !== 'array') return undefined;
|
|
146
|
+
const element = phpType(bare.items, model, dateType);
|
|
147
|
+
return element === 'mixed' ? undefined : element;
|
|
148
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Ejected from @redocly/client-generator@0.4.1 — the built-in "python" generator.
|
|
2
|
+
// This file is yours: edit freely; the generated client stays machine-owned and is
|
|
3
|
+
// rebuilt by `redocly generate-client`. Newer generator versions merge in with
|
|
4
|
+
// `redocly eject-generator python --update`.
|
|
5
|
+
// The `client` stage: the `Servers` helper class and the `Client`/`AsyncClient`
|
|
6
|
+
// classes that assemble the per-operation methods.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ApiModel,
|
|
10
|
+
type DateType,
|
|
11
|
+
identifierFor,
|
|
12
|
+
jsonSuccessSchema,
|
|
13
|
+
paginationItemSchema,
|
|
14
|
+
type ServerModel,
|
|
15
|
+
serverUrlParts,
|
|
16
|
+
sseResponse,
|
|
17
|
+
} from '@redocly/client-generator';
|
|
18
|
+
import type { PythonPrinter } from '@redocly/client-generator/printers/python';
|
|
19
|
+
|
|
20
|
+
import { fieldName, naming, operationIdents, PY } from './naming.ts';
|
|
21
|
+
import { writeMethod } from './operations.ts';
|
|
22
|
+
import { writePaginationWrappers } from './pagination.ts';
|
|
23
|
+
import { pythonType } from './types.ts';
|
|
24
|
+
|
|
25
|
+
/** The server URL as a Python expression: literals concatenated with declared-variable args. */
|
|
26
|
+
function serverUrlExpression(server: ServerModel): string {
|
|
27
|
+
const parts = serverUrlParts(server).map((part) =>
|
|
28
|
+
part.kind === 'literal' ? naming.string(part.value) : fieldName(part.name).python
|
|
29
|
+
);
|
|
30
|
+
return parts.join(' + ');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One static method per declared server; server variables become keyword arguments. */
|
|
34
|
+
export function writePythonServers(printer: PythonPrinter, model: ApiModel): void {
|
|
35
|
+
const servers = model.servers ?? [];
|
|
36
|
+
if (servers.length === 0) return;
|
|
37
|
+
const usedNames = new Set<string>();
|
|
38
|
+
printer.block('class Servers:', () => {
|
|
39
|
+
printer.line(
|
|
40
|
+
'"""The declared servers; variables default to the values from the description."""'
|
|
41
|
+
);
|
|
42
|
+
printer.blank();
|
|
43
|
+
servers.forEach((server, index) => {
|
|
44
|
+
let name = identifierFor(server.description ?? `server${index + 1}`, {
|
|
45
|
+
style: 'snake',
|
|
46
|
+
reserved: PY,
|
|
47
|
+
});
|
|
48
|
+
if (usedNames.has(name)) name = `${name}_${index + 1}`;
|
|
49
|
+
usedNames.add(name);
|
|
50
|
+
const params = server.variables.map(
|
|
51
|
+
(variable) => `${fieldName(variable.name).python}: str = ${naming.string(variable.default)}`
|
|
52
|
+
);
|
|
53
|
+
if (index > 0) printer.blank();
|
|
54
|
+
printer.line('@staticmethod');
|
|
55
|
+
printer.block(`def ${name}(${params.join(', ')}) -> str:`, () => {
|
|
56
|
+
printer.line(`return ${serverUrlExpression(server)}`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
printer.blank();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function writeClientClass(
|
|
64
|
+
printer: PythonPrinter,
|
|
65
|
+
model: ApiModel,
|
|
66
|
+
errorMode: 'throw' | 'result',
|
|
67
|
+
isAsync: boolean,
|
|
68
|
+
paginationSpecs: Map<string, Record<string, unknown> | undefined>,
|
|
69
|
+
serverUrl: string,
|
|
70
|
+
dateType: DateType
|
|
71
|
+
): void {
|
|
72
|
+
const name = isAsync ? 'AsyncClient' : 'Client';
|
|
73
|
+
const httpType = isAsync ? 'httpx.AsyncClient' : 'httpx.Client';
|
|
74
|
+
printer.block(`class ${name}:`, () => {
|
|
75
|
+
printer.doc(`${isAsync ? 'Async ' : ''}client for ${model.title} (${model.version}).`);
|
|
76
|
+
printer.block(
|
|
77
|
+
`def __init__(self, server_url: str = ${naming.string(serverUrl)}, *, ` +
|
|
78
|
+
'auth: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, ' +
|
|
79
|
+
'timeout: Optional[float] = None, retry: Optional[Dict[str, Any]] = None, ' +
|
|
80
|
+
'middleware: Optional[List[Any]] = None, idempotency_key: Any = None, ' +
|
|
81
|
+
`http_client: Optional[${httpType}] = None) -> None:`,
|
|
82
|
+
() => {
|
|
83
|
+
printer.line('self._server_url = server_url');
|
|
84
|
+
printer.line('self._auth = auth or {}');
|
|
85
|
+
printer.line('self._config: Dict[str, Any] = {');
|
|
86
|
+
printer.indent(() => {
|
|
87
|
+
printer.line('"headers": headers or {},');
|
|
88
|
+
printer.line('"timeout": timeout,');
|
|
89
|
+
printer.line('"retry": retry or {},');
|
|
90
|
+
printer.line('"middleware": middleware or [],');
|
|
91
|
+
printer.line('"idempotency_key": idempotency_key,');
|
|
92
|
+
});
|
|
93
|
+
printer.line('}');
|
|
94
|
+
printer.line(`self._http = http_client or ${httpType}()`);
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
printer.blank();
|
|
98
|
+
for (const { op, ident } of operationIdents(model)) {
|
|
99
|
+
writeMethod(printer, op, ident, errorMode, isAsync, dateType);
|
|
100
|
+
if (sseResponse(op) === undefined && (op.successResponseHeaders?.length ?? 0) > 0) {
|
|
101
|
+
writeMethod(printer, op, ident, errorMode, isAsync, dateType, model, true);
|
|
102
|
+
}
|
|
103
|
+
const spec = paginationSpecs.get(ident);
|
|
104
|
+
if (spec !== undefined) {
|
|
105
|
+
const success = jsonSuccessSchema(op);
|
|
106
|
+
const element = paginationItemSchema(
|
|
107
|
+
success,
|
|
108
|
+
typeof spec.items === 'string' ? spec.items : undefined,
|
|
109
|
+
model
|
|
110
|
+
);
|
|
111
|
+
writePaginationWrappers(
|
|
112
|
+
printer,
|
|
113
|
+
op,
|
|
114
|
+
ident,
|
|
115
|
+
isAsync,
|
|
116
|
+
element === undefined ? 'Any' : pythonType(element, dateType),
|
|
117
|
+
dateType
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
printer.blank();
|
|
123
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Ejected from @redocly/client-generator@0.4.1 — the built-in "python" generator.
|
|
2
|
+
// This file is yours: edit freely; the generated client stays machine-owned and is
|
|
3
|
+
// rebuilt by `redocly generate-client`. Newer generator versions merge in with
|
|
4
|
+
// `redocly eject-generator python --update`.
|
|
5
|
+
// The `descriptor` stage: the wire-shape literals the embedded runtime routes by —
|
|
6
|
+
// pagination specs, envelope-header coerce specs, and Python data literals.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ApiModel,
|
|
10
|
+
headerCoerceType,
|
|
11
|
+
identifierFor,
|
|
12
|
+
type NeutralPaginationRule,
|
|
13
|
+
type OperationModel,
|
|
14
|
+
} from '@redocly/client-generator';
|
|
15
|
+
|
|
16
|
+
import { naming, PY } from './naming.ts';
|
|
17
|
+
|
|
18
|
+
/** JSON → Python literal (dicts/lists/strings/numbers/bools/None). */
|
|
19
|
+
export function pythonLiteral(value: unknown): string {
|
|
20
|
+
return naming.literal(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The resolved pagination rule mapped to the snake_case spec dict the embedded
|
|
24
|
+
* Python runtime consumes. */
|
|
25
|
+
export function paginationSpec(
|
|
26
|
+
rule: NeutralPaginationRule | undefined
|
|
27
|
+
): Record<string, unknown> | undefined {
|
|
28
|
+
if (rule === undefined) return undefined;
|
|
29
|
+
return {
|
|
30
|
+
style: rule.style,
|
|
31
|
+
...(rule.param !== undefined ? { param: rule.param } : {}),
|
|
32
|
+
...(rule.nextCursor !== undefined ? { next_cursor: rule.nextCursor } : {}),
|
|
33
|
+
...(rule.hasMore !== undefined ? { has_more: rule.hasMore } : {}),
|
|
34
|
+
...(rule.limitParam !== undefined ? { limit_param: rule.limitParam } : {}),
|
|
35
|
+
...(rule.items !== undefined ? { items: rule.items } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Declared response headers as runtime coerce specs: `("wire-name", "snake_key", "type")`. */
|
|
40
|
+
export function envelopeHeaderSpecs(op: OperationModel, model: ApiModel): string {
|
|
41
|
+
const used = new Set<string>();
|
|
42
|
+
const specs = (op.successResponseHeaders ?? []).map((header) => {
|
|
43
|
+
const base = identifierFor(header.name, { style: 'snake', reserved: PY });
|
|
44
|
+
let key = base;
|
|
45
|
+
let suffix = 2;
|
|
46
|
+
while (used.has(key)) key = `${base}_${suffix++}`;
|
|
47
|
+
used.add(key);
|
|
48
|
+
const type = headerCoerceType(header.schema, model);
|
|
49
|
+
return `(${naming.string(header.name)}, ${naming.string(key)}, ${naming.string(type)})`;
|
|
50
|
+
});
|
|
51
|
+
return `[${specs.join(', ')}]`;
|
|
52
|
+
}
|