vovk 3.5.1 → 3.7.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.
Files changed (113) hide show
  1. package/dist/client/create-rpc.d.ts +13 -0
  2. package/dist/client/create-rpc.js +147 -0
  3. package/dist/client/default-handler.d.ts +6 -0
  4. package/dist/client/default-handler.js +25 -0
  5. package/dist/client/default-stream-handler.d.ts +16 -0
  6. package/dist/client/default-stream-handler.js +282 -0
  7. package/dist/client/fetcher.d.ts +1 -1
  8. package/dist/client/fetcher.js +2 -2
  9. package/dist/client/serialize-query.d.ts +13 -0
  10. package/dist/client/serialize-query.js +62 -0
  11. package/dist/core/apply-decorator-adapter.d.ts +7 -0
  12. package/dist/core/apply-decorator-adapter.js +50 -0
  13. package/dist/core/controllers-to-static-params.d.ts +13 -0
  14. package/dist/core/controllers-to-static-params.js +32 -0
  15. package/dist/core/create-decorator.d.ts +12 -0
  16. package/dist/core/create-decorator.js +52 -0
  17. package/dist/core/decorators.js +4 -4
  18. package/dist/core/get-schema.d.ts +21 -0
  19. package/dist/core/get-schema.js +31 -0
  20. package/dist/core/http-exception.d.ts +16 -0
  21. package/dist/core/http-exception.js +26 -0
  22. package/dist/core/init-segment.d.ts +33 -0
  23. package/dist/core/init-segment.js +62 -0
  24. package/dist/core/json-lines-responder.d.ts +42 -0
  25. package/dist/core/json-lines-responder.js +94 -0
  26. package/dist/core/resolve-generator-config-values.d.ts +19 -0
  27. package/dist/core/resolve-generator-config-values.js +59 -0
  28. package/dist/core/set-handler-schema.d.ts +4 -0
  29. package/dist/core/set-handler-schema.js +12 -0
  30. package/dist/core/to-download-response.d.ts +11 -0
  31. package/dist/core/to-download-response.js +25 -0
  32. package/dist/core/vovk-app.d.ts +36 -0
  33. package/dist/core/vovk-app.js +318 -0
  34. package/dist/index.d.ts +10 -10
  35. package/dist/index.js +10 -10
  36. package/dist/internal.d.ts +10 -10
  37. package/dist/internal.js +9 -9
  38. package/dist/openapi/error.js +1 -1
  39. package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.d.ts +23 -0
  40. package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.js +90 -0
  41. package/dist/openapi/openapi-to-vovk-schema/index.d.ts +5 -0
  42. package/dist/openapi/openapi-to-vovk-schema/index.js +179 -0
  43. package/dist/openapi/openapi-to-vovk-schema/inline-refs.d.ts +9 -0
  44. package/dist/openapi/openapi-to-vovk-schema/inline-refs.js +99 -0
  45. package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.d.ts +7 -0
  46. package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.js +51 -0
  47. package/dist/openapi/operation.js +1 -1
  48. package/dist/openapi/tool.js +1 -1
  49. package/dist/openapi/vovk-schema-to-openapi.d.ts +21 -0
  50. package/dist/openapi/vovk-schema-to-openapi.js +250 -0
  51. package/dist/req/buffer-body.d.ts +1 -0
  52. package/dist/req/buffer-body.js +30 -0
  53. package/dist/req/parse-body.d.ts +4 -0
  54. package/dist/req/parse-body.js +49 -0
  55. package/dist/req/parse-form.d.ts +1 -0
  56. package/dist/req/parse-form.js +24 -0
  57. package/dist/req/parse-query.d.ts +24 -0
  58. package/dist/req/parse-query.js +156 -0
  59. package/dist/req/req-meta.d.ts +2 -0
  60. package/dist/req/req-meta.js +10 -0
  61. package/dist/req/req-query.d.ts +2 -0
  62. package/dist/req/req-query.js +4 -0
  63. package/dist/req/validate-content-type.d.ts +1 -0
  64. package/dist/req/validate-content-type.js +32 -0
  65. package/dist/samples/create-code-samples.d.ts +20 -0
  66. package/dist/samples/create-code-samples.js +293 -0
  67. package/dist/samples/object-to-code.d.ts +8 -0
  68. package/dist/samples/object-to-code.js +38 -0
  69. package/dist/samples/schema-to-code.d.ts +11 -0
  70. package/dist/samples/schema-to-code.js +264 -0
  71. package/dist/samples/schema-to-object.d.ts +2 -0
  72. package/dist/samples/schema-to-object.js +164 -0
  73. package/dist/samples/schema-to-ts-type.d.ts +2 -0
  74. package/dist/samples/schema-to-ts-type.js +114 -0
  75. package/dist/tools/create-tool-factory.d.ts +135 -0
  76. package/dist/tools/create-tool-factory.js +62 -0
  77. package/dist/tools/create-tool.d.ts +126 -0
  78. package/dist/tools/create-tool.js +6 -0
  79. package/dist/tools/derive-tools.d.ts +46 -0
  80. package/dist/tools/derive-tools.js +131 -0
  81. package/dist/tools/to-model-output-default.d.ts +7 -0
  82. package/dist/tools/to-model-output-default.js +7 -0
  83. package/dist/tools/to-model-output-mcp.d.ts +30 -0
  84. package/dist/tools/to-model-output-mcp.js +54 -0
  85. package/dist/tools/to-model-output.d.ts +8 -0
  86. package/dist/tools/to-model-output.js +10 -0
  87. package/dist/types/client.d.ts +3 -3
  88. package/dist/types/core.d.ts +1 -1
  89. package/dist/types/inference.d.ts +1 -1
  90. package/dist/types/validation.d.ts +1 -1
  91. package/dist/utils/camel-case.d.ts +6 -0
  92. package/dist/utils/camel-case.js +34 -0
  93. package/dist/utils/deep-extend.d.ts +54 -0
  94. package/dist/utils/deep-extend.js +127 -0
  95. package/dist/utils/file-name-to-disposition.d.ts +1 -0
  96. package/dist/utils/file-name-to-disposition.js +3 -0
  97. package/dist/utils/to-kebab-case.d.ts +1 -0
  98. package/dist/utils/to-kebab-case.js +5 -0
  99. package/dist/utils/trim-path.d.ts +1 -0
  100. package/dist/utils/trim-path.js +1 -0
  101. package/dist/utils/upper-first.d.ts +1 -0
  102. package/dist/utils/upper-first.js +3 -0
  103. package/dist/validation/create-standard-validation.d.ts +268 -0
  104. package/dist/validation/create-standard-validation.js +45 -0
  105. package/dist/validation/create-validate-on-client.d.ts +14 -0
  106. package/dist/validation/create-validate-on-client.js +23 -0
  107. package/dist/validation/procedure.d.ts +24 -24
  108. package/dist/validation/procedure.js +1 -1
  109. package/dist/validation/validation-schemas-object-to-single-validation-schema.d.ts +17 -0
  110. package/dist/validation/validation-schemas-object-to-single-validation-schema.js +92 -0
  111. package/dist/validation/with-validation-library.d.ts +119 -0
  112. package/dist/validation/with-validation-library.js +184 -0
  113. package/package.json +13 -5
