vovk-rust 0.0.1-beta.3 → 0.0.1-beta.6

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 for Next.js (beta)</strong>
11
+ <br />
12
+ <a href="https://vovk.dev/about">About Vovk.ts</a>
13
+ &nbsp;
14
+ <a href="https://vovk.dev/quick-install">Quick Start</a>
15
+ &nbsp;
16
+ <a href="https://github.com/finom/vovk">Github Repo</a>
11
17
  </p>
12
18
 
13
19
  ---
@@ -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)]
@@ -49,7 +51,7 @@ fn prepare_request<B, Q, P>(
49
51
  headers: Option<&HashMap<String, String>>,
50
52
  api_root: Option<&str>,
51
53
  disable_client_validation: bool,
52
- ) -> Result<(reqwest::blocking::RequestBuilder, String), Box<dyn Error>>
54
+ ) -> Result<(reqwest::RequestBuilder, String), Box<dyn Error + Send + Sync>>
53
55
  where
54
56
  B: Serialize + ?Sized,
55
57
  Q: Serialize + ?Sized,
@@ -230,7 +232,7 @@ where
230
232
 
231
233
  // Main request function for regular (non-streaming) responses
232
234
  #[allow(dead_code)]
233
- pub fn http_request<T, B, Q, P>(
235
+ pub async fn http_request<T, B, Q, P>(
234
236
  default_api_root: &str,
235
237
  segment_name: &str,
236
238
  controller_name: &str,
@@ -249,7 +251,6 @@ where
249
251
  Q: Serialize + ?Sized,
250
252
  P: Serialize + ?Sized,
251
253
  {
252
- // Prepare the request using the helper function
253
254
  let (request, _) = prepare_request(
254
255
  default_api_root,
255
256
  segment_name,
@@ -267,67 +268,76 @@ where
267
268
  status_code: 0,
268
269
  cause: None,
269
270
  })?;
270
-
271
- // Send the request
272
- let response = request.send().map_err(|e| HttpException {
271
+
272
+ let response = request.send().await.map_err(|e| HttpException {
273
273
  message: e.to_string(),
274
274
  status_code: 0,
275
275
  cause: None,
276
276
  })?;
277
-
278
- // Handle the response based on Content-Type
277
+
278
+ let status = response.status();
279
+ let status_code = status.as_u16() as i32;
280
+
279
281
  let content_type = response
280
282
  .headers()
281
283
  .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)
284
+ .and_then(|v| v.to_str().ok())
285
+ .unwrap_or("")
286
+ .to_string();
287
+
288
+ if content_type.contains("application/json") {
289
+ let value: Value = response.json().await.map_err(|e| HttpException {
290
+ message: e.to_string(),
291
+ status_code,
292
+ cause: None,
293
+ })?;
294
+
295
+ if status.is_client_error() || status.is_server_error() || value.get("isError").is_some() {
296
+ let message = value
297
+ .get("message")
298
+ .and_then(|m| m.as_str())
299
+ .unwrap_or("Unknown error")
300
+ .to_string();
301
+ let cause = value.get("cause").cloned();
302
+
303
+ return Err(HttpException {
304
+ message,
305
+ status_code,
306
+ cause,
307
+ });
311
308
  }
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,
309
+
310
+ serde_json::from_value::<T>(value).map_err(|e| HttpException {
311
+ message: e.to_string(),
312
+ status_code,
313
+ cause: None,
314
+ })
315
+ } else {
316
+ let text = response.text().await.map_err(|e| HttpException {
317
+ message: e.to_string(),
318
+ status_code,
319
+ cause: None,
320
+ })?;
321
+
322
+ if status.is_client_error() || status.is_server_error() {
323
+ return Err(HttpException {
324
+ message: text.clone(),
325
+ status_code,
321
326
  cause: None,
322
- })?;
323
- Ok(typed_value)
327
+ });
324
328
  }
329
+
330
+ serde_json::from_str::<T>(&text).map_err(|e| HttpException {
331
+ message: e.to_string(),
332
+ status_code,
333
+ cause: None,
334
+ })
325
335
  }
326
336
  }
