vovk-rust 0.0.1-draft.31
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 +18 -0
- package/index.d.ts +16 -0
- package/index.js +534 -0
- package/package.json +33 -0
- package/requirements.txt +4 -0
- package/template/Cargo.toml.ejs +83 -0
- package/template/README.md.ejs +35 -0
- package/template/src/http_request.rs +488 -0
- package/template/src/lib.rs.ejs +81 -0
- package/template/src/read_full_schema.rs +88 -0
- package/vovk.config.test.mjs +26 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const data = {
|
|
3
|
+
package: {
|
|
4
|
+
name: t.package.name.replace(/-/g, '_'),
|
|
5
|
+
version: t.package.version,
|
|
6
|
+
edition: "2021"
|
|
7
|
+
},
|
|
8
|
+
dependencies: {
|
|
9
|
+
serde: { version: "1.0", features: ["derive"] },
|
|
10
|
+
serde_json: "1.0",
|
|
11
|
+
reqwest: { version: "0.12", features: ["blocking", "json", "multipart"] },
|
|
12
|
+
jsonschema: "0.17",
|
|
13
|
+
urlencoding: "2.1",
|
|
14
|
+
once_cell: "1.17"
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Add optional fields to package section
|
|
19
|
+
if (t.package.description) {
|
|
20
|
+
data.package.description = t.package.description;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (t.package.license) {
|
|
24
|
+
data.package.license = t.package.license || 'UNLICENSED';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (t.package.repository) {
|
|
28
|
+
data.package.repository = typeof t.package.repository === 'string'
|
|
29
|
+
? t.package.repository
|
|
30
|
+
: t.package.repository.url;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (t.package.homepage) {
|
|
34
|
+
data.package.homepage = t.package.homepage;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build authors array
|
|
38
|
+
const authors = [];
|
|
39
|
+
if (t.package.author) {
|
|
40
|
+
if (typeof t.package.author === 'string') {
|
|
41
|
+
authors.push(t.package.author);
|
|
42
|
+
} else {
|
|
43
|
+
let author = t.package.author.name;
|
|
44
|
+
if (t.package.author.email) author += ` <${t.package.author.email}>`;
|
|
45
|
+
authors.push(author);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (t.package.contributors && t.package.contributors.length) {
|
|
50
|
+
t.package.contributors.forEach(contributor => {
|
|
51
|
+
if (typeof contributor === 'string') {
|
|
52
|
+
authors.push(contributor);
|
|
53
|
+
} else {
|
|
54
|
+
let contribStr = contributor.name;
|
|
55
|
+
if (contributor.email) contribStr += ` <${contributor.email}>`;
|
|
56
|
+
authors.push(contribStr);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (authors.length) {
|
|
62
|
+
data.package.authors = authors;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add keywords if they exist
|
|
66
|
+
if (t.package.keywords && t.package.keywords.length) {
|
|
67
|
+
data.package.keywords = t.package.keywords;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Add bugs if they exist
|
|
71
|
+
if (t.package.bugs) {
|
|
72
|
+
if (!data.package.metadata) {
|
|
73
|
+
data.package.metadata = {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
data.package.metadata.package = {
|
|
77
|
+
bugs: typeof t.package.bugs === 'string'
|
|
78
|
+
? t.package.bugs
|
|
79
|
+
: t.package.bugs.url
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
%>
|
|
83
|
+
<%- t.TOML.stringify(data) %>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# <%= t.package.name.replace(/-/g, '_') %> v<%= t.package.version %> 
|
|
2
|
+
|
|
3
|
+
<%- t.package.description ? `> ${t.package.description}` : '' %>
|
|
4
|
+
|
|
5
|
+
<%- t.package.license ? `License: **${t.package.license}**` : '' %>
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# Install the package
|
|
10
|
+
cargo install <%= t.package.name.replace(/-/g, '_') %>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
<% Object.entries(t.schema.segments).forEach(([segmentName, segment]) => {
|
|
14
|
+
Object.values(segment.controllers).forEach((controllerSchema) => { %>
|
|
15
|
+
|
|
16
|
+
## mod <%= t._.snakeCase(controllerSchema.rpcModuleName) %>
|
|
17
|
+
<% Object.entries(controllerSchema.handlers).forEach(([handlerName, handlerSchema]) => { %>
|
|
18
|
+
### <%= t._.snakeCase(controllerSchema.rpcModuleName) %>::<%= t._.snakeCase(handlerName) %>
|
|
19
|
+
<%- handlerSchema.openapi?.summary ? `> ${handlerSchema.openapi.summary}` : '' %>
|
|
20
|
+
|
|
21
|
+
<%- handlerSchema.openapi?.description ? `${handlerSchema.openapi.description}` : '' %>
|
|
22
|
+
|
|
23
|
+
`<%= handlerSchema.httpMethod %> <%= [t.apiRoot, segmentName, controllerSchema.prefix, handlerSchema.path].filter(Boolean).join('/') %>`
|
|
24
|
+
|
|
25
|
+
```rust
|
|
26
|
+
<%- t.createCodeExamples({
|
|
27
|
+
handlerSchema,
|
|
28
|
+
handlerName,
|
|
29
|
+
controllerSchema,
|
|
30
|
+
package: t.package,
|
|
31
|
+
}).rs %>
|
|
32
|
+
```
|
|
33
|
+
<% }) %>
|
|
34
|
+
<% }) %>
|
|
35
|
+
<% }) %>
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
use serde::{Serialize, de::DeserializeOwned};
|
|
2
|
+
use reqwest::blocking::Client;
|
|
3
|
+
use reqwest::Method;
|
|
4
|
+
use reqwest::blocking::multipart;
|
|
5
|
+
use core::panic;
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
use std::error::Error;
|
|
8
|
+
use std::fmt;
|
|
9
|
+
use jsonschema::JSONSchema;
|
|
10
|
+
use serde_json::Value;
|
|
11
|
+
use urlencoding;
|
|
12
|
+
use crate::read_full_schema;
|
|
13
|
+
use once_cell::sync::Lazy;
|
|
14
|
+
|
|
15
|
+
// Custom error type for HTTP exceptions
|
|
16
|
+
#[derive(Debug, Serialize)]
|
|
17
|
+
pub struct HttpException {
|
|
18
|
+
message: String,
|
|
19
|
+
status_code: i32,
|
|
20
|
+
#[allow(dead_code)]
|
|
21
|
+
cause: Option<Value>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl fmt::Display for HttpException {
|
|
25
|
+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
26
|
+
write!(f, "[Status: {}] {}", self.status_code, self.message)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Error for HttpException {}
|
|
31
|
+
|
|
32
|
+
// Load the full schema only once using lazy initialization
|
|
33
|
+
static FULL_SCHEMA: Lazy<Result<Value, String>> = Lazy::new(|| {
|
|
34
|
+
read_full_schema::read_full_schema()
|
|
35
|
+
.map(|schema| serde_json::to_value(schema).expect("Failed to convert schema to Value"))
|
|
36
|
+
.map_err(|e| format!("Failed to read schema: {}", e))
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Private helper function for request preparation
|
|
40
|
+
fn prepare_request<B, Q, P>(
|
|
41
|
+
default_api_root: &str,
|
|
42
|
+
segment_name: &str,
|
|
43
|
+
controller_name: &str,
|
|
44
|
+
handler_name: &str,
|
|
45
|
+
body: Option<&B>,
|
|
46
|
+
form: Option<multipart::Form>,
|
|
47
|
+
query: Option<&Q>,
|
|
48
|
+
params: Option<&P>,
|
|
49
|
+
headers: Option<&HashMap<String, String>>,
|
|
50
|
+
api_root: Option<&str>,
|
|
51
|
+
disable_client_validation: bool,
|
|
52
|
+
) -> Result<(reqwest::blocking::RequestBuilder, String), Box<dyn Error>>
|
|
53
|
+
where
|
|
54
|
+
B: Serialize + ?Sized,
|
|
55
|
+
Q: Serialize + ?Sized,
|
|
56
|
+
P: Serialize + ?Sized,
|
|
57
|
+
{
|
|
58
|
+
// Extract schema information
|
|
59
|
+
let schema = match &*FULL_SCHEMA {
|
|
60
|
+
Ok(schema) => schema,
|
|
61
|
+
Err(e) => return Err(format!("Failed to load schema: {}", e).into()),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let segment = schema.get("segments")
|
|
65
|
+
.and_then(|s| s.get(segment_name))
|
|
66
|
+
.ok_or("Segment not found")?;
|
|
67
|
+
|
|
68
|
+
let controller = segment.get("controllers")
|
|
69
|
+
.and_then(|c| c.get(controller_name))
|
|
70
|
+
.ok_or("Controller not found")?;
|
|
71
|
+
|
|
72
|
+
let handlers = controller.get("handlers")
|
|
73
|
+
.and_then(|h| h.as_object())
|
|
74
|
+
.ok_or("Handlers not found")?;
|
|
75
|
+
|
|
76
|
+
let handler = handlers.get(handler_name).ok_or("Handler not found")?;
|
|
77
|
+
let prefix = controller.get("prefix")
|
|
78
|
+
.and_then(|p| p.as_str())
|
|
79
|
+
.ok_or("Prefix not found")?;
|
|
80
|
+
let handler_path = handler.get("path")
|
|
81
|
+
.and_then(|p| p.as_str())
|
|
82
|
+
.ok_or("Path not found")?;
|
|
83
|
+
let http_method = handler.get("httpMethod")
|
|
84
|
+
.ok_or("HTTP method not found")?
|
|
85
|
+
.as_str()
|
|
86
|
+
.ok_or("HTTP method is not a string")?;
|
|
87
|
+
let default_validation = Value::Object(serde_json::Map::new());
|
|
88
|
+
let validation = handler
|
|
89
|
+
.get("validation")
|
|
90
|
+
.unwrap_or(&default_validation);
|
|
91
|
+
|
|
92
|
+
// Construct the base URL
|
|
93
|
+
let url_parts: Vec<&str> = vec![api_root.unwrap_or(default_api_root), segment_name, prefix, handler_path]
|
|
94
|
+
.into_iter()
|
|
95
|
+
.filter(|s| !s.is_empty())
|
|
96
|
+
.collect();
|
|
97
|
+
let mut url = url_parts.join("/");
|
|
98
|
+
|
|
99
|
+
// Convert generic types to Value for validation if needed
|
|
100
|
+
let body_value = body.map(|b| serde_json::to_value(b))
|
|
101
|
+
.transpose()
|
|
102
|
+
.map_err(|e| format!("Failed to serialize body: {}", e))?;
|
|
103
|
+
|
|
104
|
+
let query_value = query.map(|q| serde_json::to_value(q))
|
|
105
|
+
.transpose()
|
|
106
|
+
.map_err(|e| format!("Failed to serialize query: {}", e))?;
|
|
107
|
+
|
|
108
|
+
let params_value = params.map(|p| serde_json::to_value(p))
|
|
109
|
+
.transpose()
|
|
110
|
+
.map_err(|e| format!("Failed to serialize params: {}", e))?;
|
|
111
|
+
|
|
112
|
+
// Perform JSON validation if not disabled and no form data is provided
|
|
113
|
+
if !disable_client_validation && form.is_none() {
|
|
114
|
+
if let Some(body_schema) = validation.get("body") {
|
|
115
|
+
if let Some(ref body_val) = body_value {
|
|
116
|
+
let schema =
|
|
117
|
+
JSONSchema::compile(body_schema).map_err(|e| format!("Invalid body schema: {}", e))?;
|
|
118
|
+
schema
|
|
119
|
+
.validate(body_val)
|
|
120
|
+
.map_err(|e| {
|
|
121
|
+
let error_msgs: Vec<String> = e.map(|err| format!("{}: {}", err.instance_path, err.to_string())).collect();
|
|
122
|
+
format!("Body validation failed: {}", error_msgs.join(", "))
|
|
123
|
+
})?;
|
|
124
|
+
} else if http_method != "GET" {
|
|
125
|
+
return Err("Body is required for validation but not provided".into());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if let Some(query_schema) = validation.get("query") {
|
|
130
|
+
if let Some(ref query_val) = query_value {
|
|
131
|
+
let schema =
|
|
132
|
+
JSONSchema::compile(query_schema).map_err(|e| format!("Invalid query schema: {}", e))?;
|
|
133
|
+
schema
|
|
134
|
+
.validate(query_val)
|
|
135
|
+
.map_err(|e| {
|
|
136
|
+
let error_msgs: Vec<String> = e.map(|err| format!("{}: {}", err.instance_path, err.to_string())).collect();
|
|
137
|
+
format!("Query validation failed: {}", error_msgs.join(", "))
|
|
138
|
+
})?;
|
|
139
|
+
} else {
|
|
140
|
+
return Err("Query is required for validation but not provided".into());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if let Some(params_schema) = validation.get("params") {
|
|
145
|
+
if let Some(ref params_val) = params_value {
|
|
146
|
+
let schema = JSONSchema::compile(params_schema)
|
|
147
|
+
.map_err(|e| format!("Invalid params schema: {}", e))?;
|
|
148
|
+
schema
|
|
149
|
+
.validate(params_val)
|
|
150
|
+
.map_err(|e| {
|
|
151
|
+
let error_msgs: Vec<String> = e.map(|err| format!("{}: {}", err.instance_path, err.to_string())).collect();
|
|
152
|
+
format!("Params validation failed: {}", error_msgs.join(", "))
|
|
153
|
+
})?;
|
|
154
|
+
} else {
|
|
155
|
+
return Err("Params are required for validation but not provided".into());
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Substitute path parameters in the URL
|
|
161
|
+
if let Some(ref params_val) = params_value {
|
|
162
|
+
if let Value::Object(map) = params_val {
|
|
163
|
+
for (key, value) in map {
|
|
164
|
+
let pattern = format!(":{}", key);
|
|
165
|
+
if let Value::String(s) = value {
|
|
166
|
+
url = url.replace(&pattern, s);
|
|
167
|
+
} else {
|
|
168
|
+
return Err(format!("Param {} must be a string", key).into());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Append query string if query parameters are provided
|
|
175
|
+
if let Some(ref query_val) = query_value {
|
|
176
|
+
let query_string = build_query_string(query_val, "");
|
|
177
|
+
if !query_string.is_empty() {
|
|
178
|
+
if url.contains('?') {
|
|
179
|
+
url += "&";
|
|
180
|
+
} else {
|
|
181
|
+
url += "?";
|
|
182
|
+
}
|
|
183
|
+
url += &query_string;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Set up request headers
|
|
188
|
+
let mut headers_map = reqwest::header::HeaderMap::new();
|
|
189
|
+
headers_map.insert("Accept", "application/jsonl, application/json".parse().unwrap());
|
|
190
|
+
if body_value.is_some() && form.is_none() {
|
|
191
|
+
headers_map.insert("Content-Type", "application/json".parse().unwrap());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Merge with user-provided headers if any
|
|
195
|
+
if let Some(provided_headers) = headers {
|
|
196
|
+
for (key, value) in provided_headers {
|
|
197
|
+
if let Ok(header_name) = reqwest::header::HeaderName::from_bytes(key.as_bytes()) {
|
|
198
|
+
if let Ok(header_value) = reqwest::header::HeaderValue::from_str(value) {
|
|
199
|
+
headers_map.insert(header_name, header_value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Map HTTP method string to reqwest::Method
|
|
206
|
+
let method = match http_method.to_uppercase().as_str() {
|
|
207
|
+
"GET" => Method::GET,
|
|
208
|
+
"POST" => Method::POST,
|
|
209
|
+
"PUT" => Method::PUT,
|
|
210
|
+
"DELETE" => Method::DELETE,
|
|
211
|
+
"PATCH" => Method::PATCH,
|
|
212
|
+
"OPTIONS" => Method::OPTIONS,
|
|
213
|
+
"HEAD" => Method::HEAD,
|
|
214
|
+
_ => return Err("Invalid HTTP method".into()),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Build the HTTP request
|
|
218
|
+
let client = Client::new();
|
|
219
|
+
let mut request = client.request(method, &url).headers(headers_map);
|
|
220
|
+
|
|
221
|
+
// Apply form data or JSON body to the request
|
|
222
|
+
if let Some(form_data) = form {
|
|
223
|
+
request = request.multipart(form_data);
|
|
224
|
+
} else if let Some(body_val) = body_value {
|
|
225
|
+
request = request.json(&body_val);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
Ok((request, http_method.to_string()))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Main request function for regular (non-streaming) responses
|
|
232
|
+
#[allow(dead_code)]
|
|
233
|
+
pub fn http_request<T, B, Q, P>(
|
|
234
|
+
default_api_root: &str,
|
|
235
|
+
segment_name: &str,
|
|
236
|
+
controller_name: &str,
|
|
237
|
+
handler_name: &str,
|
|
238
|
+
body: Option<&B>,
|
|
239
|
+
form: Option<multipart::Form>,
|
|
240
|
+
query: Option<&Q>,
|
|
241
|
+
params: Option<&P>,
|
|
242
|
+
headers: Option<&HashMap<String, String>>,
|
|
243
|
+
api_root: Option<&str>,
|
|
244
|
+
disable_client_validation: bool,
|
|
245
|
+
) -> Result<T, HttpException>
|
|
246
|
+
where
|
|
247
|
+
T: DeserializeOwned + 'static,
|
|
248
|
+
B: Serialize + ?Sized,
|
|
249
|
+
Q: Serialize + ?Sized,
|
|
250
|
+
P: Serialize + ?Sized,
|
|
251
|
+
{
|
|
252
|
+
// Prepare the request using the helper function
|
|
253
|
+
let (request, _) = prepare_request(
|
|
254
|
+
default_api_root,
|
|
255
|
+
segment_name,
|
|
256
|
+
controller_name,
|
|
257
|
+
handler_name,
|
|
258
|
+
body,
|
|
259
|
+
form,
|
|
260
|
+
query,
|
|
261
|
+
params,
|
|
262
|
+
headers,
|
|
263
|
+
api_root,
|
|
264
|
+
disable_client_validation,
|
|
265
|
+
).map_err(|e| HttpException {
|
|
266
|
+
message: e.to_string(),
|
|
267
|
+
status_code: 0,
|
|
268
|
+
cause: None,
|
|
269
|
+
})?;
|
|
270
|
+
|
|
271
|
+
// Send the request
|
|
272
|
+
let response = request.send().map_err(|e| HttpException {
|
|
273
|
+
message: e.to_string(),
|
|
274
|
+
status_code: 0,
|
|
275
|
+
cause: None,
|
|
276
|
+
})?;
|
|
277
|
+
|
|
278
|
+
// Handle the response based on Content-Type
|
|
279
|
+
let content_type = response
|
|
280
|
+
.headers()
|
|
281
|
+
.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)
|
|
311
|
+
}
|
|
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,
|
|
321
|
+
cause: None,
|
|
322
|
+
})?;
|
|
323
|
+
Ok(typed_value)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Request function specifically for streaming responses
|
|
329
|
+
#[allow(dead_code)]
|
|
330
|
+
pub fn http_request_stream<T, B, Q, P>(
|
|
331
|
+
default_api_root: &str,
|
|
332
|
+
segment_name: &str,
|
|
333
|
+
controller_name: &str,
|
|
334
|
+
handler_name: &str,
|
|
335
|
+
body: Option<&B>,
|
|
336
|
+
form: Option<multipart::Form>,
|
|
337
|
+
query: Option<&Q>,
|
|
338
|
+
params: Option<&P>,
|
|
339
|
+
headers: Option<&HashMap<String, String>>,
|
|
340
|
+
api_root: Option<&str>,
|
|
341
|
+
disable_client_validation: bool,
|
|
342
|
+
) -> Result<Box<dyn Iterator<Item = T>>, HttpException>
|
|
343
|
+
where
|
|
344
|
+
T: DeserializeOwned + 'static,
|
|
345
|
+
B: Serialize + ?Sized,
|
|
346
|
+
Q: Serialize + ?Sized,
|
|
347
|
+
P: Serialize + ?Sized,
|
|
348
|
+
{
|
|
349
|
+
// Prepare the request using the helper function
|
|
350
|
+
let (request, _) = prepare_request(
|
|
351
|
+
default_api_root,
|
|
352
|
+
segment_name,
|
|
353
|
+
controller_name,
|
|
354
|
+
handler_name,
|
|
355
|
+
body,
|
|
356
|
+
form,
|
|
357
|
+
query,
|
|
358
|
+
params,
|
|
359
|
+
headers,
|
|
360
|
+
api_root,
|
|
361
|
+
disable_client_validation,
|
|
362
|
+
).map_err(|e| HttpException {
|
|
363
|
+
message: e.to_string(),
|
|
364
|
+
status_code: 0,
|
|
365
|
+
cause: None,
|
|
366
|
+
})?;
|
|
367
|
+
|
|
368
|
+
// Send the request
|
|
369
|
+
let response = request.send().map_err(|e| HttpException {
|
|
370
|
+
message: e.to_string(),
|
|
371
|
+
status_code: 0,
|
|
372
|
+
cause: None,
|
|
373
|
+
})?;
|
|
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
|
+
}
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
Err(e) => {
|
|
398
|
+
panic!("Error reading from stream: {}", e);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
Ok(Box::new(typed_stream))
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Helper function to build query strings from nested JSON
|
|
407
|
+
fn build_query_string(data: &Value, prefix: &str) -> String {
|
|
408
|
+
match data {
|
|
409
|
+
Value::Object(map) => {
|
|
410
|
+
let parts: Vec<String> = map
|
|
411
|
+
.iter()
|
|
412
|
+
.map(|(k, v)| {
|
|
413
|
+
let new_prefix = if prefix.is_empty() {
|
|
414
|
+
k.to_string()
|
|
415
|
+
} else {
|
|
416
|
+
format!("{}[{}]", prefix, k)
|
|
417
|
+
};
|
|
418
|
+
build_query_string(v, &new_prefix)
|
|
419
|
+
})
|
|
420
|
+
.collect();
|
|
421
|
+
parts.join("&")
|
|
422
|
+
}
|
|
423
|
+
Value::Array(arr) => {
|
|
424
|
+
let parts: Vec<String> = arr
|
|
425
|
+
.iter()
|
|
426
|
+
.enumerate()
|
|
427
|
+
.map(|(i, v)| {
|
|
428
|
+
let new_prefix = format!("{}[{}]", prefix, i);
|
|
429
|
+
build_query_string(v, &new_prefix)
|
|
430
|
+
})
|
|
431
|
+
.collect();
|
|
432
|
+
parts.join("&")
|
|
433
|
+
}
|
|
434
|
+
Value::Null => String::new(),
|
|
435
|
+
_ => {
|
|
436
|
+
let value_str = match data {
|
|
437
|
+
Value::String(s) => s.clone(),
|
|
438
|
+
_ => data.to_string(),
|
|
439
|
+
};
|
|
440
|
+
format!("{}={}", prefix, urlencoding::encode(&value_str))
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
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
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
imports: ['vovk-rust']
|
|
3
|
+
---
|
|
4
|
+
<% const vars = {
|
|
5
|
+
convertJSONSchemasToRustTypes: t.imports['vovk-rust'].convertJSONSchemasToRustTypes,
|
|
6
|
+
}; %>
|
|
7
|
+
<%- `// auto-generated by Vovk.ts ${new Date().toISOString()}` %>
|
|
8
|
+
mod http_request;
|
|
9
|
+
mod read_full_schema;
|
|
10
|
+
|
|
11
|
+
pub use crate::http_request::HttpException;
|
|
12
|
+
|
|
13
|
+
<% Object.entries(t.schema.segments).forEach(([segmentName, segment]) => {
|
|
14
|
+
Object.values(segment.controllers).forEach((controllerSchema) => { %>
|
|
15
|
+
pub mod <%= t._.snakeCase(controllerSchema.rpcModuleName) %> {
|
|
16
|
+
#[allow(unused_imports)]
|
|
17
|
+
use crate::http_request::{HttpException, http_request, http_request_stream};
|
|
18
|
+
use std::collections::HashMap;
|
|
19
|
+
<% Object.entries(controllerSchema.handlers).forEach(([handlerNameOriginal, handlerSchema]) => {
|
|
20
|
+
const { validation, openapi, path, httpMethod } = handlerSchema;
|
|
21
|
+
const handlerName = t._.snakeCase(handlerNameOriginal);
|
|
22
|
+
%>
|
|
23
|
+
// <%= controllerSchema.rpcModuleName %>.<%= handlerName %> <%= httpMethod %> `<%= [t.apiRoot, segmentName, controllerSchema.prefix, handlerSchema.path].filter(Boolean).join('/') %>`
|
|
24
|
+
<%-
|
|
25
|
+
vars.convertJSONSchemasToRustTypes({
|
|
26
|
+
schemas: {
|
|
27
|
+
body: validation?.body,
|
|
28
|
+
query: validation?.query,
|
|
29
|
+
params: validation?.params,
|
|
30
|
+
output: validation?.output,
|
|
31
|
+
iteration: validation?.iteration
|
|
32
|
+
},
|
|
33
|
+
rootName: handlerName,
|
|
34
|
+
pad: 4
|
|
35
|
+
})
|
|
36
|
+
%>
|
|
37
|
+
<%= ([
|
|
38
|
+
openapi?.summary ? `Summary: ${openapi.summary}` : '',
|
|
39
|
+
openapi?.description ? `Description: ${openapi.description}` : '',
|
|
40
|
+
validation?.params?.description ? `Params: ${validation?.params?.description}` : '',
|
|
41
|
+
validation?.body?.description ? `Body: ${validation?.body?.description}`: '',
|
|
42
|
+
validation?.query?.description ? `Query: ${validation?.query?.description}`: '',
|
|
43
|
+
validation?.output?.description ? `Returns: ${validation?.output?.description}`: ''
|
|
44
|
+
]).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-formData'] ?'reqwest::blocking::multipart::Form' : `${handlerName}_::body`): '()' %>,
|
|
47
|
+
query: <%- validation?.query ? `${handlerName}_::query` : '()' %>,
|
|
48
|
+
params: <%- validation?.params ? `${handlerName}_::params` : '()' %>,
|
|
49
|
+
headers: Option<&HashMap<String, String>>,
|
|
50
|
+
api_root: Option<&str>,
|
|
51
|
+
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>' %>{
|
|
53
|
+
let result = <%= validation?.iteration ? 'http_request_stream' : 'http_request' %>::<
|
|
54
|
+
<%- [
|
|
55
|
+
validation?.output ? `${handlerName}_::output` : validation?.iteration ? `${handlerName}_::iteration` : 'serde_json::Value',
|
|
56
|
+
validation?.body && !validation?.body?.['x-formData'] ? `${handlerName}_::body` : '()',
|
|
57
|
+
validation?.query ? `${handlerName}_::query` : '()',
|
|
58
|
+
validation?.params ? `${handlerName}_::params` : '()'
|
|
59
|
+
].filter(Boolean).map((s) => ' '.repeat(12) + s).join(',\n') %>
|
|
60
|
+
>(
|
|
61
|
+
"<%= t.apiRoot %>",
|
|
62
|
+
"<%= segmentName %>",
|
|
63
|
+
"<%= controllerSchema.rpcModuleName %>",
|
|
64
|
+
"<%= handlerNameOriginal %>",
|
|
65
|
+
<%- !validation?.body || !validation?.body?.['x-formData'] ? `Some(&body)` : 'None' %>,
|
|
66
|
+
<%- validation?.body && validation?.body?.['x-formData'] ? `Some(body)` : 'None' %>,
|
|
67
|
+
Some(&query),
|
|
68
|
+
Some(¶ms),
|
|
69
|
+
headers,
|
|
70
|
+
api_root,
|
|
71
|
+
disable_client_validation,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
result
|
|
75
|
+
}
|
|
76
|
+
<% }) %>
|
|
77
|
+
}
|
|
78
|
+
<% }) %>
|
|
79
|
+
|
|
80
|
+
<% }) %>
|
|
81
|
+
|