@@ -0,0 +1,4 @@
1
+ /** Application MIME types that are parsed as text by parseBody. */
2
+ export declare const textTypes: readonly ["application/xml", "application/xhtml+xml", "application/javascript", "application/x-javascript", "application/ecmascript", "application/yaml", "application/x-yaml", "application/graphql", "application/sql", "application/toml", "application/x-ndjson", "application/ndjson", "application/jsonl", "application/jsonlines", "application/x-jsonlines"];
3
+ export declare const textSuffixPattern: RegExp;
4
+ export declare function parseBody(req: Request): Promise<Record<string, unknown> | FormData | URLSearchParams | string | File>;
@@ -0,0 +1,49 @@
1
+ import { parseForm } from './parse-form.js';
2
+ const formTypes = ['multipart/form-data', 'application/x-www-form-urlencoded'];
3
+ /** Application MIME types that are parsed as text by parseBody. */
4
+ export const textTypes = [
5
+ 'application/xml',
6
+ 'application/xhtml+xml',
7
+ 'application/javascript',
8
+ 'application/x-javascript',
9
+ 'application/ecmascript',
10
+ 'application/yaml',
11
+ 'application/x-yaml',
12
+ 'application/graphql',
13
+ 'application/sql',
14
+ 'application/toml',
15
+ 'application/x-ndjson',
16
+ 'application/ndjson',
17
+ 'application/jsonl',
18
+ 'application/jsonlines',
19
+ 'application/x-jsonlines',
20
+ ];
21
+ export const textSuffixPattern = /\+(xml|text|yaml|json-seq)\b/;
22
+ const includes = (ct, types) => types.some((t) => ct.includes(t));
23
+ export async function parseBody(req) {
24
+ const contentType = req.headers?.get('content-type');
25
+ // application/json or +json suffix types (e.g. application/ld+json, application/vnd.api+json) → object
26
+ if (!contentType || contentType.includes('application/json') || contentType.includes('+json')) {
27
+ const body = await req.json();
28
+ req.json = () => Promise.resolve(body);
29
+ return body;
30
+ }
31
+ // multipart/form-data → FormData
32
+ if (includes(contentType, formTypes)) {
33
+ const body = await req.formData();
34
+ req.formData = () => Promise.resolve(body);
35
+ return parseForm(body);
36
+ }
37
+ // text/* or known text-based application types → string
38
+ if (contentType.startsWith('text/') || includes(contentType, textTypes) || textSuffixPattern.test(contentType)) {
39
+ const body = await req.text();
40
+ req.text = () => Promise.resolve(body);
41
+ return body;
42
+ }
43
+ // Everything else (octet-stream, image/*, video/*, application/pdf, etc.) → File
44
+ const disposition = req.headers?.get('content-disposition');
45
+ const fileName = disposition?.match(/filename="(.+?)"/)?.[1] ?? 'file';
46
+ const body = await req.blob();
47
+ req.blob = () => Promise.resolve(body);
48
+ return new File([body], fileName, { type: contentType });
49
+ }
@@ -0,0 +1 @@
1
+ export declare function parseForm<T>(body: FormData): Promise<T>;
@@ -0,0 +1,24 @@
1
+ export async function parseForm(body) {
2
+ const formData = {};
3
+ for (const [key, value] of body.entries()) {
4
+ const existing = formData[key];
5
+ if (value instanceof File) {
6
+ if (existing) {
7
+ formData[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
8
+ }
9
+ else {
10
+ formData[key] = value;
11
+ }
12
+ }
13
+ else {
14
+ const str = value.toString();
15
+ if (existing) {
16
+ formData[key] = Array.isArray(existing) ? [...existing, str] : [existing, str];
17
+ }
18
+ else {
19
+ formData[key] = str;
20
+ }
21
+ }
22
+ }
23
+ return formData;
24
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Deserialize a bracket-based query string into an object.
3
+ *
4
+ * Supports:
5
+ * - Key/value pairs with nested brackets (e.g. "a[b][0]=value")
6
+ * - Arrays with empty bracket (e.g. "arr[]=1&arr[]=2")
7
+ * - Mixed arrays of objects, etc.
8
+ *
9
+ * @example
10
+ * parseQuery("x=xx&y[0]=yy&y[1]=uu&z[f]=x&z[u][0]=uu&z[u][1]=xx&z[d][x]=ee")
11
+ * => {
12
+ * x: "xx",
13
+ * y: ["yy", "uu"],
14
+ * z: {
15
+ * f: "x",
16
+ * u: ["uu", "xx"],
17
+ * d: { x: "ee" }
18
+ * }
19
+ * }
20
+ *
21
+ * @param queryString - The raw query string (e.g. location.search.slice(1))
22
+ * @returns - A nested object representing the query params
23
+ */
24
+ export declare function parseQuery(queryString: string): Record<string, unknown>;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Parse a bracket-based key (e.g. "z[d][0][x]" or "arr[]")
3
+ * into an array of path segments (strings or special push-markers).
4
+ *
5
+ * Example: "z[d][0][x]" => ["z", "d", "0", "x"]
6
+ * Example: "arr[]" => ["arr", "" ] // "" indicates "push" onto array
7
+ */
8
+ function parseKey(key) {
9
+ // The first segment is everything up to the first '[' (or the entire key if no '[')
10
+ const segments = [];
11
+ const topKeyMatch = key.match(/^([^[\]]+)/);
12
+ if (topKeyMatch) {
13
+ segments.push(topKeyMatch[1]);
14
+ }
15
+ else {
16
+ // If it starts with brackets, treat it as empty? (edge case)
17
+ segments.push('');
18
+ }
19
+ // Now capture all bracket parts: [something], [0], []
20
+ const bracketRegex = /\[([^[\]]*)\]/g;
21
+ let match;
22
+ while (true) {
23
+ match = bracketRegex.exec(key);
24
+ if (match === null)
25
+ break;
26
+ // match[1] is the content inside the brackets
27
+ segments.push(match[1]);
28
+ }
29
+ return segments;
30
+ }
31
+ /**
32
+ * Recursively set a value in a nested object/array, given a path of segments.
33
+ * - If segment is numeric => treat as array index
34
+ * - If segment is empty "" => push to array
35
+ * - Else => object property
36
+ */
37
+ function setValue(obj, path, value) {
38
+ let current = obj;
39
+ for (let i = 0; i < path.length; i++) {
40
+ const segment = path[i];
41
+ // If we're at the last segment, set the value
42
+ if (i === path.length - 1) {
43
+ if (segment === '') {
44
+ // Empty bracket => push
45
+ if (!Array.isArray(current)) {
46
+ current = [];
47
+ }
48
+ current.push(value);
49
+ }
50
+ else if (!Number.isNaN(Number(segment))) {
51
+ // Numeric segment => array index
52
+ const idx = Number(segment);
53
+ if (!Array.isArray(current)) {
54
+ current = [];
55
+ }
56
+ current[idx] = value;
57
+ }
58
+ else {
59
+ // Object property
60
+ current[segment] = value;
61
+ }
62
+ }
63
+ else {
64
+ // Not the last segment: descend into existing structure or create it
65
+ const nextSegment = path[i + 1];
66
+ if (segment === '') {
67
+ // Empty bracket => push
68
+ if (!Array.isArray(current)) {
69
+ // Convert the current node into an array, if not one
70
+ current = [];
71
+ }
72
+ // If we are not at the last path, we need a placeholder object or array
73
+ // for the next segment. We'll push something and move current to that.
74
+ if (current.length === 0) {
75
+ // nothing in array yet
76
+ current.push(typeof nextSegment === 'string' && !Number.isNaN(Number(nextSegment)) ? [] : {});
77
+ }
78
+ else if (typeof nextSegment === 'string' && !Number.isNaN(Number(nextSegment))) {
79
+ // next is numeric => we want an array
80
+ if (!Array.isArray(current[current.length - 1])) {
81
+ current[current.length - 1] = [];
82
+ }
83
+ }
84
+ else {
85
+ // next is not numeric => we want an object
86
+ if (typeof current[current.length - 1] !== 'object') {
87
+ current[current.length - 1] = {};
88
+ }
89
+ }
90
+ current = current[current.length - 1];
91
+ }
92
+ else if (!Number.isNaN(Number(segment))) {
93
+ // segment is numeric => array index
94
+ const idx = Number(segment);
95
+ if (!Array.isArray(current)) {
96
+ current = [];
97
+ }
98
+ if (current[idx] === undefined) {
99
+ // Create placeholder for next segment
100
+ current[idx] = typeof nextSegment === 'string' && !Number.isNaN(Number(nextSegment)) ? [] : {};
101
+ }
102
+ current = current[idx];
103
+ }
104
+ else {
105
+ // segment is an object key
106
+ if (current[segment] === undefined) {
107
+ // Create placeholder
108
+ current[segment] = typeof nextSegment === 'string' && !Number.isNaN(Number(nextSegment)) ? [] : {};
109
+ }
110
+ current = current[segment];
111
+ }
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * Deserialize a bracket-based query string into an object.
117
+ *
118
+ * Supports:
119
+ * - Key/value pairs with nested brackets (e.g. "a[b][0]=value")
120
+ * - Arrays with empty bracket (e.g. "arr[]=1&arr[]=2")
121
+ * - Mixed arrays of objects, etc.
122
+ *
123
+ * @example
124
+ * parseQuery("x=xx&y[0]=yy&y[1]=uu&z[f]=x&z[u][0]=uu&z[u][1]=xx&z[d][x]=ee")
125
+ * => {
126
+ * x: "xx",
127
+ * y: ["yy", "uu"],
128
+ * z: {
129
+ * f: "x",
130
+ * u: ["uu", "xx"],
131
+ * d: { x: "ee" }
132
+ * }
133
+ * }
134
+ *
135
+ * @param queryString - The raw query string (e.g. location.search.slice(1))
136
+ * @returns - A nested object representing the query params
137
+ */
138
+ export function parseQuery(queryString) {
139
+ const result = {};
140
+ if (!queryString)
141
+ return result;
142
+ // Split into key=value pairs
143
+ const pairs = queryString
144
+ .replace(/^\?/, '') // Remove leading "?" if present
145
+ .split('&');
146
+ for (const pair of pairs) {
147
+ const [rawKey, rawVal = ''] = pair.split('=');
148
+ const decodedKey = decodeURIComponent(rawKey);
149
+ const decodedVal = decodeURIComponent(rawVal);
150
+ // Parse bracket notation
151
+ const pathSegments = parseKey(decodedKey);
152
+ // Insert into the result object
153
+ setValue(result, pathSegments, decodedVal);
154
+ }
155
+ return result;
156
+ }
@@ -0,0 +1,2 @@
1
+ import type { VovkRequest } from '../types/request.js';
2
+ export declare function reqMeta<T = Record<'mcpOutput' | 'xMetaHeader' | (string & {}), unknown>>(req: Partial<VovkRequest>, meta?: T | null): T;
@@ -0,0 +1,10 @@
1
+ const metaMap = new WeakMap();
2
+ export function reqMeta(req, meta) {
3
+ if (meta) {
4
+ metaMap.set(req, { ...metaMap.get(req), ...meta });
5
+ }
6
+ else if (meta === null) {
7
+ metaMap.delete(req);
8
+ }
9
+ return (metaMap.get(req) ?? {});
10
+ }
@@ -0,0 +1,2 @@
1
+ import type { VovkRequest } from '../types/request.js';
2
+ export declare function reqQuery<T extends object | undefined>(req: VovkRequest<unknown, T>): T;
@@ -0,0 +1,4 @@
1
+ import { parseQuery } from './parse-query.js';
2
+ export function reqQuery(req) {
3
+ return parseQuery(req.nextUrl.search);
4
+ }
@@ -0,0 +1 @@
1
+ export declare function validateContentType(request: Request | undefined, allowed: string[]): Response | null;
@@ -0,0 +1,32 @@
1
+ import { HttpException } from '../core/http-exception.js';
2
+ import { HttpStatus } from '../types/enums.js';
3
+ export function validateContentType(request, allowed) {
4
+ // Wildcard — skip validation
5
+ if (!request?.headers || allowed.includes('*/*'))
6
+ return null;
7
+ const raw = request.headers.get('content-type');
8
+ if (!raw) {
9
+ throw new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE, 'Missing Content-Type header', { allowed });
10
+ }
11
+ // Handle comma-separated content types and strip parameters like charset, boundary
12
+ const contentTypes = raw
13
+ .split(',')
14
+ .map((part) => part.split(';')[0].trim().toLowerCase())
15
+ .filter(Boolean);
16
+ const match = contentTypes.some((contentType) => allowed.some((pattern) => {
17
+ const normalized = pattern.toLowerCase();
18
+ // Partial wildcard: image/*, text/*, etc.
19
+ if (normalized.endsWith('/*')) {
20
+ const prefix = normalized.slice(0, -1);
21
+ return contentType.startsWith(prefix);
22
+ }
23
+ return contentType === normalized;
24
+ }));
25
+ if (!match) {
26
+ throw new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE, `Unsupported media type: ${contentTypes.join(', ')}`, {
27
+ contentTypes,
28
+ allowed,
29
+ });
30
+ }
31
+ return null;
32
+ }
@@ -0,0 +1,20 @@
1
+ import type { VovkControllerSchema, VovkHandlerSchema } from '../types/core.js';
2
+ import type { VovkSamplesConfig } from '../types/config.js';
3
+ export type CodeSamplePackageJson = {
4
+ name?: string;
5
+ version?: string;
6
+ description?: string;
7
+ rs_name?: string;
8
+ py_name?: string;
9
+ };
10
+ export declare function createCodeSamples({ handlerName, handlerSchema, controllerSchema, package: packageJson, config, }: {
11
+ handlerName: string;
12
+ handlerSchema: VovkHandlerSchema;
13
+ controllerSchema: VovkControllerSchema;
14
+ package?: CodeSamplePackageJson;
15
+ config: VovkSamplesConfig;
16
+ }): {
17
+ ts: string;
18
+ py: string;
19
+ rs: string;
20
+ };
@@ -0,0 +1,293 @@
1
+ import { schemaToCode, getSampleValue } from './schema-to-code.js';
2
+ import { objectToCode } from './object-to-code.js';
3
+ const toSnakeCase = (str) => str
4
+ .replace(/-/g, '_') // Replace hyphens with underscores
5
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2') // Add underscore between lowercase/digit and uppercase
6
+ .replace(/([A-Z])([A-Z])(?=[a-z])/g, '$1_$2') // Add underscore between uppercase letters if the second one is followed by a lowercase
7
+ .toLowerCase()
8
+ .replace(/^_/, ''); // Remove leading underscore
9
+ const getIndentSpaces = (level) => ' '.repeat(level);
10
+ function isTextFormat(mimeType) {
11
+ if (!mimeType)
12
+ return false;
13
+ return (mimeType.startsWith('text/') ||
14
+ [
15
+ 'application/json',
16
+ 'application/ld+json',
17
+ 'application/xml',
18
+ 'application/xhtml+xml',
19
+ 'application/javascript',
20
+ 'application/typescript',
21
+ 'application/yaml',
22
+ 'application/x-yaml',
23
+ 'application/toml',
24
+ 'application/sql',
25
+ 'application/graphql',
26
+ 'application/x-www-form-urlencoded',
27
+ ].includes(mimeType) ||
28
+ mimeType.endsWith('+json') ||
29
+ mimeType.endsWith('+xml'));
30
+ }
31
+ const isForm = (schema) => {
32
+ const contentTypes = schema['x-contentType'] ?? [];
33
+ return contentTypes.some((ct) => ct === 'multipart/form-data' || ct === 'application/x-www-form-urlencoded') ?? false;
34
+ };
35
+ function generateTypeScriptCode({ handlerName, rpcName, packageName, queryValidation, bodyValidation, paramsValidation, outputValidation, iterationValidation, hasArg, config, }) {
36
+ const getTsSample = (schema, indent) => schemaToCode(schema, { stripQuotes: true, indent: indent ?? 4 });
37
+ const getTsFormSample = (schema) => {
38
+ let formSample = '\nconst formData = new FormData();';
39
+ for (const [key, prop] of Object.entries(schema.properties || {})) {
40
+ const target = prop.oneOf?.[0] || prop.anyOf?.[0] || prop.allOf?.[0] || prop;
41
+ const desc = target.description ?? prop.description ?? undefined;
42
+ if (target.type === 'array' && target.items && typeof target.items !== 'boolean') {
43
+ formSample += getTsFormAppend(target.items, key, desc);
44
+ formSample += getTsFormAppend(target.items, key, desc);
45
+ }
46
+ else {
47
+ formSample += getTsFormAppend(target, key, desc);
48
+ }
49
+ }
50
+ return formSample;
51
+ };
52
+ const getTsFormAppend = (schema, key, description) => {
53
+ let sampleValue;
54
+ if (schema.type === 'string' && schema.format === 'binary') {
55
+ sampleValue = `new Blob(${isTextFormat(schema.contentMediaType) ? '["text_content"]' : '[binary_data]'}${schema.contentMediaType ? `, { type: "${schema.contentMediaType}" }` : ''})`;
56
+ }
57
+ else if (schema.type === 'object') {
58
+ sampleValue = '"object_unknown"';
59
+ }
60
+ else {
61
+ sampleValue = `"${getSampleValue(schema)}"`;
62
+ }
63
+ const desc = schema.description ?? description;
64
+ return `\n${desc ? `// ${desc}\n` : ''}formData.append("${key}", ${sampleValue});`;
65
+ };
66
+ const tsArgs = hasArg
67
+ ? `{
68
+ ${[
69
+ bodyValidation ? ` body: ${isForm(bodyValidation) ? 'formData' : getTsSample(bodyValidation)},` : null,
70
+ queryValidation ? ` query: ${getTsSample(queryValidation)},` : null,
71
+ paramsValidation ? ` params: ${getTsSample(paramsValidation)},` : null,
72
+ config?.apiRoot ? ` apiRoot: '${config.apiRoot}',` : null,
73
+ config?.headers
74
+ ? ` init: {
75
+ headers: ${objectToCode(config.headers, { stripQuotes: true, indent: 6, nestingIndent: 4 })}
76
+ },`
77
+ : null,
78
+ ]
79
+ .filter(Boolean)
80
+ .join('\n')}
81
+ }`
82
+ : '';
83
+ const TS_CODE = `import { ${rpcName} } from '${packageName}';
84
+ ${bodyValidation && isForm(bodyValidation) ? `${getTsFormSample(bodyValidation)}\n` : ''}
85
+ ${iterationValidation ? 'using' : 'const'} response = await ${rpcName}.${handlerName}(${tsArgs});
86
+ ${outputValidation
87
+ ? `
88
+ console.log(response);
89
+ /*
90
+ ${getTsSample(outputValidation, 0)}
91
+ */`
92
+ : ''}${iterationValidation
93
+ ? `
94
+ for await (const item of response) {
95
+ console.log(item);
96
+ /*
97
+ ${getTsSample(iterationValidation)}
98
+ */
99
+ }`
100
+ : ''}`;
101
+ return TS_CODE.trim();
102
+ }
103
+ function generatePythonCode({ handlerName, rpcName, packageName, queryValidation, bodyValidation, paramsValidation, outputValidation, iterationValidation, hasArg, config, }) {
104
+ const getPySample = (schema, indent) => schemaToCode(schema, {
105
+ stripQuotes: false,
106
+ indent: indent ?? 4,
107
+ comment: '#',
108
+ ignoreBinary: true,
109
+ nestingIndent: 4,
110
+ });
111
+ const handlerNameSnake = toSnakeCase(handlerName);
112
+ const getFileTouple = (schema) => {
113
+ return `('name.ext', BytesIO(${isTextFormat(schema.contentMediaType) ? '"text_content".encode("utf-8")' : 'binary_data'})${schema.contentMediaType ? `, "${schema.contentMediaType}"` : ''})`;
114
+ };
115
+ const getPyFiles = (schema) => {
116
+ return Object.entries(schema.properties ?? {}).reduce((acc, [key, prop]) => {
117
+ const target = prop.oneOf?.[0] || prop.anyOf?.[0] || prop.allOf?.[0] || prop;
118
+ const desc = target.description ?? prop.description ?? undefined;
119
+ if (target.type === 'string' && target.format === 'binary') {
120
+ acc.push(`${desc ? `${getIndentSpaces(8)}# ${desc}\n` : ''}${getIndentSpaces(8)}('${key}', ${getFileTouple(target)})`);
121
+ }
122
+ else if (target.type === 'array' &&
123
+ target.items &&
124
+ typeof target.items !== 'boolean' &&
125
+ target.items.format === 'binary') {
126
+ const val = `${desc ? `${getIndentSpaces(8)}# ${desc}\n` : ''}${getIndentSpaces(8)}('${key}', ${getFileTouple(target.items)})`;
127
+ acc.push(val, val);
128
+ }
129
+ return acc;
130
+ }, []);
131
+ };
132
+ const pyFiles = bodyValidation ? getPyFiles(bodyValidation) : null;
133
+ const pyFilesArg = pyFiles?.length
134
+ ? `${getIndentSpaces(4)}files=[\n${pyFiles.join(',\n')}\n${getIndentSpaces(4)}],`
135
+ : null;
136
+ const PY_CODE = `from ${packageName} import ${rpcName}
137
+ ${bodyValidation && isForm(bodyValidation) ? 'from io import BytesIO\n' : ''}
138
+ response = ${rpcName}.${handlerNameSnake}(${hasArg
139
+ ? '\n' +
140
+ [
141
+ bodyValidation ? ` body=${getPySample(bodyValidation)},` : null,
142
+ pyFilesArg,
143
+ queryValidation ? ` query=${getPySample(queryValidation)},` : null,
144
+ paramsValidation ? ` params=${getPySample(paramsValidation)},` : null,
145
+ config?.apiRoot ? ` api_root="${config.apiRoot}",` : null,
146
+ config?.headers
147
+ ? ` headers=${objectToCode(config.headers, { stripQuotes: false, indent: 4, nestingIndent: 4 })},`
148
+ : null,
149
+ ]
150
+ .filter(Boolean)
151
+ .join('\n') +
152
+ '\n'
153
+ : ''})
154
+
155
+ ${outputValidation ? `print(response)\n${getPySample(outputValidation, 0)}` : ''}${iterationValidation
156
+ ? `for i, item in enumerate(response):
157
+ print(f"iteration #{i}:\\n {item}")
158
+ # iteration #0:
159
+ ${getPySample(iterationValidation)}`
160
+ : ''}`;
161
+ return PY_CODE.trim();
162
+ }
163
+ function generateRustCode({ handlerName, rpcName, packageName, queryValidation, bodyValidation, paramsValidation, outputValidation, iterationValidation, config, }) {
164
+ const getRsJSONSample = (schema, indent) => schemaToCode(schema, { stripQuotes: false, indent: indent ?? 4 });
165
+ const getRsOutputSample = (schema, indent) => schemaToCode(schema, { stripQuotes: true, indent: indent ?? 4 });
166
+ const getRsFormSample = (schema) => {
167
+ let formSample = 'let form = reqwest::multipart::Form::new()';
168
+ for (const [key, prop] of Object.entries(schema.properties || {})) {
169
+ const target = prop.oneOf?.[0] || prop.anyOf?.[0] || prop.allOf?.[0] || prop;
170
+ const desc = target.description ?? prop.description ?? undefined;
171
+ if (target.type === 'array' && target.items && typeof target.items !== 'boolean') {
172
+ formSample += getRsFormPart(target.items, key, desc);
173
+ formSample += getRsFormPart(target.items, key, desc);
174
+ }
175
+ else {
176
+ formSample += getRsFormPart(target, key, desc);
177
+ }
178
+ }
179
+ return formSample;
180
+ };
181
+ const getRsFormPart = (schema, key, description) => {
182
+ let sampleValue;
183
+ if (schema.type === 'string' && schema.format === 'binary') {
184
+ sampleValue = isTextFormat(schema.contentMediaType)
185
+ ? 'reqwest::multipart::Part::text("text_content")'
186
+ : 'reqwest::multipart::Part::bytes(binary_data)';
187
+ if (schema.contentMediaType) {
188
+ sampleValue += `.mime_str("${schema.contentMediaType}").unwrap()`;
189
+ }
190
+ }
191
+ else if (schema.type === 'object') {
192
+ sampleValue = '"object_unknown"';
193
+ }
194
+ else {
195
+ sampleValue = `"${getSampleValue(schema)}"`;
196
+ }
197
+ const desc = schema.description ?? description;
198
+ return `\n${getIndentSpaces(4)}${desc ? `// ${desc}\n` : ''}${getIndentSpaces(4)}.part("${key}", ${sampleValue});`;
199
+ };
200
+ const getHashMapSample = (map, indent = 4) => {
201
+ const entries = Object.entries(map)
202
+ .map(([key, value]) => {
203
+ return `${getIndentSpaces(indent + 2)}("${key}".to_string(), "${value}".to_string())`;
204
+ })
205
+ .join(',\n');
206
+ return `Some(&HashMap::from([\n${entries}\n${getIndentSpaces(4)}]))`;
207
+ };
208
+ const getBody = (schema) => {
209
+ if (isForm(schema)) {
210
+ return 'form';
211
+ }
212
+ return serdeUnwrap(getRsJSONSample(schema));
213
+ };
214
+ const handlerNameSnake = toSnakeCase(handlerName);
215
+ const rpcNameSnake = toSnakeCase(rpcName);
216
+ const serdeUnwrap = (fake) => `from_value(json!(${fake})).unwrap()`;
217
+ const RS_CODE = `use ${packageName}::${rpcNameSnake};
218
+ use serde_json::{
219
+ from_value,
220
+ json
221
+ };
222
+ ${iterationValidation ? 'use futures_util::StreamExt;\n' : ''}${bodyValidation && isForm(bodyValidation) ? `use reqwest::multipart;\n` : ''}#[tokio::main]
223
+ async fn main() {${bodyValidation && isForm(bodyValidation) ? `\n ${getRsFormSample(bodyValidation)}\n` : ''}
224
+ let response = ${rpcNameSnake}::${handlerNameSnake}(
225
+ ${bodyValidation ? getBody(bodyValidation) : '()'}, /* body */
226
+ ${queryValidation ? serdeUnwrap(getRsJSONSample(queryValidation)) : '()'}, /* query */
227
+ ${paramsValidation ? serdeUnwrap(getRsJSONSample(paramsValidation)) : '()'}, /* params */
228
+ ${config?.headers ? `${getHashMapSample(config.headers)}, /* headers */` : 'None, /* headers (HashMap) */ '}
229
+ ${config?.apiRoot ? `Some("${config.apiRoot}"), /* api_root */` : 'None, /* api_root */'}
230
+ false, /* disable_client_validation */
231
+ ).await;${outputValidation
232
+ ? `\n\nmatch response {
233
+ Ok(output) => println!("{:?}", output),
234
+ /*
235
+ output ${getRsOutputSample(outputValidation)}
236
+ */
237
+ Err(e) => println!("error: {:?}", e),
238
+ }`
239
+ : ''}${iterationValidation
240
+ ? `\n\nmatch response {
241
+ Ok(mut stream) => {
242
+ let mut i = 0;
243
+ while let Some(item) = stream.next().await {
244
+ match item {
245
+ Ok(value) => {
246
+ println!("#{}: {:?}", i, value);
247
+ /*
248
+ #0: iteration ${getRsOutputSample(iterationValidation, 8)}
249
+ */
250
+ i += 1;
251
+ }
252
+ Err(e) => {
253
+ eprintln!("stream error: {:?}", e);
254
+ break;
255
+ }
256
+ }
257
+ }
258
+ },
259
+ Err(e) => println!("Error initiating stream: {:?}", e),
260
+ }`
261
+ : ''}
262
+ }`;
263
+ return RS_CODE.trim();
264
+ }
265
+ export function createCodeSamples({ handlerName, handlerSchema, controllerSchema, package: packageJson, config, }) {
266
+ const queryValidation = handlerSchema?.validation?.query;
267
+ const bodyValidation = handlerSchema?.validation?.body;
268
+ const paramsValidation = handlerSchema?.validation?.params;
269
+ const outputValidation = handlerSchema?.validation?.output;
270
+ const iterationValidation = handlerSchema?.validation?.iteration;
271
+ const hasArg = !!queryValidation || !!bodyValidation || !!paramsValidation || !!config?.apiRoot || !!config?.headers;
272
+ const rpcName = controllerSchema.rpcModuleName;
273
+ const packageName = packageJson?.name || 'vovk-client';
274
+ const packageNameSnake = toSnakeCase(packageName);
275
+ const pyPackageName = packageJson?.py_name ?? packageNameSnake;
276
+ const rsPackageName = packageJson?.rs_name ?? packageNameSnake;
277
+ const commonParams = {
278
+ handlerName,
279
+ rpcName,
280
+ packageName,
281
+ queryValidation,
282
+ bodyValidation,
283
+ paramsValidation,
284
+ outputValidation,
285
+ iterationValidation,
286
+ hasArg,
287
+ config,
288
+ };
289
+ const ts = generateTypeScriptCode(commonParams);
290
+ const py = generatePythonCode({ ...commonParams, packageName: pyPackageName });
291
+ const rs = generateRustCode({ ...commonParams, packageName: rsPackageName });
292
+ return { ts, py, rs };
293
+ }
@@ -0,0 +1,8 @@
1
+ interface SamplerOptions {
2
+ stripQuotes?: boolean;
3
+ indent?: number;
4
+ nestingIndent?: number;
5
+ quote?: '"' | "'";
6
+ }
7
+ export declare function objectToCode(obj: unknown, options?: SamplerOptions): string;
8
+ export {};