vovk 3.5.0 → 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.
- package/dist/client/create-rpc.d.ts +13 -0
- package/dist/client/create-rpc.js +147 -0
- package/dist/client/default-handler.d.ts +6 -0
- package/dist/client/default-handler.js +25 -0
- package/dist/client/default-stream-handler.d.ts +16 -0
- package/dist/client/default-stream-handler.js +282 -0
- package/dist/client/fetcher.d.ts +1 -1
- package/dist/client/fetcher.js +2 -2
- package/dist/client/serialize-query.d.ts +13 -0
- package/dist/client/serialize-query.js +62 -0
- package/dist/core/apply-decorator-adapter.d.ts +7 -0
- package/dist/core/apply-decorator-adapter.js +50 -0
- package/dist/core/controllers-to-static-params.d.ts +13 -0
- package/dist/core/controllers-to-static-params.js +32 -0
- package/dist/core/create-decorator.d.ts +12 -0
- package/dist/core/create-decorator.js +52 -0
- package/dist/core/decorators.js +4 -4
- package/dist/core/get-schema.d.ts +21 -0
- package/dist/core/get-schema.js +31 -0
- package/dist/core/http-exception.d.ts +16 -0
- package/dist/core/http-exception.js +26 -0
- package/dist/core/init-segment.d.ts +33 -0
- package/dist/core/init-segment.js +62 -0
- package/dist/core/json-lines-responder.d.ts +42 -0
- package/dist/core/json-lines-responder.js +94 -0
- package/dist/core/resolve-generator-config-values.d.ts +19 -0
- package/dist/core/resolve-generator-config-values.js +59 -0
- package/dist/core/set-handler-schema.d.ts +4 -0
- package/dist/core/set-handler-schema.js +12 -0
- package/dist/core/to-download-response.d.ts +11 -0
- package/dist/core/to-download-response.js +25 -0
- package/dist/core/vovk-app.d.ts +36 -0
- package/dist/core/vovk-app.js +318 -0
- package/dist/index.d.ts +10 -10
- package/dist/index.js +10 -10
- package/dist/internal.d.ts +10 -10
- package/dist/internal.js +9 -9
- package/dist/openapi/error.js +1 -1
- package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.d.ts +23 -0
- package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.js +90 -0
- package/dist/openapi/openapi-to-vovk-schema/index.d.ts +5 -0
- package/dist/openapi/openapi-to-vovk-schema/index.js +179 -0
- package/dist/openapi/openapi-to-vovk-schema/inline-refs.d.ts +9 -0
- package/dist/openapi/openapi-to-vovk-schema/inline-refs.js +99 -0
- package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.d.ts +7 -0
- package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.js +51 -0
- package/dist/openapi/operation.js +1 -1
- package/dist/openapi/tool.js +1 -1
- package/dist/openapi/vovk-schema-to-openapi.d.ts +21 -0
- package/dist/openapi/vovk-schema-to-openapi.js +250 -0
- package/dist/req/buffer-body.d.ts +1 -0
- package/dist/req/buffer-body.js +30 -0
- package/dist/req/parse-body.d.ts +4 -0
- package/dist/req/parse-body.js +49 -0
- package/dist/req/parse-form.d.ts +1 -0
- package/dist/req/parse-form.js +24 -0
- package/dist/req/parse-query.d.ts +24 -0
- package/dist/req/parse-query.js +156 -0
- package/dist/req/req-meta.d.ts +2 -0
- package/dist/req/req-meta.js +10 -0
- package/dist/req/req-query.d.ts +2 -0
- package/dist/req/req-query.js +4 -0
- package/dist/req/validate-content-type.d.ts +1 -0
- package/dist/req/validate-content-type.js +32 -0
- package/dist/samples/create-code-samples.d.ts +20 -0
- package/dist/samples/create-code-samples.js +293 -0
- package/dist/samples/object-to-code.d.ts +8 -0
- package/dist/samples/object-to-code.js +38 -0
- package/dist/samples/schema-to-code.d.ts +11 -0
- package/dist/samples/schema-to-code.js +264 -0
- package/dist/samples/schema-to-object.d.ts +2 -0
- package/dist/samples/schema-to-object.js +164 -0
- package/dist/samples/schema-to-ts-type.d.ts +2 -0
- package/dist/samples/schema-to-ts-type.js +114 -0
- package/dist/tools/create-tool-factory.d.ts +135 -0
- package/dist/tools/create-tool-factory.js +62 -0
- package/dist/tools/create-tool.d.ts +126 -0
- package/dist/tools/create-tool.js +6 -0
- package/dist/tools/derive-tools.d.ts +46 -0
- package/dist/tools/derive-tools.js +131 -0
- package/dist/tools/to-model-output-default.d.ts +7 -0
- package/dist/tools/to-model-output-default.js +7 -0
- package/dist/tools/to-model-output-mcp.d.ts +30 -0
- package/dist/tools/to-model-output-mcp.js +54 -0
- package/dist/tools/to-model-output.d.ts +8 -0
- package/dist/tools/to-model-output.js +10 -0
- package/dist/types/client.d.ts +3 -3
- package/dist/types/core.d.ts +1 -1
- package/dist/types/inference.d.ts +1 -1
- package/dist/types/validation.d.ts +1 -1
- package/dist/utils/camel-case.d.ts +6 -0
- package/dist/utils/camel-case.js +34 -0
- package/dist/utils/deep-extend.d.ts +54 -0
- package/dist/utils/deep-extend.js +127 -0
- package/dist/utils/file-name-to-disposition.d.ts +1 -0
- package/dist/utils/file-name-to-disposition.js +3 -0
- package/dist/utils/to-kebab-case.d.ts +1 -0
- package/dist/utils/to-kebab-case.js +5 -0
- package/dist/utils/trim-path.d.ts +1 -0
- package/dist/utils/trim-path.js +1 -0
- package/dist/utils/upper-first.d.ts +1 -0
- package/dist/utils/upper-first.js +3 -0
- package/dist/validation/create-standard-validation.d.ts +268 -0
- package/dist/validation/create-standard-validation.js +45 -0
- package/dist/validation/create-validate-on-client.d.ts +14 -0
- package/dist/validation/create-validate-on-client.js +23 -0
- package/dist/validation/procedure.d.ts +24 -24
- package/dist/validation/procedure.js +1 -1
- package/dist/validation/validation-schemas-object-to-single-validation-schema.d.ts +17 -0
- package/dist/validation/validation-schemas-object-to-single-validation-schema.js +92 -0
- package/dist/validation/with-validation-library.d.ts +119 -0
- package/dist/validation/with-validation-library.js +184 -0
- 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 @@
|
|
|
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
|
+
}
|