327
337
 
328
338
  // Request function specifically for streaming responses
329
339
  #[allow(dead_code)]
330
- pub fn http_request_stream<T, B, Q, P>(
340
+ pub async fn http_request_stream<T, B, Q, P>(
331
341
  default_api_root: &str,
332
342
  segment_name: &str,
333
343
  controller_name: &str,
@@ -339,14 +349,13 @@ pub fn http_request_stream<T, B, Q, P>(
339
349
  headers: Option<&HashMap<String, String>>,
340
350
  api_root: Option<&str>,
341
351
  disable_client_validation: bool,
342
- ) -> Result<Box<dyn Iterator<Item = T>>, HttpException>
352
+ ) -> Result<Pin<Box<dyn Stream<Item = Result<T, HttpException>> + Send>>, HttpException>
343
353
  where
344
354
  T: DeserializeOwned + 'static,
345
355
  B: Serialize + ?Sized,
346
356
  Q: Serialize + ?Sized,
347
357
  P: Serialize + ?Sized,
348
358
  {
349
- // Prepare the request using the helper function
350
359
  let (request, _) = prepare_request(
351
360
  default_api_root,
352
361
  segment_name,
@@ -364,43 +373,86 @@ where
364
373
  status_code: 0,
365
374
  cause: None,
366
375
  })?;
367
-
368
- // Send the request
369
- let response = request.send().map_err(|e| HttpException {
376
+
377
+ let response = request.send().await.map_err(|e| HttpException {
370
378
  message: e.to_string(),
371
379
  status_code: 0,
372
380
  cause: None,
373
381
  })?;
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
- }
382
+
383
+ let status = response.status();
384
+ let status_code = status.as_u16() as i32;
385
+
386
+ if !status.is_success() {
387
+ let message = response
388
+ .text()
389
+ .await
390
+ .unwrap_or_else(|_| "Streaming request failed".to_string());
391
+
392
+ return Err(HttpException {
393
+ message,
394
+ status_code,
395
+ cause: None,
396
+ });
397
+ }
398
+
399
+ let byte_stream = response
400
+ .bytes_stream()
401
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e));
402
+
403
+ let reader = StreamReader::new(byte_stream);
404
+ let lines = FramedRead::new(reader, LinesCodec::new());
405
+
406
+ let json_stream = lines.filter_map(move |line| async move {
407
+ match line {
408
+ Ok(content) => {
409
+ let trimmed = content.trim();
410
+ if trimmed.is_empty() {
411
+ return None;
412
+ }
413
+
414
+ match serde_json::from_str::<Value>(trimmed) {
415
+ Ok(value) => Some(Ok(value)),
416
+ Err(e) => Some(Err(HttpException {
417
+ message: e.to_string(),
418
+ status_code,
419
+ cause: None,
420
+ })),
395
421
  }
396
- },
397
- Err(e) => {
398
- panic!("Error reading from stream: {}", e);
399
422
  }
423
+ Err(e) => Some(Err(HttpException {
424
+ message: e.to_string(),
425
+ status_code,
426
+ cause: None,
427
+ })),
400
428
  }
401
429
  });
402
-
403
- Ok(Box::new(typed_stream))
430
+
431
+ let typed_stream = json_stream.map(move |result| {
432
+ result.and_then(|value| {
433
+ if value.get("isError").is_some() {
434
+ let message = value["message"]
435
+ .as_str()
436
+ .unwrap_or("Unknown error")
437
+ .to_string();
438
+ let cause = value.get("cause").cloned();
439
+
440
+ Err(HttpException {
441
+ message,
442
+ status_code,
443
+ cause,
444
+ })
445
+ } else {
446
+ serde_json::from_value::<T>(value).map_err(|e| HttpException {
447
+ message: e.to_string(),
448
+ status_code,
449
+ cause: None,
450
+ })
451
+ }
452
+ })
453
+ });
454
+
455
+ Ok(Box::pin(typed_stream))
404
456
  }
