vovk-rust 0.0.1-draft.69 → 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 +25 -1
- package/client-templates/rsPkg/Cargo.toml.ejs +4 -1
- package/client-templates/rsSrc/http_request.rs +158 -132
- package/client-templates/rsSrc/lib.rs.ejs +26 -9
- package/index.d.ts +10 -10
- package/index.js +98 -21
- package/package.json +12 -5
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
|
+
|
|
14
|
+
<a href="https://vovk.dev/quick-install">Quick Start</a>
|
|
15
|
+
|
|
16
|
+
<a href="https://vovk.dev/performance">Performance</a>
|
|
11
17
|
</p>
|
|
12
18
|
|
|
13
19
|
---
|
|
@@ -15,3 +21,21 @@
|
|
|
15
21
|
## vovk-python [](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: ["
|
|
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::
|
|
3
|
-
use reqwest::
|
|
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::
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
let
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<%-
|
|
66
|
-
<%-
|
|
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(¶ms),
|
|
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 {
|
|
1
|
+
import type { VovkJSONSchemaBase } from 'vovk';
|
|
2
2
|
export declare function indent(level: number, pad?: number): string;
|
|
3
|
-
export declare function generateDocComment(schema:
|
|
4
|
-
export declare function resolveRef(ref: string, rootSchema:
|
|
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:
|
|
7
|
-
export declare function generateEnum(schema:
|
|
8
|
-
export declare function generateVariantEnum(schema:
|
|
9
|
-
export declare function generateAllOfType(schemas:
|
|
10
|
-
export declare function processObject(schema:
|
|
11
|
-
export declare function processPrimitive(schema:
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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' &&
|
|
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
|
-
|
|
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
|
-
|
|
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)}
|
|
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-
|
|
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
|
|
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-
|
|
570
|
+
'x-contentType': schemaObj['x-contentType'],
|
|
495
571
|
};
|
|
496
572
|
result += processObject(rootObject, [schemaName], 1, schemaObj, pad);
|
|
497
573
|
}
|
|
498
|
-
else if (
|
|
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
|
|
4
|
-
"description": "
|
|
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
|
|
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
|
-
"
|
|
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://
|
|
39
|
+
"homepage": "https://vovk.dev/rust"
|
|
33
40
|
}
|