vovk-rust 0.0.1-draft.68 → 0.0.1

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/README.md CHANGED
@@ -7,7 +7,13 @@
7
7
  </picture>
8
8
  </a>
9
9
  <br>
10
- <strong>Back-end for Next.js</strong>
10
+ <strong>Back-end Framework for Next.js App Router</strong>
11
+ <br />
12
+ <a href="https://vovk.dev/">Documentation</a>
13
+ &nbsp;&nbsp;
14
+ <a href="https://vovk.dev/quick-install">Quick Start</a>
15
+ &nbsp;&nbsp;
16
+ <a href="https://vovk.dev/performance">Performance</a>
11
17
  </p>
12
18
 
13
19
  ---
@@ -15,3 +21,21 @@
15
21
  ## vovk-python [![npm version](https://badge.fury.io/js/vovk-python.svg)](https://www.npmjs.com/package/vovk-python)
16
22
 
17
23
  Provides template files and necessary utilities to generate [Rust](https://vovk.dev/rust) client.
24
+
25
+ Install:
26
+
27
+ ```sh
28
+ npm i vovk-rust -D
29
+ ```
30
+
31
+ Generate:
32
+
33
+ ```sh
34
+ npx vovk generate --from rs --out ./rust_package
35
+ ```
36
+
37
+ Publish:
38
+
39
+ ```sh
40
+ cargo publish --manifest-path rust_package/Cargo.toml
41
+ ```
@@ -9,7 +9,10 @@ const data = {
9
9
  dependencies: {
10
10
  serde: { version: "1.0", features: ["derive"] },
11
11
  serde_json: "1.0",
12
- reqwest: { version: "0.12", features: ["blocking", "json", "multipart"] },
12
+ reqwest: { version: "0.12", features: ["json", "multipart", "stream"] },
13
+ tokio: { version: "1", features: ["macros", "rt-multi-thread", "io-util"] },
14
+ "futures-util": "0.3",
15
+ "tokio-util": { version: "0.7", features: ["codec"] },
13
16
  jsonschema: "0.17",
14
17
  urlencoding: "2.1",
15
18
  once_cell: "1.17"
@@ -1,16 +1,18 @@
1
1
  use serde::{Serialize, de::DeserializeOwned};
2
- use reqwest::blocking::Client;
3
- use reqwest::Method;
4
- use reqwest::blocking::multipart;
5
- use core::panic;
2
+ use reqwest::{Client, Method};
3
+ use reqwest::multipart;
6
4
  use std::collections::HashMap;
7
5
  use std::error::Error;
8
6
  use std::fmt;
7
+ use std::pin::Pin;
9
8
  use jsonschema::JSONSchema;
10
9
  use serde_json::Value;
11
10
  use urlencoding;
12
11
  use crate::read_full_schema;
13
12
  use once_cell::sync::Lazy;
13
+ use futures_util::{Stream, StreamExt, TryStreamExt};
14
+ use tokio_util::codec::{FramedRead, LinesCodec};
15
+ use tokio_util::io::StreamReader;
14
16
 
15
17
  // Custom error type for HTTP exceptions
16
18
  #[derive(Debug, Serialize)]
@@ -44,12 +46,14 @@ fn prepare_request<B, Q, P>(
44
46
  handler_name: &str,
45
47
  body: Option<&B>,
46
48
  form: Option<multipart::Form>,
49
+ text_body: Option<String>,
50
+ binary_body: Option<(Vec<u8>, String)>,
47
51
  query: Option<&Q>,
48
52
  params: Option<&P>,
49
53
  headers: Option<&HashMap<String, String>>,
50
54
  api_root: Option<&str>,
51
55
  disable_client_validation: bool,
52
- ) -> Result<(reqwest::blocking::RequestBuilder, String), Box<dyn Error>>
56
+ ) -> Result<(reqwest::RequestBuilder, String), Box<dyn Error + Send + Sync>>
53
57
  where
54
58
  B: Serialize + ?Sized,
55
59
  Q: Serialize + ?Sized,
@@ -109,8 +113,8 @@ where
109
113
  .transpose()
110
114
  .map_err(|e| format!("Failed to serialize params: {}", e))?;
111
115
 
112
- // Perform JSON validation if not disabled and no form data is provided
113
- if !disable_client_validation && form.is_none() {
116
+ // Perform JSON validation if not disabled and no form/text/binary data is provided
117
+ if !disable_client_validation && form.is_none() && text_body.is_none() && binary_body.is_none() {
114
118
  if let Some(body_schema) = validation.get("body") {
115
119
  if let Some(ref body_val) = body_value {
116
120
  let schema =
@@ -187,8 +191,12 @@ where
187
191
  // Set up request headers
188
192
  let mut headers_map = reqwest::header::HeaderMap::new();
189
193
  headers_map.insert("Accept", "application/jsonl, application/json".parse().unwrap());
190
- if body_value.is_some() && form.is_none() {
194
+ if body_value.is_some() && form.is_none() && text_body.is_none() && binary_body.is_none() {
191
195
  headers_map.insert("Content-Type", "application/json".parse().unwrap());
196
+ } else if text_body.is_some() {
197
+ headers_map.insert("Content-Type", "text/plain".parse().unwrap());
198
+ } else if let Some((_, ref content_type)) = binary_body {
199
+ headers_map.insert("Content-Type", content_type.parse().unwrap());
192
200
  }
193
201
 
194
202
  // Merge with user-provided headers if any
@@ -218,9 +226,13 @@ where
218
226
  let client = Client::new();
219
227
  let mut request = client.request(method, &url).headers(headers_map);
220
228
 
221
- // Apply form data or JSON body to the request
229
+ // Apply form data, text body, binary body, or JSON body to the request
222
230
  if let Some(form_data) = form {
223
231
  request = request.multipart(form_data);
232
+ } else if let Some(text) = text_body {
233
+ request = request.body(text);
234
+ } else if let Some((bytes, _)) = binary_body {
235
+ request = request.body(bytes);
224
236
  } else if let Some(body_val) = body_value {
225
237
  request = request.json(&body_val);
226
238
  }
@@ -230,13 +242,15 @@ where
230
242
 
231
243
  // Main request function for regular (non-streaming) responses
232
244
  #[allow(dead_code)]
233
- pub fn http_request<T, B, Q, P>(
245
+ pub async fn http_request<T, B, Q, P>(
234
246
  default_api_root: &str,
235
247
  segment_name: &str,
236
248
  controller_name: &str,
237
249
  handler_name: &str,
238
250
  body: Option<&B>,
239
251
  form: Option<multipart::Form>,
252
+ text_body: Option<String>,
253
+ binary_body: Option<(Vec<u8>, String)>,
240
254
  query: Option<&Q>,
241
255
  params: Option<&P>,
242
256
  headers: Option<&HashMap<String, String>>,
@@ -249,7 +263,6 @@ where
249
263
  Q: Serialize + ?Sized,
250
264
  P: Serialize + ?Sized,
251
265
  {
252
- // Prepare the request using the helper function
253
266
  let (request, _) = prepare_request(
254
267
  default_api_root,
255
268
  segment_name,
@@ -257,6 +270,8 @@ where
257
270
  handler_name,
258
271
  body,
259
272
  form,
273
+ text_body,
274
+ binary_body,
260
275
  query,
261
276
  params,
262
277
  headers,
@@ -267,86 +282,96 @@ where
267
282
  status_code: 0,
268
283
  cause: None,
269
284
  })?;
270
-
271
- // Send the request
272
- let response = request.send().map_err(|e| HttpException {
285
+
286
+ let response = request.send().await.map_err(|e| HttpException {
273
287
  message: e.to_string(),
274
288
  status_code: 0,
275
289
  cause: None,
276
290
  })?;
277
-
278
- // Handle the response based on Content-Type
291
+
292
+ let status = response.status();
293
+ let status_code = status.as_u16() as i32;
294
+
279
295
  let content_type = response
280
296
  .headers()
281
297
  .get("Content-Type")
282
- .and_then(|v| v.to_str().ok());
283
-
284
- match content_type {
285
- Some(ct) if ct.contains("application/json") => {
286
- let value: Value = response.json().map_err(|e| HttpException {
287
- message: e.to_string(),
288
- status_code: 0,
289
- cause: None,
290
- })?;
291
- if value.get("isError").is_some() {
292
- let message = value["message"]
293
- .as_str()
294
- .unwrap_or("Unknown error")
295
- .to_string();
296
- let status_code = value["statusCode"].as_i64().unwrap_or(0) as i32;
297
- let cause = value.get("cause").cloned();
298
- return Err(HttpException {
299
- message,
300
- status_code,
301
- cause,
302
- });
303
- }
304
-
305
- let typed_value = serde_json::from_value::<T>(value).map_err(|e| HttpException {
306
- message: e.to_string(),
307
- status_code: 0,
308
- cause: None,
309
- })?;
310
- Ok(typed_value)
298
+ .and_then(|v| v.to_str().ok())
299
+ .unwrap_or("")
300
+ .to_string();
301
+
302
+ if content_type.contains("application/json") {
303
+ let value: Value = response.json().await.map_err(|e| HttpException {
304
+ message: e.to_string(),
305
+ status_code,
306
+ cause: None,
307
+ })?;
308
+
309
+ if status.is_client_error() || status.is_server_error() || value.get("isError").is_some() {
310
+ let message = value
311
+ .get("message")
312
+ .and_then(|m| m.as_str())
313
+ .unwrap_or("Unknown error")
314
+ .to_string();
315
+ let cause = value.get("cause").cloned();
316
+
317
+ return Err(HttpException {
318
+ message,
319
+ status_code,
320
+ cause,
321
+ });
311
322
  }
312
- _ => {
313
- let text = response.text().map_err(|e| HttpException {
314
- message: e.to_string(),
315
- status_code: 0,
316
- cause: None,
317
- })?;
318
- let typed_value = serde_json::from_str::<T>(&text).map_err(|e| HttpException {
319
- message: e.to_string(),
320
- status_code: 0,
323
+
324
+ serde_json::from_value::<T>(value).map_err(|e| HttpException {
325
+ message: e.to_string(),
326
+ status_code,
327
+ cause: None,
328
+ })
329
+ } else {
330
+ let text = response.text().await.map_err(|e| HttpException {
331
+ message: e.to_string(),
332
+ status_code,
333
+ cause: None,
334
+ })?;
335
+
336
+ if status.is_client_error() || status.is_server_error() {
337
+ return Err(HttpException {
338
+ message: text.clone(),
339
+ status_code,
321
340
  cause: None,
322
- })?;
323
- Ok(typed_value)
341
+ });
324
342
  }
343
+
344
+ serde_json::from_str::<T>(&text).map_err(|e| HttpException {
345
+ message: e.to_string(),
346
+ status_code,
347
+ cause: None,
348
+ })
325
349
  }
326
350
  }
327
351
 
328
352
  // Request function specifically for streaming responses
329
353
  #[allow(dead_code)]
330
- pub fn http_request_stream<T, B, Q, P>(
354
+ pub async fn http_request_stream<T, B, Q, P>(
331
355
  default_api_root: &str,
332
356
  segment_name: &str,
333
357
  controller_name: &str,
334
358
  handler_name: &str,
335
359
  body: Option<&B>,
336
360
  form: Option<multipart::Form>,
361
+ text_body: Option<String>,
362
+ binary_body: Option<(Vec<u8>, String)>,
337
363
  query: Option<&Q>,
338
364
  params: Option<&P>,
339
365
  headers: Option<&HashMap<String, String>>,
340
366
  api_root: Option<&str>,
341
367
  disable_client_validation: bool,
342
- ) -> Result<Box<dyn Iterator<Item = T>>, HttpException>
368
+ ) -> Result<Pin<Box<dyn Stream<Item = Result<T, HttpException>> + Send>>, HttpException>
343
369
  where
344
370
  T: DeserializeOwned + 'static,
345
371
  B: Serialize + ?Sized,
346
372
  Q: Serialize + ?Sized,
347
373
  P: Serialize + ?Sized,
348
374
  {
349
- // Prepare the request using the helper function
350
375
  let (request, _) = prepare_request(
351
376
  default_api_root,
352
377
  segment_name,
@@ -354,6 +379,8 @@ where
354
379
  handler_name,
355
380
  body,
356
381
  form,
382
+ text_body,
383
+ binary_body,
357
384
  query,
358
385
  params,
359
386
  headers,
@@ -364,43 +391,86 @@ where
364
391
  status_code: 0,
365
392
  cause: None,
366
393
  })?;
367
-
368
- // Send the request
369
- let response = request.send().map_err(|e| HttpException {
394
+
395
+ let response = request.send().await.map_err(|e| HttpException {
370
396
  message: e.to_string(),
371
397
  status_code: 0,
372
398
  cause: None,
373
399
  })?;
374
-
375
- // Create the streaming iterator
376
- let json_stream = JsonlStream {
377
- reader: std::io::BufReader::new(response),
378
- buffer: String::new(),
379
- };
380
-
381
- let typed_stream = json_stream.map(|result| {
382
- match result {
383
- Ok(value) => {
384
- if value.get("isError").is_some() {
385
- let message = value["message"]
386
- .as_str()
387
- .unwrap_or("Unknown error")
388
- .to_string();
389
- panic!("Error from server: {}", message);
390
- } else {
391
- match serde_json::from_value::<T>(value) {
392
- Ok(typed_value) => typed_value,
393
- Err(e) => panic!("Failed to deserialize value: {}", e),
394
- }
400
+
401
+ let status = response.status();
402
+ let status_code = status.as_u16() as i32;
403
+
404
+ if !status.is_success() {
405
+ let message = response
406
+ .text()
407
+ .await
408
+ .unwrap_or_else(|_| "Streaming request failed".to_string());
409
+
410
+ return Err(HttpException {
411
+ message,
412
+ status_code,
413
+ cause: None,
414
+ });
415
+ }
416
+
417
+ let byte_stream = response
418
+ .bytes_stream()
419
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e));
420
+
421
+ let reader = StreamReader::new(byte_stream);
422
+ let lines = FramedRead::new(reader, LinesCodec::new());
423
+
424
+ let json_stream = lines.filter_map(move |line| async move {
425
+ match line {
426
+ Ok(content) => {
427
+ let trimmed = content.trim();
428
+ if trimmed.is_empty() {
429
+ return None;
430
+ }
431
+
432
+ match serde_json::from_str::<Value>(trimmed) {
433
+ Ok(value) => Some(Ok(value)),
434
+ Err(e) => Some(Err(HttpException {
435
+ message: e.to_string(),
436
+ status_code,
437
+ cause: None,
438
+ })),
395
439
  }
396
- },
397
- Err(e) => {
398
- panic!("Error reading from stream: {}", e);
399
440
  }
441
+ Err(e) => Some(Err(HttpException {
442
+ message: e.to_string(),
443
+ status_code,
444
+ cause: None,
445
+ })),
400
446
  }
401
447
  });
402
-
403
- Ok(Box::new(typed_stream))
448
+
449
+ let typed_stream = json_stream.map(move |result| {
450
+ result.and_then(|value| {
451
+ if value.get("isError").is_some() {
452
+ let message = value["message"]
453
+ .as_str()
454
+ .unwrap_or("Unknown error")
455
+ .to_string();
456
+ let cause = value.get("cause").cloned();
457
+
458
+ Err(HttpException {
459
+ message,
460
+ status_code,
461
+ cause,
462
+ })
463
+ } else {
464
+ serde_json::from_value::<T>(value).map_err(|e| HttpException {
465
+ message: e.to_string(),
466
+ status_code,
467
+ cause: None,
468
+ })
469
+ }
470
+ })
471
+ });
472
+
473
+ Ok(Box::pin(typed_stream))
404
474
  }
405
475
 
406
476
  // Helper function to build query strings from nested JSON
@@ -442,47 +512,3 @@ fn build_query_string(data: &Value, prefix: &str) -> String {
442
512
  }
443
513
  }
444
514
 
445
- // Struct and Iterator implementation for streaming JSONL responses
446
- struct JsonlStream {
447
- reader: std::io::BufReader<reqwest::blocking::Response>,
448
- buffer: String,
449
- }
450
-
451
- impl Iterator for JsonlStream {
452
- type Item = Result<Value, Box<dyn Error>>;
453
-
454
- fn next(&mut self) -> Option<Self::Item> {
455
- use std::io::BufRead;
456
-
457
- self.buffer.clear();
458
- match self.reader.read_line(&mut self.buffer) {
459
- Ok(0) => None, // End of stream
460
- Ok(_) => {
461
- let line = self.buffer.trim();
462
- if line.is_empty() {
463
- return self.next(); // Skip empty lines
464
- }
465
-
466
- match serde_json::from_str::<Value>(line) {
467
- Ok(value) => {
468
- if value.get("isError").is_some() {
469
- let message = value["message"]
470
- .as_str()
471
- .unwrap_or("Unknown error")
472
- .to_string();
473
- Some(Err(Box::new(HttpException {
474
- message,
475
- status_code: 0,
476
- cause: None,
477
- })))
478
- } else {
479
- Some(Ok(value))
480
- }
481
- },
482
- Err(e) => Some(Err(Box::new(e))),
483
- }
484
- },
485
- Err(e) => Some(Err(Box::new(e))),
486
- }
487
- }
488
- }
@@ -1,21 +1,24 @@
1
1
  ---
2
2
  imports: ['vovk-rust']
3
3
  ---
4
+ <%- t.getFirstLineBanner() %>
4
5
  <% const vars = {
5
6
  convertJSONSchemasToRustTypes: t.imports['vovk-rust'].convertJSONSchemasToRustTypes,
6
7
  }; %>
7
- <%- t.getFirstLineBanner() %>
8
8
  mod http_request;
9
9
  mod read_full_schema;
10
10
 
11
11
  pub use crate::http_request::HttpException;
12
-
13
12
  <% Object.entries(t.schema.segments).forEach(([segmentName, segment]) => {
14
13
  Object.values(segment.controllers).forEach((controllerSchema) => { %>
15
14
  pub mod <%= t._.snakeCase(controllerSchema.rpcModuleName) %> {
16
15
  #[allow(unused_imports)]
17
16
  use crate::http_request::{HttpException, http_request, http_request_stream};
17
+ #[allow(unused_imports)]
18
+ use futures_util::Stream;
18
19
  use std::collections::HashMap;
20
+ #[allow(unused_imports)]
21
+ use std::pin::Pin;
19
22
  <% Object.entries(controllerSchema.handlers).forEach(([handlerNameOriginal, handlerSchema]) => {
20
23
  const { validation, openapi, path, httpMethod } = handlerSchema;
21
24
  const handlerName = t._.snakeCase(handlerNameOriginal);
@@ -42,18 +45,30 @@ vars.convertJSONSchemasToRustTypes({
42
45
  validation?.query?.description ? `Query: ${validation?.query?.description}`: '',
43
46
  validation?.output?.description ? `Returns: ${validation?.output?.description}`: ''
44
47
  ]).filter(Boolean).map((s) => s.split('\n')).flat().map((s) => ' '.repeat(4) + '/// ' + s).join('\n') %>
45
- pub fn <%= handlerName %>(
46
- body: <%- validation?.body ? (validation?.body?.['x-isForm'] ?'reqwest::blocking::multipart::Form' : `${handlerName}_::body`): '()' %>,
48
+ <%
49
+ // Determine body kind: 'none', 'form', 'binary', 'text', or 'json'
50
+ const bodyKind = (() => {
51
+ if (!validation?.body) return 'none';
52
+ const ct = validation.body['x-contentType'];
53
+ if (ct?.includes('multipart/form-data') || ct?.includes('application/x-www-form-urlencoded')) return 'form';
54
+ if (validation.body.format === 'binary' || validation.body.contentEncoding === 'binary') return 'binary';
55
+ if (ct?.some(c => c.startsWith('text/'))) return 'text';
56
+ return 'json';
57
+ })();
58
+ const firstContentType = validation?.body?.['x-contentType']?.[0] || 'application/octet-stream';
59
+ %>
60
+ pub async fn <%= handlerName %>(
61
+ body: <%- bodyKind === 'form' ? 'reqwest::multipart::Form' : bodyKind !== 'none' ? `${handlerName}_::body` : '()' %>,
47
62
  query: <%- validation?.query ? `${handlerName}_::query` : '()' %>,
48
63
  params: <%- validation?.params ? `${handlerName}_::params` : '()' %>,
49
64
  headers: Option<&HashMap<String, String>>,
50
65
  api_root: Option<&str>,
51
66
  disable_client_validation: bool,
52
- ) -> <%- validation?.output ? `Result<${handlerName}_::output, HttpException>` : validation?.iteration ? `Result<Box<dyn Iterator<Item = ${handlerName}_::iteration>>, HttpException>` : 'Result<serde_json::Value, HttpException>' %>{
67
+ ) -> <%- validation?.output ? `Result<${handlerName}_::output, HttpException>` : validation?.iteration ? `Result<Pin<Box<dyn Stream<Item = Result<${handlerName}_::iteration, HttpException>> + Send>>, HttpException>` : 'Result<serde_json::Value, HttpException>' %>{
53
68
  let result = <%= validation?.iteration ? 'http_request_stream' : 'http_request' %>::<
54
69
  <%- [
55
70
  validation?.output ? `${handlerName}_::output` : validation?.iteration ? `${handlerName}_::iteration` : 'serde_json::Value',
56
- validation?.body && !validation?.body?.['x-isForm'] ? `${handlerName}_::body` : '()',
71
+ bodyKind === 'json' ? `${handlerName}_::body` : '()',
57
72
  validation?.query ? `${handlerName}_::query` : '()',
58
73
  validation?.params ? `${handlerName}_::params` : '()'
59
74
  ].filter(Boolean).map((s) => ' '.repeat(12) + s).join(',\n') %>
@@ -62,14 +77,16 @@ vars.convertJSONSchemasToRustTypes({
62
77
  "<%= segmentName %>",
63
78
  "<%= controllerSchema.rpcModuleName %>",
64
79
  "<%= handlerNameOriginal %>",
65
- <%- !validation?.body || !validation?.body?.['x-isForm'] ? `Some(&body)` : 'None' %>,
66
- <%- validation?.body && validation?.body?.['x-isForm'] ? `Some(body)` : 'None' %>,
80
+ <%- bodyKind === 'json' ? 'Some(&body)' : 'None' %>,
81
+ <%- bodyKind === 'form' ? 'Some(body)' : 'None' %>,
82
+ <%- bodyKind === 'text' ? 'Some(body)' : 'None' %>,
83
+ <%- bodyKind === 'binary' ? `Some((body, "${firstContentType}".to_string()))` : 'None' %>,
67
84
  Some(&query),
68
85
  Some(&params),
69
86
  headers,
70
87
  api_root,
71
88
  disable_client_validation,
72
- );
89
+ ).await;
73
90
 
74
91
  result
75
92
  }
package/index.d.ts CHANGED
@@ -1,16 +1,16 @@
1
- import type { KnownAny } from 'vovk';
1
+ import type { VovkJSONSchemaBase } from 'vovk';
2
2
  export declare function indent(level: number, pad?: number): string;
3
- export declare function generateDocComment(schema: KnownAny, level: number, pad?: number): string;
4
- export declare function resolveRef(ref: string, rootSchema: KnownAny): KnownAny | undefined;
3
+ export declare function generateDocComment(schema: VovkJSONSchemaBase, level: number, pad?: number): string;
4
+ export declare function resolveRef(ref: string, rootSchema: VovkJSONSchemaBase): VovkJSONSchemaBase | undefined;
5
5
  export declare function getModulePath(path: string[]): string;
6
- export declare function toRustType(schema: KnownAny, path: string[], rootSchema?: KnownAny): string;
7
- export declare function generateEnum(schema: KnownAny, name: string, level: number, pad?: number): string;
8
- export declare function generateVariantEnum(schema: KnownAny, name: string, path: string[], level: number, rootSchema: KnownAny, pad?: number): string;
9
- export declare function generateAllOfType(schemas: KnownAny[], name: string, path: string[], level: number, rootSchema: KnownAny, pad?: number): string;
10
- export declare function processObject(schema: KnownAny, path: string[], level: number, rootSchema?: KnownAny, pad?: number): string;
11
- export declare function processPrimitive(schema: KnownAny, name: string, level: number, pad?: number): string;
6
+ export declare function toRustType(schema: VovkJSONSchemaBase, path: string[], rootSchema?: VovkJSONSchemaBase): string;
7
+ export declare function generateEnum(schema: VovkJSONSchemaBase, name: string, level: number, pad?: number): string;
8
+ export declare function generateVariantEnum(schema: VovkJSONSchemaBase, name: string, path: string[], level: number, rootSchema: VovkJSONSchemaBase, pad?: number): string;
9
+ export declare function generateAllOfType(schemas: VovkJSONSchemaBase[], name: string, path: string[], level: number, rootSchema: VovkJSONSchemaBase, pad?: number): string;
10
+ export declare function processObject(schema: VovkJSONSchemaBase, path: string[], level: number, rootSchema?: VovkJSONSchemaBase, pad?: number): string;
11
+ export declare function processPrimitive(schema: VovkJSONSchemaBase, name: string, level: number, pad?: number): string;
12
12
  export declare function convertJSONSchemasToRustTypes({ schemas, pad, rootName, }: {
13
- schemas: Record<string, KnownAny | undefined>;
13
+ schemas: Record<string, VovkJSONSchemaBase | undefined>;
14
14
  pad?: number;
15
15
  rootName: string;
16
16
  }): string;
package/index.js CHANGED
@@ -1,3 +1,58 @@
1
+ // Rust reserved keywords that cannot be used as identifiers
2
+ const RUST_KEYWORDS = new Set([
3
+ 'as',
4
+ 'break',
5
+ 'const',
6
+ 'continue',
7
+ 'crate',
8
+ 'else',
9
+ 'enum',
10
+ 'extern',
11
+ 'false',
12
+ 'fn',
13
+ 'for',
14
+ 'if',
15
+ 'impl',
16
+ 'in',
17
+ 'let',
18
+ 'loop',
19
+ 'match',
20
+ 'mod',
21
+ 'move',
22
+ 'mut',
23
+ 'pub',
24
+ 'ref',
25
+ 'return',
26
+ 'self',
27
+ 'Self',
28
+ 'static',
29
+ 'struct',
30
+ 'super',
31
+ 'trait',
32
+ 'true',
33
+ 'type',
34
+ 'unsafe',
35
+ 'use',
36
+ 'where',
37
+ 'while',
38
+ 'async',
39
+ 'await',
40
+ 'dyn',
41
+ 'abstract',
42
+ 'become',
43
+ 'box',
44
+ 'do',
45
+ 'final',
46
+ 'macro',
47
+ 'override',
48
+ 'priv',
49
+ 'typeof',
50
+ 'unsized',
51
+ 'virtual',
52
+ 'yield',
53
+ 'try',
54
+ 'union',
55
+ ]);
1
56
  // Helper function for indentation
2
57
  export function indent(level, pad = 0) {
3
58
  return ' '.repeat(pad + level * 2);
@@ -90,6 +145,10 @@ export function toRustType(schema, path, rootSchema = schema) {
90
145
  }
91
146
  }
92
147
  if (schema.type === 'string') {
148
+ // Binary format maps to Vec<u8>
149
+ if (schema.format === 'binary' || schema.contentEncoding === 'binary') {
150
+ return 'Vec<u8>';
151
+ }
93
152
  return 'String';
94
153
  }
95
154
  else if (schema.type === 'number' || schema.type === 'integer') {
@@ -153,7 +212,7 @@ export function toRustType(schema, path, rootSchema = schema) {
153
212
  return '()';
154
213
  }
155
214
  else if (schema.type === 'array') {
156
- if (schema.items) {
215
+ if (schema.items && typeof schema.items !== 'boolean') {
157
216
  // Check if array items are objects that need special handling
158
217
  if (schema.items.type === 'object' || schema.items.properties || schema.items.$ref) {
159
218
  // For array of objects, reference the item type with proper module path
@@ -187,14 +246,9 @@ export function generateEnum(schema, name, level, pad = 0) {
187
246
  code += `${indentFn(level)}#[derive(Debug, Serialize, Deserialize, Clone)]\n`;
188
247
  code += `${indentFn(level)}#[allow(non_camel_case_types)]\n`;
189
248
  code += `${indentFn(level)}pub enum ${name} {\n`;
190
- schema.enum.forEach((value, index) => {
249
+ schema.enum?.forEach((value) => {
191
250
  // Create valid Rust enum variant
192
251
  const variant = value?.replace(/[^a-zA-Z0-9_]/g, '_');
193
- // Add documentation if available in enumDescriptions
194
- if (schema.enumDescriptions && schema.enumDescriptions[index]) {
195
- const description = schema.enumDescriptions[index];
196
- code += `${indentFn(level + 1)}/// ${description}\n`;
197
- }
198
252
  code += `${indentFn(level + 1)}#[serde(rename = "${value}")]\n`;
199
253
  code += `${indentFn(level + 1)}${variant},\n`;
200
254
  });
@@ -230,8 +284,8 @@ export function generateVariantEnum(schema, name, path, level, rootSchema, pad =
230
284
  // If it's an object type, we need to create a separate struct
231
285
  if (variant.type === 'object' || variant.properties) {
232
286
  code += `${indentFn(level + 1)}${variantName}(${name}_::${variantName}),\n`;
233
- // Create a nested type definition to be added outside the enum
234
- nestedTypes += processObject(variant, variantPath, level, rootSchema, pad);
287
+ // Create a nested type definition to be added inside a sub-module
288
+ nestedTypes += processObject(variant, variantPath, level + 1, rootSchema, pad);
235
289
  }
236
290
  else {
237
291
  // For simple types, we can include them directly in the enum
@@ -240,9 +294,13 @@ export function generateVariantEnum(schema, name, path, level, rootSchema, pad =
240
294
  }
241
295
  });
242
296
  code += `${indentFn(level)}}\n\n`;
243
- // Add nested type definitions if needed
297
+ // Add nested type definitions wrapped in a sub-module
244
298
  if (nestedTypes) {
299
+ code += `${indentFn(level)}#[allow(non_snake_case)]\n`;
300
+ code += `${indentFn(level)}pub mod ${name}_ {\n`;
301
+ code += `${indentFn(level + 1)}use serde::{Serialize, Deserialize};\n\n`;
245
302
  code += nestedTypes;
303
+ code += `${indentFn(level)}}\n`;
246
304
  }
247
305
  return code;
248
306
  }
@@ -269,7 +327,7 @@ export function generateAllOfType(schemas, name, path, level, rootSchema, pad =
269
327
  };
270
328
  }
271
329
  if (schema.required) {
272
- mergedSchema.required = [...mergedSchema.required, ...schema.required];
330
+ mergedSchema.required = [...(mergedSchema.required ?? []), ...schema.required];
273
331
  }
274
332
  });
275
333
  // Process the merged schema as a regular object
@@ -293,7 +351,9 @@ export function processObject(schema, path, level, rootSchema = schema, pad = 0)
293
351
  let code = '';
294
352
  // Add documentation comments for the struct
295
353
  code += generateDocComment(schema, level, pad);
296
- if (schema.type === 'object' && schema['x-isForm'] === true) {
354
+ if (schema.type === 'object' &&
355
+ (schema['x-contentType']?.includes('multipart/form-data') ||
356
+ schema['x-contentType']?.includes('application/x-www-form-urlencoded'))) {
297
357
  code += `${indentFn(level)}pub use reqwest::multipart::Form as ${currentName};\n`;
298
358
  return code;
299
359
  }
@@ -342,7 +402,13 @@ export function processObject(schema, path, level, rootSchema = schema, pad = 0)
342
402
  if (!isRequired) {
343
403
  propType = `Option<${propType}>`;
344
404
  }
345
- code += `${indentFn(level + 1)}pub ${propName}: ${propType},\n`;
405
+ if (RUST_KEYWORDS.has(propName)) {
406
+ code += `${indentFn(level + 1)}#[serde(rename = "${propName}")]\n`;
407
+ code += `${indentFn(level + 1)}pub r#${propName}: ${propType},\n`;
408
+ }
409
+ else {
410
+ code += `${indentFn(level + 1)}pub ${propName}: ${propType},\n`;
411
+ }
346
412
  });
347
413
  code += `${indentFn(level)}}\n\n`;
348
414
  // Check if any properties require nested types before generating the sub-module
@@ -357,6 +423,7 @@ export function processObject(schema, path, level, rootSchema = schema, pad = 0)
357
423
  ((propSchema.type === 'string' || !propSchema.type) && propSchema.enum) ||
358
424
  (propSchema.type === 'array' &&
359
425
  propSchema.items &&
426
+ typeof propSchema.items !== 'boolean' &&
360
427
  (propSchema.items.type === 'object' || propSchema.items.properties || propSchema.items.$ref)) ||
361
428
  propSchema.anyOf ||
362
429
  propSchema.oneOf ||
@@ -384,7 +451,7 @@ export function processObject(schema, path, level, rootSchema = schema, pad = 0)
384
451
  code += generateEnum(propSchema, propName, level + 1, pad);
385
452
  }
386
453
  // Generate types for array items if they're objects
387
- else if (propSchema.type === 'array' && propSchema.items) {
454
+ else if (propSchema.type === 'array' && propSchema.items && typeof propSchema.items !== 'boolean') {
388
455
  // Check if items has a $ref
389
456
  if (propSchema.items.$ref) {
390
457
  const resolved = resolveRef(propSchema.items.$ref, rootSchema);
@@ -414,7 +481,13 @@ export function processPrimitive(schema, name, level, pad = 0) {
414
481
  // For primitive types, create a type alias
415
482
  code += `${indentFn(level)}pub type ${name} = `;
416
483
  if (schema.type === 'string') {
417
- code += 'String';
484
+ // Binary format maps to Vec<u8>
485
+ if (schema.format === 'binary' || schema.contentEncoding === 'binary') {
486
+ code += 'Vec<u8>';
487
+ }
488
+ else {
489
+ code += 'String';
490
+ }
418
491
  }
419
492
  else if (schema.type === 'number') {
420
493
  code += 'f64';
@@ -446,7 +519,9 @@ export function convertJSONSchemasToRustTypes({ schemas, pad = 0, rootName, }) {
446
519
  }
447
520
  const indentFn = (level) => ' '.repeat(pad + level * 2);
448
521
  // Start code generation
449
- let result = `${indentFn(0)}pub mod ${rootName}_ {\n`;
522
+ let result = `${indentFn(0)}#[allow(non_camel_case_types)]\n`;
523
+ result += `${indentFn(0)}pub mod ${rootName}_ {\n`;
524
+ result += `${indentFn(1)}#[allow(unused_imports)]\n`;
450
525
  result += `${indentFn(1)}use serde::{Serialize, Deserialize};\n`;
451
526
  // Process each schema in the schemas object
452
527
  Object.entries(schemas).forEach(([schemaName, schemaObj]) => {
@@ -465,7 +540,7 @@ export function convertJSONSchemasToRustTypes({ schemas, pad = 0, rootName, }) {
465
540
  required: defSchema.required || [],
466
541
  title: defSchema.title,
467
542
  description: defSchema.description,
468
- 'x-isForm': defSchema['x-isForm'],
543
+ 'x-contentType': defSchema['x-contentType'],
469
544
  };
470
545
  result += processObject(rootDefObject, [defName], 1, schemaObj, pad);
471
546
  }
@@ -475,7 +550,8 @@ export function convertJSONSchemasToRustTypes({ schemas, pad = 0, rootName, }) {
475
550
  else if (defSchema.anyOf || defSchema.oneOf || defSchema.allOf) {
476
551
  result += generateVariantEnum(defSchema, defName, [defName], 1, schemaObj, pad);
477
552
  }
478
- else if (defSchema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(defSchema.type)) {
553
+ else if (typeof defSchema.type === 'string' &&
554
+ ['string', 'number', 'integer', 'boolean', 'null'].includes(defSchema.type)) {
479
555
  // Handle primitive types in $defs
480
556
  result += processPrimitive(defSchema, defName, 1, pad);
481
557
  }
@@ -491,11 +567,12 @@ export function convertJSONSchemasToRustTypes({ schemas, pad = 0, rootName, }) {
491
567
  required: schemaObj.required || [],
492
568
  title: schemaObj.title,
493
569
  description: schemaObj.description,
494
- 'x-isForm': schemaObj['x-isForm'],
570
+ 'x-contentType': schemaObj['x-contentType'],
495
571
  };
496
572
  result += processObject(rootObject, [schemaName], 1, schemaObj, pad);
497
573
  }
498
- else if (['string', 'number', 'integer', 'boolean', 'null'].includes(schemaObj.type)) {
574
+ else if (typeof schemaObj.type === 'string' &&
575
+ ['string', 'number', 'integer', 'boolean', 'null'].includes(schemaObj.type)) {
499
576
  // Handle primitive schema
500
577
  result += processPrimitive(schemaObj, schemaName, 1, pad);
501
578
  }
@@ -510,7 +587,7 @@ export function convertJSONSchemasToRustTypes({ schemas, pad = 0, rootName, }) {
510
587
  else if (schemaObj.type === 'array') {
511
588
  // For array as root type, create a type alias to Vec<ItemType>
512
589
  let itemType = 'String'; // Default if no items specified
513
- if (schemaObj.items) {
590
+ if (schemaObj.items && typeof schemaObj.items !== 'boolean') {
514
591
  if (schemaObj.items.type === 'object' || schemaObj.items.properties) {
515
592
  // Create the item type
516
593
  const itemSchema = {
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "vovk-rust",
3
- "version": "0.0.1-draft.68",
4
- "description": "Vovk.ts Rust client",
3
+ "version": "0.0.1",
4
+ "description": "Codegen templates for Rust client library for Vovk.ts",
5
+ "files": [
6
+ "client-templates",
7
+ "index.js",
8
+ "index.d.ts"
9
+ ],
5
10
  "scripts": {
6
11
  "build": "tsc",
7
12
  "npm-publish": "npm publish",
@@ -13,7 +18,7 @@
13
18
  "unit:rs": "RUST_BACKTRACE=full cargo test --manifest-path ./test_rust/Cargo.toml -- --show-output",
14
19
  "unit:ts": "npm run pre-test && node --experimental-strip-types --test --test-concurrency=1 test_ts/*.mts",
15
20
  "generate": "npm run generate --prefix ../../test -- --config=../packages/vovk-rust/vovk.config.test.mjs",
16
- "test": "npm run unit:ts && PORT=3210 concurrently 'sleep 60 && npm run unit:rs' 'npm run generate && npm run build --prefix ../../test && npm run start --prefix ../../test' --kill-others --success first"
21
+ "test": "npm run unit:ts && PORT=3210 npm run generate && npm run build --prefix ../../test && PORT=3210 concurrently 'sleep 15 && npm run unit:rs' 'npm run start --prefix ../../test' --kill-others --success first"
17
22
  },
18
23
  "main": "./index.js",
19
24
  "type": "module",
@@ -22,12 +27,14 @@
22
27
  "url": "git+https://github.com/finom/vovk.git"
23
28
  },
24
29
  "keywords": [
25
- "Vovk"
30
+ "vovk",
31
+ "rust",
32
+ "codegen"
26
33
  ],
27
34
  "author": "Andrey Gubanov",
28
35
  "license": "MIT",
29
36
  "bugs": {
30
37
  "url": "https://github.com/finom/vovk/issues"
31
38
  },
32
- "homepage": "https://github.com/finom/vovk#readme"
39
+ "homepage": "https://vovk.dev/rust"
33
40
  }