405
457
 
406
458
  // Helper function to build query strings from nested JSON
@@ -442,47 +494,3 @@ fn build_query_string(data: &Value, prefix: &str) -> String {
442
494
  }
443
495
  }
444
496
 
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
- }
@@ -15,7 +15,11 @@ pub use crate::http_request::HttpException;
15
15
  pub mod <%= t._.snakeCase(controllerSchema.rpcModuleName) %> {
16
16
  #[allow(unused_imports)]
17
17
  use crate::http_request::{HttpException, http_request, http_request_stream};
18
+ #[allow(unused_imports)]
19
+ use futures_util::Stream;
18
20
  use std::collections::HashMap;
21
+ #[allow(unused_imports)]
22
+ use std::pin::Pin;
19
23
  <% Object.entries(controllerSchema.handlers).forEach(([handlerNameOriginal, handlerSchema]) => {
20
24
  const { validation, openapi, path, httpMethod } = handlerSchema;
21
25
  const handlerName = t._.snakeCase(handlerNameOriginal);
@@ -42,14 +46,14 @@ vars.convertJSONSchemasToRustTypes({
42
46
  validation?.query?.description ? `Query: ${validation?.query?.description}`: '',
43
47
  validation?.output?.description ? `Returns: ${validation?.output?.description}`: ''
44
48
  ]).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`): '()' %>,
49
+ pub async fn <%= handlerName %>(
50
+ body: <%- validation?.body ? (validation?.body?.['x-isForm'] ?'reqwest::multipart::Form' : `${handlerName}_::body`): '()' %>,
47
51
  query: <%- validation?.query ? `${handlerName}_::query` : '()' %>,
48
52
  params: <%- validation?.params ? `${handlerName}_::params` : '()' %>,
49
53
  headers: Option<&HashMap<String, String>>,
50
54
  api_root: Option<&str>,
51
55
  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>' %>{
56
+ ) -> <%- 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
57
  let result = <%= validation?.iteration ? 'http_request_stream' : 'http_request' %>::<
54
58
  <%- [
55
59
  validation?.output ? `${handlerName}_::output` : validation?.iteration ? `${handlerName}_::iteration` : 'serde_json::Value',
@@ -69,7 +73,7 @@ vars.convertJSONSchemasToRustTypes({
69
73
  headers,
70
74
  api_root,
71
75
  disable_client_validation,
72
- );
76
+ ).await;
73
77
 
74
78
  result
75
79
  }
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
@@ -187,14 +187,9 @@ export function generateEnum(schema, name, level, pad = 0) {
187
187
  code += `${indentFn(level)}#[derive(Debug, Serialize, Deserialize, Clone)]\n`;
188
188
  code += `${indentFn(level)}#[allow(non_camel_case_types)]\n`;
189
189
  code += `${indentFn(level)}pub enum ${name} {\n`;
190
- schema.enum.forEach((value, index) => {
190
+ schema.enum?.forEach((value) => {
191
191
  // Create valid Rust enum variant
192
192
  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
193
  code += `${indentFn(level + 1)}#[serde(rename = "${value}")]\n`;
199
194
  code += `${indentFn(level + 1)}${variant},\n`;
200
195
  });
@@ -269,7 +264,7 @@ export function generateAllOfType(schemas, name, path, level, rootSchema, pad =
269
264
  };
270
265
  }
271
266
  if (schema.required) {
272
- mergedSchema.required = [...mergedSchema.required, ...schema.required];
267
+ mergedSchema.required = [...(mergedSchema.required ?? []), ...schema.required];
273
268
  }
274
269
  });
275
270
  // Process the merged schema as a regular object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-rust",
3
- "version": "0.0.1-beta.3",
3
+ "version": "0.0.1-beta.6",
4
4
  "description": "Vovk.ts Rust client",
5
5
  "scripts": {
6
6
  "build": "tsc",