ebely 0.0.1 → 0.0.2

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/index.mjs ADDED
@@ -0,0 +1,691 @@
1
+ // src/base-store.ts
2
+ var BaseStore = class {
3
+ store = /* @__PURE__ */ new Map();
4
+ /**
5
+ * Типизированный доступ к эндпоинтам ИЗНУТРИ методов-сценариев
6
+ * («actions»): `this.api.<группа>.<метод>(...)`. Само значение
7
+ * подставляет генерируемый `World` (для user-store — клиент этого
8
+ * пользователя, для world-store — анонимный клиент), поэтому здесь это
9
+ * объявление без инициализатора. Тип `Api` пользователь задаёт вторым
10
+ * дженериком, передавая сгенерированный `WorldApi`:
11
+ *
12
+ * import type { WorldApi } from './generated'
13
+ * class UserStore extends BaseStore<Vars, WorldApi> {
14
+ * async fullRegister(args: { email: string; password: string }) {
15
+ * await this.api.auth.register({ body: args })
16
+ * await this.api.auth.confirm({ body: { code: '0000' } })
17
+ * }
18
+ * }
19
+ *
20
+ * (Тип импортируется как `import type` → рантайм-цикла нет, ровно как
21
+ * у `hooks.ts`; см. ARCHITECTURE.md §7–§8.)
22
+ */
23
+ api;
24
+ /** Сохранить внутреннюю переменную пользователя. */
25
+ set(args) {
26
+ this.store.set(args.key, args.value);
27
+ }
28
+ /** Прочитать внутреннюю переменную пользователя (undefined, если не задана). */
29
+ get(args) {
30
+ return this.store.get(args.key);
31
+ }
32
+ };
33
+
34
+ // src/response.ts
35
+ var EbelyAssertionError = class extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = "EbelyAssertionError";
39
+ }
40
+ };
41
+ function safeJson(value) {
42
+ try {
43
+ return JSON.stringify(value) ?? String(value);
44
+ } catch {
45
+ return String(value);
46
+ }
47
+ }
48
+ function matchPartial(args) {
49
+ const { actual, expected, path = "" } = args;
50
+ if (Array.isArray(expected)) {
51
+ if (!Array.isArray(actual))
52
+ return { path, expected, actual };
53
+ for (let i = 0; i < expected.length; i++) {
54
+ const m = matchPartial({
55
+ actual: actual[i],
56
+ expected: expected[i],
57
+ path: `${path}[${i}]`
58
+ });
59
+ if (m)
60
+ return m;
61
+ }
62
+ return null;
63
+ }
64
+ if (expected !== null && typeof expected === "object") {
65
+ if (actual === null || typeof actual !== "object" || Array.isArray(actual)) {
66
+ return { path, expected, actual };
67
+ }
68
+ const exp = expected;
69
+ const act = actual;
70
+ for (const key of Object.keys(exp)) {
71
+ const m = matchPartial({
72
+ actual: act[key],
73
+ expected: exp[key],
74
+ path: path ? `${path}.${key}` : key
75
+ });
76
+ if (m)
77
+ return m;
78
+ }
79
+ return null;
80
+ }
81
+ return Object.is(actual, expected) ? null : { path, expected, actual };
82
+ }
83
+ function assertResponse(args) {
84
+ const { actualStatus, actualBody, expectedStatus, expectedBody } = args;
85
+ if (actualStatus !== expectedStatus) {
86
+ throw new EbelyAssertionError(
87
+ `\u041E\u0436\u0438\u0434\u0430\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441 ${expectedStatus}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D ${actualStatus}.
88
+ \u0422\u0435\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u0430: ${safeJson(actualBody)}`
89
+ );
90
+ }
91
+ if (expectedBody === void 0)
92
+ return;
93
+ const mismatch = matchPartial({ actual: actualBody, expected: expectedBody });
94
+ if (mismatch) {
95
+ throw new EbelyAssertionError(
96
+ `\u0422\u0435\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u043F\u0443\u0442\u0438 "${mismatch.path || "<root>"}": \u043E\u0436\u0438\u0434\u0430\u043B\u043E\u0441\u044C ${safeJson(mismatch.expected)}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E ${safeJson(mismatch.actual)}.
97
+ \u041F\u043E\u043B\u043D\u043E\u0435 \u0442\u0435\u043B\u043E: ${safeJson(actualBody)}`
98
+ );
99
+ }
100
+ }
101
+ var ApiResponse = class {
102
+ /** Фактический HTTP-статус ответа. */
103
+ status;
104
+ /** Распарсенное тело ответа. */
105
+ body;
106
+ constructor(args) {
107
+ this.status = args.status;
108
+ this.body = args.body;
109
+ }
110
+ /**
111
+ * Проверяет статус и (опционально) часть тела ответа.
112
+ *
113
+ * @param status Ожидаемый статус. Подсказывается интеллисенсом из
114
+ * swagger; незадекларированный литерал — ошибка типов. Чтобы
115
+ * проверить статус, которого нет в схеме (например `400`),
116
+ * используйте `res.assert(400 as any, ...)`.
117
+ * @param expectedBody Необязательная часть тела: проверяются только
118
+ * переданные поля (глубоко-частично), остальные игнорируются.
119
+ * @returns тот же объект ответа — удобно читать `.body` после проверки.
120
+ */
121
+ assert(status, expectedBody) {
122
+ assertResponse({
123
+ actualStatus: this.status,
124
+ actualBody: this.body,
125
+ expectedStatus: status,
126
+ expectedBody
127
+ });
128
+ return this;
129
+ }
130
+ };
131
+
132
+ // src/hooks.ts
133
+ var HookRegistry = class {
134
+ beforeMap = /* @__PURE__ */ new Map();
135
+ afterMap = /* @__PURE__ */ new Map();
136
+ /** Зарегистрировать `before`-хук на ключ `"<группа>.<метод>"`. */
137
+ before(args) {
138
+ const list = this.beforeMap.get(args.key) ?? [];
139
+ list.push(args.fn);
140
+ this.beforeMap.set(args.key, list);
141
+ }
142
+ /** Зарегистрировать `after`-хук на ключ `"<группа>.<метод>"`. */
143
+ after(args) {
144
+ const list = this.afterMap.get(args.key) ?? [];
145
+ list.push(args.fn);
146
+ this.afterMap.set(args.key, list);
147
+ }
148
+ /** Прогнать все `before`-хуки ключа по очереди (await на async). */
149
+ async runBefore(args) {
150
+ for (const fn of this.beforeMap.get(args.key) ?? []) {
151
+ await fn({ request: args.request, ctx: args.ctx });
152
+ }
153
+ }
154
+ /** Прогнать все `after`-хуки ключа по очереди (await на async). */
155
+ async runAfter(args) {
156
+ for (const fn of this.afterMap.get(args.key) ?? []) {
157
+ await fn({ request: args.request, response: args.response, ctx: args.ctx });
158
+ }
159
+ }
160
+ };
161
+
162
+ // src/generate-client.ts
163
+ import { writeFile } from "fs/promises";
164
+ import { resolve as resolve2 } from "path";
165
+
166
+ // src/generator/schema.ts
167
+ function schemaToType(args) {
168
+ const { schema, spec, indent = 0 } = args;
169
+ if (!schema)
170
+ return "unknown";
171
+ if (typeof schema.$ref === "string") {
172
+ const resolved = resolveRef({ ref: schema.$ref, spec });
173
+ return schemaToType({ schema: resolved, spec, indent });
174
+ }
175
+ for (const key of ["allOf", "oneOf", "anyOf"]) {
176
+ if (Array.isArray(schema[key])) {
177
+ const joiner = key === "allOf" ? " & " : " | ";
178
+ return schema[key].map((s) => schemaToType({ schema: s, spec, indent })).join(joiner);
179
+ }
180
+ }
181
+ if (Array.isArray(schema.enum)) {
182
+ return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
183
+ }
184
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
185
+ const rendered = types.map((type) => {
186
+ switch (type) {
187
+ case "string":
188
+ return "string";
189
+ case "number":
190
+ case "integer":
191
+ return "number";
192
+ case "boolean":
193
+ return "boolean";
194
+ case "null":
195
+ return "null";
196
+ case "array":
197
+ return `Array<${schemaToType({ schema: schema.items, spec, indent })}>`;
198
+ case "object": {
199
+ const props = schema.properties ?? {};
200
+ const required = schema.required ?? [];
201
+ const keys = Object.keys(props);
202
+ if (keys.length === 0)
203
+ return "Record<string, unknown>";
204
+ const pad = " ".repeat(indent + 1);
205
+ const closePad = " ".repeat(indent);
206
+ const lines = keys.map((k) => {
207
+ const optional = required.includes(k) ? "" : "?";
208
+ const valueType = schemaToType({ schema: props[k], spec, indent: indent + 1 });
209
+ return `${pad}${JSON.stringify(k)}${optional}: ${valueType}`;
210
+ });
211
+ return `{
212
+ ${lines.join("\n")}
213
+ ${closePad}}`;
214
+ }
215
+ default:
216
+ return "unknown";
217
+ }
218
+ });
219
+ return rendered.join(" | ");
220
+ }
221
+ function resolveRef(args) {
222
+ const { ref, spec } = args;
223
+ if (!ref.startsWith("#/"))
224
+ return void 0;
225
+ return ref.slice(2).split("/").reduce((acc, part) => acc ? acc[part] : void 0, spec);
226
+ }
227
+ function pickResponseSchema(args) {
228
+ const { responses } = args;
229
+ const status = Object.keys(responses ?? {}).find((s) => s.startsWith("2")) ?? "default";
230
+ return responses?.[status]?.content?.["application/json"]?.schema;
231
+ }
232
+ function collectResponseSchemas(args) {
233
+ const { responses } = args;
234
+ const out = [];
235
+ for (const key of Object.keys(responses ?? {})) {
236
+ const status = Number(key);
237
+ if (!Number.isInteger(status))
238
+ continue;
239
+ out.push({
240
+ status,
241
+ schema: responses[key]?.content?.["application/json"]?.schema
242
+ });
243
+ }
244
+ return out;
245
+ }
246
+
247
+ // src/generator/operations.ts
248
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
249
+ function collectOperations(args) {
250
+ const { spec } = args;
251
+ const operations = [];
252
+ for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {
253
+ for (const method of HTTP_METHODS) {
254
+ const op = pathItem[method];
255
+ if (!op)
256
+ continue;
257
+ const operationId = op.operationId ?? `${method}${path}`;
258
+ const dotIndex = operationId.indexOf(".");
259
+ const group = dotIndex === -1 ? "default" : operationId.slice(0, dotIndex);
260
+ const name = dotIndex === -1 ? operationId : operationId.slice(dotIndex + 1);
261
+ const parameters = op.parameters ?? [];
262
+ const pathParams = parameters.filter((p) => p.in === "path").map((p) => p.name);
263
+ const queryParams = parameters.filter((p) => p.in === "query").map((p) => p.name);
264
+ const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
265
+ const responseSchema = pickResponseSchema({ responses: op.responses, spec });
266
+ const responseType = schemaToType({ schema: responseSchema, spec, indent: 3 });
267
+ const declared = collectResponseSchemas({ responses: op.responses, spec });
268
+ const responses = declared.length > 0 ? declared.map((r) => ({
269
+ status: r.status,
270
+ bodyType: schemaToType({ schema: r.schema, spec, indent: 4 })
271
+ })) : [{ status: 200, bodyType: responseType }];
272
+ operations.push({
273
+ group,
274
+ name,
275
+ method,
276
+ path,
277
+ pathParams,
278
+ queryParams,
279
+ bodyType: bodySchema ? schemaToType({ schema: bodySchema, spec, indent: 4 }) : null,
280
+ bodyRequired: Boolean(op.requestBody?.required),
281
+ responseType,
282
+ responses,
283
+ summary: op.summary
284
+ });
285
+ }
286
+ }
287
+ return operations;
288
+ }
289
+
290
+ // src/generator/render.ts
291
+ function buildInputType(op) {
292
+ const parts = [];
293
+ if (op.pathParams.length > 0) {
294
+ const fields = op.pathParams.map((p) => `${JSON.stringify(p)}: string`).join("; ");
295
+ parts.push(`path: { ${fields} }`);
296
+ }
297
+ if (op.queryParams.length > 0) {
298
+ const fields = op.queryParams.map((p) => `${JSON.stringify(p)}?: string | number | boolean`).join("; ");
299
+ parts.push(`query?: { ${fields} }`);
300
+ }
301
+ if (op.bodyType) {
302
+ parts.push(`body${op.bodyRequired ? "" : "?"}: ${op.bodyType}`);
303
+ }
304
+ if (parts.length === 0)
305
+ return { type: "{}", optional: true };
306
+ const optional = op.pathParams.length === 0 && (!op.bodyType || !op.bodyRequired);
307
+ return { type: `{ ${parts.join("; ")} }`, optional };
308
+ }
309
+ function buildStatusMapType(op) {
310
+ const entries = op.responses.map((r) => `${r.status}: ${r.bodyType}`);
311
+ return `{ ${entries.join("; ")} }`;
312
+ }
313
+ function methodReturnType(args) {
314
+ const { op, mode } = args;
315
+ if (mode === "frontend")
316
+ return `Promise<${op.responseType}>`;
317
+ return `Promise<ApiResponse<${buildStatusMapType(op)}>>`;
318
+ }
319
+ function hookKey(op) {
320
+ return `${op.group}.${op.name}`;
321
+ }
322
+ function hookBodyType(op) {
323
+ return op.bodyType ?? "undefined";
324
+ }
325
+ function renderMethod(args) {
326
+ const { op, mode } = args;
327
+ const { type, optional } = buildInputType(op);
328
+ const inputParam = `input${optional ? "?" : ""}: ${type}`;
329
+ const doc = op.summary ? `
330
+ /** ${op.summary} */` : "";
331
+ const call = `request({
332
+ method: ${JSON.stringify(op.method.toUpperCase())},
333
+ path: ${JSON.stringify(op.path)},
334
+ opKey: ${JSON.stringify(hookKey(op))},
335
+ input: input as RequestInput,
336
+ })`;
337
+ if (mode === "frontend") {
338
+ return `${doc}
339
+ ${JSON.stringify(op.name)}: (${inputParam}): Promise<${op.responseType}> =>
340
+ ${call} as Promise<${op.responseType}>,`;
341
+ }
342
+ const map = buildStatusMapType(op);
343
+ return `${doc}
344
+ ${JSON.stringify(op.name)}: async (${inputParam}): Promise<ApiResponse<${map}>> => {
345
+ const res = await ${call}
346
+ return new ApiResponse(res) as unknown as ApiResponse<${map}>
347
+ },`;
348
+ }
349
+ function renderImports(args) {
350
+ const { mode, userStoreImport, configImport } = args;
351
+ const values = mode === "frontend" ? "BaseStore, HookRegistry" : "BaseStore, ApiResponse, HookRegistry";
352
+ return `import { ${values} } from ${JSON.stringify(userStoreImport)}
353
+ import type { BeforeHook, AfterHook } from ${JSON.stringify(userStoreImport)}
354
+ import { ebely } from ${JSON.stringify(configImport)}`;
355
+ }
356
+ function renderRequestTail(mode) {
357
+ if (mode === "frontend") {
358
+ return {
359
+ returnType: "unknown",
360
+ ret: ` if (!response.ok) {
361
+ throw new Error(
362
+ \`Request \${method} \${resolvedPath} failed with \${response.status}: \${text}\`,
363
+ )
364
+ }
365
+
366
+ return data`
367
+ };
368
+ }
369
+ return {
370
+ returnType: "{ status: number; body: unknown }",
371
+ ret: ` return { status: response.status, body: data }`
372
+ };
373
+ }
374
+ function groupOperations(operations) {
375
+ const groups = /* @__PURE__ */ new Map();
376
+ for (const op of operations) {
377
+ const list = groups.get(op.group) ?? [];
378
+ list.push(op);
379
+ groups.set(op.group, list);
380
+ }
381
+ return groups;
382
+ }
383
+ function renderApiType(args) {
384
+ const { groups, mode } = args;
385
+ const blocks = [...groups.entries()].map(([group, ops]) => {
386
+ const leaves = ops.map((op) => {
387
+ const { type, optional } = buildInputType(op);
388
+ const inputParam = `input${optional ? "?" : ""}: ${type}`;
389
+ return ` ${JSON.stringify(op.name)}: (${inputParam}) => ${methodReturnType({ op, mode })}`;
390
+ });
391
+ return ` ${JSON.stringify(group)}: {
392
+ ${leaves.join("\n")}
393
+ }`;
394
+ });
395
+ return `export type WorldApi = {
396
+ ${blocks.join("\n")}
397
+ }`;
398
+ }
399
+ function renderHookTreeType(groups) {
400
+ const blocks = [...groups.entries()].map(([group, ops]) => {
401
+ const leaves = ops.map((op) => {
402
+ const body = hookBodyType(op);
403
+ return ` ${JSON.stringify(op.name)}: {
404
+ before(fn: BeforeHook<Store, ${body}>): void
405
+ after(fn: AfterHook<Store, ${body}, ${op.responseType}>): void
406
+ }`;
407
+ });
408
+ return ` ${JSON.stringify(group)}: {
409
+ ${leaves.join("\n")}
410
+ }`;
411
+ });
412
+ return `type EbelyHookTree<Store extends BaseStore> = {
413
+ ${blocks.join("\n")}
414
+ }`;
415
+ }
416
+ function renderHookTreeBuilder(groups) {
417
+ const blocks = [...groups.entries()].map(([group, ops]) => {
418
+ const leaves = ops.map((op) => {
419
+ const key = JSON.stringify(hookKey(op));
420
+ return ` ${JSON.stringify(op.name)}: {
421
+ before: (fn: BeforeHook<Store>) => r.before({ key: ${key}, fn }),
422
+ after: (fn: AfterHook<Store>) => r.after({ key: ${key}, fn }),
423
+ },`;
424
+ });
425
+ return ` ${JSON.stringify(group)}: {
426
+ ${leaves.join("\n")}
427
+ },`;
428
+ });
429
+ return ` private buildHookTree(): EbelyHookTree<Store> {
430
+ const r = this.hookRegistry
431
+ return {
432
+ ${blocks.join("\n")}
433
+ } as unknown as EbelyHookTree<Store>
434
+ }`;
435
+ }
436
+ function renderApiTreeBuilder(args) {
437
+ const { groups, mode } = args;
438
+ const groupBlocks = [...groups.entries()].map(([group, ops]) => {
439
+ const methods = ops.map((op) => renderMethod({ op, mode }));
440
+ return ` ${JSON.stringify(group)}: {
441
+ ${methods.join("\n")}
442
+ },`;
443
+ });
444
+ return ` /** \u0414\u0435\u0440\u0435\u0432\u043E \u0442\u0438\u043F\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0432\u044B\u0437\u043E\u0432\u043E\u0432 \u044D\u043D\u0434\u043F\u043E\u0438\u043D\u0442\u043E\u0432 \u043F\u043E\u0432\u0435\u0440\u0445 \u043E\u0434\u043D\u043E\u0433\u043E \`request\`. */
445
+ private buildApiTree(request: RequestFn) {
446
+ return {
447
+ ${groupBlocks.join("\n")}
448
+ }
449
+ }`;
450
+ }
451
+ function renderClient(args) {
452
+ const { spec, operations, mode, userStoreImport, configImport } = args;
453
+ const basePath = spec.servers?.[0]?.url ?? "";
454
+ const groups = groupOperations(operations);
455
+ const tail = renderRequestTail(mode);
456
+ return `// AUTO-GENERATED by ebely (generateClient) \u2014 \u041D\u0415 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0440\u0443\u0447\u043D\u0443\u044E.
457
+ // \u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A: ${spec.info?.title ?? "OpenAPI spec"} v${spec.info?.version ?? "?"}
458
+ // \u0420\u0435\u0436\u0438\u043C \u043A\u043B\u0438\u0435\u043D\u0442\u0430: ${mode}
459
+ // \u041F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F: pnpm run client:generate
460
+
461
+ ${renderImports({ mode, userStoreImport, configImport })}
462
+
463
+ type RequestInput = {
464
+ path?: Record<string, string>
465
+ query?: Record<string, string | number | boolean | undefined>
466
+ body?: unknown
467
+ }
468
+
469
+ type RequestFn = (req: {
470
+ method: string
471
+ path: string
472
+ opKey: string
473
+ input?: RequestInput
474
+ }) => Promise<${tail.returnType}>
475
+
476
+ export type CreateUserArgs = {
477
+ /** \u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043A\u043E \u0432\u0441\u0435\u043C \u0437\u0430\u043F\u0440\u043E\u0441\u0430\u043C \u044D\u0442\u043E\u0433\u043E \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F. */
478
+ headers?: Record<string, string>
479
+ }
480
+
481
+ ${renderApiType({ groups, mode })}
482
+
483
+ ${renderHookTreeType(groups)}
484
+
485
+ /**
486
+ * \u0422\u0438\u043F \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u0445\u0443\u043A\u043E\u0432 \u0434\u043B\u044F \u042D\u0422\u041E\u0413\u041E \u0431\u044D\u043A\u0435\u043D\u0434\u0430. \u041E\u0431\u044A\u044F\u0432\u0438\u0442\u0435 \u0445\u0443\u043A\u0438 \u0432 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u043C
487
+ * \u0444\u0430\u0439\u043B\u0435 \u0438 \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435 \u043E\u0434\u043D\u043E\u0439 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439 \u0432 \u043A\u043E\u043D\u0444\u0438\u0433 (\`EbelyConfig.hooks\`).
488
+ * \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0421\u0412\u041E\u0419 \u043A\u043B\u0430\u0441\u0441 store \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u043C, \u0447\u0442\u043E\u0431\u044B \`ctx\` \u0431\u044B\u043B \u0442\u0438\u043F\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D:
489
+ *
490
+ * import type { UserStore } from './userStore'
491
+ * export const hooks: Hooks<UserStore> = (h) => {
492
+ * h.posts.create.after(({ response, ctx }) => { \u2026 })
493
+ * }
494
+ *
495
+ * (\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u041D\u0415 \u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u0438\u0437 \`ebely.userStore\` \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u043D\u043E: \u044D\u0442\u043E \u0441\u043E\u0437\u0434\u0430\u043B\u043E
496
+ * \u0431\u044B \u0446\u0438\u043A\u043B \u0442\u0438\u043F\u043E\u0432 \`ebely\` \u21C4 \`Hooks\`, \u0442.\u043A. \`hooks\` \u043B\u0435\u0436\u0438\u0442 \u0432\u043D\u0443\u0442\u0440\u0438 \`ebely\`.)
497
+ */
498
+ export type Hooks<Store extends BaseStore = BaseStore> = (
499
+ h: EbelyHookTree<Store>,
500
+ ) => void
501
+
502
+ /**
503
+ * \u0411\u0430\u0437\u0430 \`World\` \u2014 \u044D\u0442\u043E \u0441\u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \`ebely.worldStore\` (\u0438\u043B\u0438 \u043F\u0443\u0441\u0442\u043E\u0439
504
+ * \`BaseStore\`, \u0435\u0441\u043B\u0438 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D). \u041F\u043E\u044D\u0442\u043E\u043C\u0443 \`world.<\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439>()\` \u0438
505
+ * \`world.get/set\` \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B \u0438 \u0442\u0438\u043F\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u044B \u0440\u043E\u0432\u043D\u043E \u043A\u0430\u043A \u0443 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F,
506
+ * \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u0431\u043B\u0430\u0441\u0442\u044C \u2014 \u0432\u0435\u0441\u044C \u043C\u0438\u0440. \u0422\u0438\u043F \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0438\u0437 \`ebely\` \u0442\u0435\u043C \u0436\u0435 \u043F\u0440\u0438\u0451\u043C\u043E\u043C, \u0447\u0442\u043E
507
+ * \u0438 \`Store\` (\u043D\u0438\u043A\u0430\u043A\u0438\u0445 \u0440\u0430\u043D\u0442\u0430\u0439\u043C-\u0443\u0441\u043B\u043E\u0432\u0438\u0439 \u2014 \u0441\u043C. ARCHITECTURE.md \xA78).
508
+ */
509
+ type ConfiguredWorldStore =
510
+ typeof ebely extends { worldStore: new () => infer I extends BaseStore }
511
+ ? I
512
+ : BaseStore
513
+ const WorldStoreBase = ((ebely as { worldStore?: new () => BaseStore })
514
+ .worldStore ?? BaseStore) as new () => ConfiguredWorldStore
515
+
516
+ export class World<
517
+ Store extends BaseStore = InstanceType<typeof ebely.userStore>,
518
+ > extends WorldStoreBase {
519
+ /**
520
+ * \u041E\u0431\u0449\u0438\u0439 \u0440\u0435\u0435\u0441\u0442\u0440 \u0445\u0443\u043A\u043E\u0432. \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F \u2014 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0430\u044F (\u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u0438\u0437 \u043A\u043E\u043D\u0444\u0438\u0433\u0430),
521
+ * \u043D\u043E \`ctx\` \u043F\u043E\u0434\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0432 \u043C\u043E\u043C\u0435\u043D\u0442 \u0437\u0430\u043F\u0440\u043E\u0441\u0430 = store \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u0433\u043E \u044E\u0437\u0435\u0440\u0430.
522
+ */
523
+ private hookRegistry = new HookRegistry()
524
+
525
+ constructor(
526
+ public args: {
527
+ /** URL \u0431\u044D\u043A\u0435\u043D\u0434\u0430. \u0415\u0441\u043B\u0438 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F ebely.url \u0438\u0437 \u043A\u043E\u043D\u0444\u0438\u0433\u0430. */
528
+ url?: string
529
+ /** \u041A\u043B\u0430\u0441\u0441-\u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435. \u0415\u0441\u043B\u0438 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F ebely.userStore. */
530
+ store?: new () => Store
531
+ } = {},
532
+ ) {
533
+ super()
534
+ const registrar = (ebely as { hooks?: (h: unknown) => void }).hooks
535
+ if (registrar) registrar(this.buildHookTree())
536
+ // world-store \u0445\u043E\u0434\u0438\u0442 \u0410\u041D\u041E\u041D\u0418\u041C\u041D\u042B\u041C \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C (\u0431\u0435\u0437 per-user \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432);
537
+ // ctx \u0445\u0443\u043A\u043E\u0432 \u0434\u043B\u044F \u0442\u0430\u043A\u0438\u0445 \u0432\u044B\u0437\u043E\u0432\u043E\u0432 = \u0441\u0430\u043C world.
538
+ ;(this as unknown as { api: unknown }).api = this.buildApiTree(
539
+ this.makeRequest({ headers: {}, store: this }),
540
+ )
541
+ }
542
+
543
+ /** \u0411\u0430\u0437\u043E\u0432\u044B\u0439 URL \u0441 \u0443\u0447\u0451\u0442\u043E\u043C server.url \u0438\u0437 \u0441\u0445\u0435\u043C\u044B. */
544
+ private baseUrl(): string {
545
+ return (this.args.url ?? ebely.url).replace(/\\/$/, '') + ${JSON.stringify(basePath)}
546
+ }
547
+
548
+ ${renderHookTreeBuilder(groups)}
549
+
550
+ /**
551
+ * \u0421\u043E\u0437\u0434\u0430\u0451\u0442 \`request\`, \u0437\u0430\u043C\u043A\u043D\u0443\u0442\u044B\u0439 \u043D\u0430 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0438 \u0438 \`store\` (\u043E\u043D \u0436\u0435 \`ctx\`
552
+ * \u0445\u0443\u043A\u043E\u0432). \u041E\u0434\u0438\u043D \u0438 \u0442\u043E\u0442 \u0436\u0435 \u0434\u0432\u0438\u0436\u043E\u043A \u0438 \u0434\u043B\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, \u0438 \u0434\u043B\u044F world.
553
+ */
554
+ private makeRequest(cfg: {
555
+ headers: Record<string, string>
556
+ store: BaseStore
557
+ }): RequestFn {
558
+ const baseUrl = this.baseUrl()
559
+ const registry = this.hookRegistry
560
+ const { headers: baseHeaders, store } = cfg
561
+
562
+ return async (req): Promise<${tail.returnType}> => {
563
+ const { method, path, opKey, input } = req
564
+
565
+ const hookReq = {
566
+ method,
567
+ path,
568
+ pathParams: { ...(input?.path ?? {}) },
569
+ query: { ...(input?.query ?? {}) },
570
+ body: input?.body,
571
+ headers: { ...baseHeaders },
572
+ }
573
+ await registry.runBefore({ key: opKey, request: hookReq, ctx: store })
574
+
575
+ let resolvedPath = path
576
+ for (const [k, v] of Object.entries(hookReq.pathParams)) {
577
+ resolvedPath = resolvedPath.replace(
578
+ \`{\${k}}\`,
579
+ encodeURIComponent(String(v)),
580
+ )
581
+ }
582
+
583
+ const url = new URL(baseUrl + resolvedPath)
584
+ for (const [k, v] of Object.entries(hookReq.query)) {
585
+ if (v !== undefined) url.searchParams.set(k, String(v))
586
+ }
587
+
588
+ const hasBody = hookReq.body !== undefined
589
+ const response = await fetch(url, {
590
+ method,
591
+ headers: {
592
+ ...(hasBody ? { 'content-type': 'application/json' } : {}),
593
+ ...hookReq.headers,
594
+ },
595
+ body: hasBody ? JSON.stringify(hookReq.body) : undefined,
596
+ })
597
+
598
+ const text = await response.text()
599
+ let data: unknown
600
+ try {
601
+ data = text ? JSON.parse(text) : undefined
602
+ } catch {
603
+ data = text
604
+ }
605
+
606
+ await registry.runAfter({
607
+ key: opKey,
608
+ request: hookReq,
609
+ response: { status: response.status, body: data },
610
+ ctx: store,
611
+ })
612
+
613
+ ${tail.ret}
614
+ }
615
+ }
616
+
617
+ ${renderApiTreeBuilder({ groups, mode })}
618
+
619
+ /**
620
+ * \u0421\u043E\u0437\u0434\u0430\u0451\u0442 \xAB\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F\xBB \u2014 \u0438\u0437\u043E\u043B\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u0442\u0438\u043F\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0432\u044B\u0437\u043E\u0432\u043E\u0432
621
+ * \u044D\u043D\u0434\u043F\u043E\u0438\u043D\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u043F\u043E\u0434 \u043A\u0430\u043F\u043E\u0442\u043E\u043C \u0445\u043E\u0434\u0438\u0442 fetch-\u0437\u0430\u043F\u0440\u043E\u0441\u0430\u043C\u0438. \`store.api\`
622
+ * \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u043D\u0430 \u0442\u043E \u0436\u0435 \u0434\u0435\u0440\u0435\u0432\u043E, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043C\u0435\u0442\u043E\u0434\u044B-\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0438 \u044D\u0442\u043E\u0433\u043E store
623
+ * (\`fullRegister\` \u0438 \u0442.\u043F.) \u0445\u043E\u0434\u044F\u0442 \u043E\u0442 \u043B\u0438\u0446\u0430 \u0438\u043C\u0435\u043D\u043D\u043E \u044D\u0442\u043E\u0433\u043E \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F.
624
+ */
625
+ createUser(userArgs: CreateUserArgs = {}) {
626
+ const StoreClass =
627
+ this.args.store ?? (ebely.userStore as unknown as new () => Store)
628
+ const store = new StoreClass()
629
+ const tree = this.buildApiTree(
630
+ this.makeRequest({ headers: { ...userArgs.headers }, store }),
631
+ )
632
+ ;(store as unknown as { api: unknown }).api = tree
633
+ return Object.assign(store, tree)
634
+ }
635
+ }
636
+ `;
637
+ }
638
+
639
+ // src/generator/swagger.ts
640
+ import { readFile } from "fs/promises";
641
+ import { resolve } from "path";
642
+ async function loadSpec(args) {
643
+ const { swagger } = args;
644
+ if (swagger.pathToFile !== void 0) {
645
+ const specPath = resolve(process.cwd(), swagger.pathToFile);
646
+ return JSON.parse(await readFile(specPath, "utf8"));
647
+ }
648
+ const response = await fetch(swagger.url);
649
+ if (!response.ok) {
650
+ throw new Error(
651
+ `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C swagger \u043F\u043E ${swagger.url}: ${response.status} ${response.statusText}`
652
+ );
653
+ }
654
+ return await response.json();
655
+ }
656
+
657
+ // src/generate-client.ts
658
+ async function generateClient(args) {
659
+ const {
660
+ swagger,
661
+ generateClientTo,
662
+ mode = "test",
663
+ userStoreImport = "ebely",
664
+ configImport = "./ebely"
665
+ } = args;
666
+ const outPath = resolve2(process.cwd(), generateClientTo);
667
+ const spec = await loadSpec({ swagger });
668
+ const operations = collectOperations({ spec });
669
+ const source = renderClient({
670
+ spec,
671
+ operations,
672
+ mode,
673
+ userStoreImport,
674
+ configImport
675
+ });
676
+ await writeFile(outPath, source, "utf8");
677
+ console.log(`
678
+ \u2705 Typed client written to:
679
+ ${outPath}
680
+
681
+ Endpoints: ${operations.length}
682
+ `);
683
+ return { outPath, operations: operations.length };
684
+ }
685
+ export {
686
+ ApiResponse,
687
+ BaseStore,
688
+ EbelyAssertionError,
689
+ HookRegistry,
690
+ generateClient
691
+ };