vovk-rust 0.0.1-beta.10
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 +40 -0
- package/client-templates/rsPkg/Cargo.toml.ejs +87 -0
- package/client-templates/rsReadme/README.md.ejs +37 -0
- package/client-templates/rsSrc/http_request.rs +496 -0
- package/client-templates/rsSrc/lib.rs.ejs +85 -0
- package/client-templates/rsSrc/read_full_schema.rs +88 -0
- package/index.d.ts +16 -0
- package/index.js +529 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://vovk.dev">
|
|
3
|
+
<picture>
|
|
4
|
+
<source width="300" media="(prefers-color-scheme: dark)" srcset="https://vovk.dev/vovk-logo-white.svg">
|
|
5
|
+
<source width="300" media="(prefers-color-scheme: light)" srcset="https://vovk.dev/vovk-logo.svg">
|
|
6
|
+
<img width="300" alt="vovk" src="https://vovk.dev/vovk-logo.svg">
|
|
7
|
+
</picture>
|
|
8
|
+
</a>
|
|
9
|
+
<br>
|
|
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>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## vovk-python [](https://www.npmjs.com/package/vovk-python)
|
|
22
|
+
|
|
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
|
+
```sh
|
|
33
|
+
npx vovk generate --from rs --out ./rust_package
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Publish:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
cargo publish --manifest-path rust_package/Cargo.toml
|
|
40
|
+
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<%- t.getFirstLineBanner('sh') %>
|
|
2
|
+
<%
|
|
3
|
+
const data = {
|
|
4
|
+
package: {
|
|
5
|
+
name: t.package.name.replace(/-/g, '_'),
|
|
6
|
+
version: t.package.version,
|
|
7
|
+
edition: "2021"
|
|
8
|
+
},
|
|
9
|
+
dependencies: {
|
|
10
|
+
serde: { version: "1.0", features: ["derive"] },
|
|
11
|
+
serde_json: "1.0",
|
|
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"] },
|
|
16
|
+
jsonschema: "0.17",
|
|
17
|
+
urlencoding: "2.1",
|
|
18
|
+
once_cell: "1.17"
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Add optional fields to package section
|
|
23
|
+
if (t.package.description) {
|
|
24
|
+
data.package.description = t.package.description;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (t.package.license) {
|
|
28
|
+
data.package.license = t.package.license || 'UNLICENSED';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (t.package.repository) {
|
|
32
|
+
data.package.repository = typeof t.package.repository === 'string'
|
|
33
|
+
? t.package.repository
|
|
34
|
+
: t.package.repository.url;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (t.package.homepage) {
|
|
38
|
+
data.package.homepage = t.package.homepage;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build authors array
|
|
42
|
+
const authors = [];
|
|
43
|
+
if (t.package.author) {
|
|
44
|
+
if (typeof t.package.author === 'string') {
|
|
45
|
+
authors.push(t.package.author);
|
|
46
|
+
} else {
|
|
47
|
+
let author = t.package.author.name;
|
|
48
|
+
if (t.package.author.email) author += ` <${t.package.author.email}>`;
|
|
49
|
+
authors.push(author);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (t.package.contributors && t.package.contributors.length) {
|
|
54
|
+
t.package.contributors.forEach(contributor => {
|
|
55
|
+
if (typeof contributor === 'string') {
|
|
56
|
+
authors.push(contributor);
|
|
57
|
+
} else {
|
|
58
|
+
let contribStr = contributor.name;
|
|
59
|
+
if (contributor.email) contribStr += ` <${contributor.email}>`;
|
|
60
|
+
authors.push(contribStr);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (authors.length) {
|
|
66
|
+
data.package.authors = authors;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add keywords if they exist
|
|
70
|
+
if (t.package.keywords && t.package.keywords.length) {
|
|
71
|
+
data.package.keywords = t.package.keywords;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add bugs if they exist
|
|
75
|
+
if (t.package.bugs) {
|
|
76
|
+
if (!data.package.metadata) {
|
|
77
|
+
data.package.metadata = {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
data.package.metadata.package = {
|
|
81
|
+
bugs: typeof t.package.bugs === 'string'
|
|
82
|
+
? t.package.bugs
|
|
83
|
+
: t.package.bugs.url
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
%>
|
|
87
|
+
<%- t.TOML.stringify(data) %>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<%- t.getFirstLineBanner('html') %>
|
|
2
|
+
# <%= t.package.name.replace(/-/g, '_') %> v<%= t.package.version %> [](https://www.rust-lang.org) [](https://vovk.dev)
|
|
3
|
+
|
|
4
|
+
<%- t.readme.description ?? (`${t.package.description ? `> ${t.package.description}` : ''}`) %>
|
|
5
|
+
|
|
6
|
+
<%- t.package.license ? `License: **${t.package.license}**` : '' %>
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# Install the package
|
|
10
|
+
<%= t.readme.installCommand ?? `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.operationObject?.summary ? `> ${handlerSchema.operationObject.summary}` : '' %>
|
|
20
|
+
|
|
21
|
+
<%- handlerSchema.operationObject?.description ? `${handlerSchema.operationObject.description}` : '' %>
|
|
22
|
+
|
|
23
|
+
<% const forceApiRoot = t.segmentMeta[segment.segmentName].forceApiRoot ?? controllerSchema.forceApiRoot; %>
|
|
24
|
+
`<%= handlerSchema.httpMethod %> <%= [...(forceApiRoot ? [forceApiRoot] : [t.apiRoot, segmentName]), controllerSchema.prefix, handlerSchema.path].map((part, i) => i > 0 ? t._.trim(part, '/') : part).filter(Boolean).join('/') %>`
|
|
25
|
+
|
|
26
|
+
```rust
|
|
27
|
+
<%- t.createCodeSamples({
|
|
28
|
+
handlerSchema,
|
|
29
|
+
handlerName,
|
|
30
|
+
controllerSchema,
|
|
31
|
+
package: t.package,
|
|
32
|
+
config: t.samples,
|
|
33
|
+
}).rs %>
|
|
34
|
+
```
|
|
35
|
+
<% }) %>
|
|
36
|
+
<% }) %>
|
|
37
|
+
<% }) %>
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
use serde::{Serialize, de::DeserializeOwned};
|
|
2
|
+
use reqwest::{Client, Method};
|
|
3
|
+
use reqwest::multipart;
|
|
4
|
+
use std::collections::HashMap;
|
|
5
|
+
use std::error::Error;
|
|
6
|
+
use std::fmt;
|
|
7
|
+
use std::pin::Pin;
|
|
8
|
+
use jsonschema::JSONSchema;
|
|
9
|
+
use serde_json::Value;
|
|
10
|
+
use urlencoding;
|
|
11
|
+
use crate::read_full_schema;
|
|
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;
|
|
16
|
+
|
|
17
|
+
// Custom error type for HTTP exceptions
|
|
18
|
+
#[derive(Debug, Serialize)]
|
|
19
|
+
pub struct HttpException {
|
|
20
|
+
message: String,
|
|
21
|
+
status_code: i32,
|
|
22
|
+
#[allow(dead_code)]
|
|
23
|
+
cause: Option<Value>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl fmt::Display for HttpException {
|
|
27
|
+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
28
|
+
write!(f, "[Status: {}] {}", self.status_code, self.message)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Error for HttpException {}
|
|
33
|
+
|
|
34
|
+
// Load the full schema only once using lazy initialization
|
|
35
|
+
static FULL_SCHEMA: Lazy<Result<Value, String>> = Lazy::new(|| {
|
|
36
|
+
read_full_schema::read_full_schema()
|
|
37
|
+
.map(|schema| serde_json::to_value(schema).expect("Failed to convert schema to Value"))
|
|
38
|
+
.map_err(|e| format!("Failed to read schema: {}", e))
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Private helper function for request preparation
|
|
42
|
+
fn prepare_request<B, Q, P>(
|
|
43
|
+
default_api_root: &str,
|
|
44
|
+
segment_name: &str,
|
|
45
|
+
controller_name: &str,
|
|
46
|
+
handler_name: &str,
|
|
47
|
+
body: Option<&B>,
|
|
48
|
+
form: Option<multipart::Form>,
|
|
49
|
+
query: Option<&Q>,
|
|
50
|
+
params: Option<&P>,
|
|
51
|
+
headers: Option<&HashMap<String, String>>,
|
|
52
|
+
api_root: Option<&str>,
|
|
53
|
+
disable_client_validation: bool,
|
|
54
|
+
) -> Result<(reqwest::RequestBuilder, String), Box<dyn Error + Send + Sync>>
|
|
55
|
+
where
|
|
56
|
+
B: Serialize + ?Sized,
|
|
57
|
+
Q: Serialize + ?Sized,
|
|
58
|
+
P: Serialize + ?Sized,
|
|
59
|
+
{
|
|
60
|
+
// Extract schema information
|
|
61
|
+
let schema = match &*FULL_SCHEMA {
|
|
62
|
+
Ok(schema) => schema,
|
|
63
|
+
Err(e) => return Err(format!("Failed to load schema: {}", e).into()),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let segment = schema.get("segments")
|
|
67
|
+
.and_then(|s| s.get(segment_name))
|
|
68
|
+
.ok_or("Segment not found")?;
|
|
69
|
+
|
|
70
|
+
let controller = segment.get("controllers")
|
|
71
|
+
.and_then(|c| c.get(controller_name))
|
|
72
|
+
.ok_or("Controller not found")?;
|
|
73
|
+
|
|
74
|
+
let handlers = controller.get("handlers")
|
|
75
|
+
.and_then(|h| h.as_object())
|
|
76
|
+
.ok_or("Handlers not found")?;
|
|
77
|
+
|
|
78
|
+
let handler = handlers.get(handler_name).ok_or("Handler not found")?;
|
|
79
|
+
let prefix = controller.get("prefix")
|
|
80
|
+
.and_then(|p| p.as_str())
|
|
81
|
+
.ok_or("Prefix not found")?;
|
|
82
|
+
let handler_path = handler.get("path")
|
|
83
|
+
.and_then(|p| p.as_str())
|
|
84
|
+
.ok_or("Path not found")?;
|
|
85
|
+
let http_method = handler.get("httpMethod")
|
|
86
|
+
.ok_or("HTTP method not found")?
|
|
87
|
+
.as_str()
|
|
88
|
+
.ok_or("HTTP method is not a string")?;
|
|
89
|
+
let default_validation = Value::Object(serde_json::Map::new());
|
|
90
|
+
let validation = handler
|
|
91
|
+
.get("validation")
|
|
92
|
+
.unwrap_or(&default_validation);
|
|
93
|
+
|
|
94
|
+
// Construct the base URL
|
|
95
|
+
let url_parts: Vec<&str> = vec![api_root.unwrap_or(default_api_root), segment_name, prefix, handler_path]
|
|
96
|
+
.into_iter()
|
|
97
|
+
.filter(|s| !s.is_empty())
|
|
98
|
+
.collect();
|
|
99
|
+
let mut url = url_parts.join("/");
|
|
100
|
+
|
|
101
|
+
// Convert generic types to Value for validation if needed
|
|
102
|
+
let body_value = body.map(|b| serde_json::to_value(b))
|
|
103
|
+
.transpose()
|
|
104
|
+
.map_err(|e| format!("Failed to serialize body: {}", e))?;
|
|
105
|
+
|
|
106
|
+
let query_value = query.map(|q| serde_json::to_value(q))
|
|
107
|
+
.transpose()
|
|
108
|
+
.map_err(|e| format!("Failed to serialize query: {}", e))?;
|
|
109
|
+
|
|
110
|
+
let params_value = params.map(|p| serde_json::to_value(p))
|
|
111
|
+
.transpose()
|
|
112
|
+
.map_err(|e| format!("Failed to serialize params: {}", e))?;
|
|
113
|
+
|
|
114
|
+
// Perform JSON validation if not disabled and no form data is provided
|
|
115
|
+
if !disable_client_validation && form.is_none() {
|
|
116
|
+
if let Some(body_schema) = validation.get("body") {
|
|
117
|
+
if let Some(ref body_val) = body_value {
|
|
118
|
+
let schema =
|
|
119
|
+
JSONSchema::compile(body_schema).map_err(|e| format!("Invalid body schema: {}", e))?;
|
|
120
|
+
schema
|
|
121
|
+
.validate(body_val)
|
|
122
|
+
.map_err(|e| {
|
|
123
|
+
let error_msgs: Vec<String> = e.map(|err| format!("{}: {}", err.instance_path, err.to_string())).collect();
|
|
124
|
+
format!("Body validation failed: {}", error_msgs.join(", "))
|
|
125
|
+
})?;
|
|
126
|
+
} else if http_method != "GET" {
|
|
127
|
+
return Err("Body is required for validation but not provided".into());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if let Some(query_schema) = validation.get("query") {
|
|
132
|
+
if let Some(ref query_val) = query_value {
|
|
133
|
+
let schema =
|
|
134
|
+
JSONSchema::compile(query_schema).map_err(|e| format!("Invalid query schema: {}", e))?;
|
|
135
|
+
schema
|
|
136
|
+
.validate(query_val)
|
|
137
|
+
.map_err(|e| {
|
|
138
|
+
let error_msgs: Vec<String> = e.map(|err| format!("{}: {}", err.instance_path, err.to_string())).collect();
|
|
139
|
+
format!("Query validation failed: {}", error_msgs.join(", "))
|
|
140
|
+
})?;
|
|
141
|
+
} else {
|
|
142
|
+
return Err("Query is required for validation but not provided".into());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if let Some(params_schema) = validation.get("params") {
|
|
147
|
+
if let Some(ref params_val) = params_value {
|
|
148
|
+
let schema = JSONSchema::compile(params_schema)
|
|
149
|
+
.map_err(|e| format!("Invalid params schema: {}", e))?;
|
|
150
|
+
schema
|
|
151
|
+
.validate(params_val)
|
|
152
|
+
.map_err(|e| {
|
|
153
|
+
let error_msgs: Vec<String> = e.map(|err| format!("{}: {}", err.instance_path, err.to_string())).collect();
|
|
154
|
+
format!("Params validation failed: {}", error_msgs.join(", "))
|
|
155
|
+
})?;
|
|
156
|
+
} else {
|
|
157
|
+
return Err("Params are required for validation but not provided".into());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Substitute path parameters in the URL
|
|
163
|
+
if let Some(ref params_val) = params_value {
|
|
164
|
+
if let Value::Object(map) = params_val {
|
|
165
|
+
for (key, value) in map {
|
|
166
|
+
let pattern = format!("{{{}}}", key);
|
|
167
|
+
if let Value::String(s) = value {
|
|
168
|
+
url = url.replace(&pattern, s);
|
|
169
|
+
} else {
|
|
170
|
+
return Err(format!("Param {} must be a string", key).into());
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Append query string if query parameters are provided
|
|
177
|
+
if let Some(ref query_val) = query_value {
|
|
178
|
+
let query_string = build_query_string(query_val, "");
|
|
179
|
+
if !query_string.is_empty() {
|
|
180
|
+
if url.contains('?') {
|
|
181
|
+
url += "&";
|
|
182
|
+
} else {
|
|
183
|
+
url += "?";
|
|
184
|
+
}
|
|
185
|
+
url += &query_string;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Set up request headers
|
|
190
|
+
let mut headers_map = reqwest::header::HeaderMap::new();
|
|
191
|
+
headers_map.insert("Accept", "application/jsonl, application/json".parse().unwrap());
|
|
192
|
+
if body_value.is_some() && form.is_none() {
|
|
193
|
+
headers_map.insert("Content-Type", "application/json".parse().unwrap());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Merge with user-provided headers if any
|
|
197
|
+
if let Some(provided_headers) = headers {
|
|
198
|
+
for (key, value) in provided_headers {
|
|
199
|
+
if let Ok(header_name) = reqwest::header::HeaderName::from_bytes(key.as_bytes()) {
|
|
200
|
+
if let Ok(header_value) = reqwest::header::HeaderValue::from_str(value) {
|
|
201
|
+
headers_map.insert(header_name, header_value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Map HTTP method string to reqwest::Method
|
|
208
|
+
let method = match http_method.to_uppercase().as_str() {
|
|
209
|
+
"GET" => Method::GET,
|
|
210
|
+
"POST" => Method::POST,
|
|
211
|
+
"PUT" => Method::PUT,
|
|
212
|
+
"DELETE" => Method::DELETE,
|
|
213
|
+
"PATCH" => Method::PATCH,
|
|
214
|
+
"OPTIONS" => Method::OPTIONS,
|
|
215
|
+
"HEAD" => Method::HEAD,
|
|
216
|
+
_ => return Err("Invalid HTTP method".into()),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Build the HTTP request
|
|
220
|
+
let client = Client::new();
|
|
221
|
+
let mut request = client.request(method, &url).headers(headers_map);
|
|
222
|
+
|
|
223
|
+
// Apply form data or JSON body to the request
|
|
224
|
+
if let Some(form_data) = form {
|
|
225
|
+
request = request.multipart(form_data);
|
|
226
|
+
} else if let Some(body_val) = body_value {
|
|
227
|
+
request = request.json(&body_val);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
Ok((request, http_method.to_string()))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Main request function for regular (non-streaming) responses
|
|
234
|
+
#[allow(dead_code)]
|
|
235
|
+
pub async fn http_request<T, B, Q, P>(
|
|
236
|
+
default_api_root: &str,
|
|
237
|
+
segment_name: &str,
|
|
238
|
+
controller_name: &str,
|
|
239
|
+
handler_name: &str,
|
|
240
|
+
body: Option<&B>,
|
|
241
|
+
form: Option<multipart::Form>,
|
|
242
|
+
query: Option<&Q>,
|
|
243
|
+
params: Option<&P>,
|
|
244
|
+
headers: Option<&HashMap<String, String>>,
|
|
245
|
+
api_root: Option<&str>,
|
|
246
|
+
disable_client_validation: bool,
|
|
247
|
+
) -> Result<T, HttpException>
|
|
248
|
+
where
|
|
249
|
+
T: DeserializeOwned + 'static,
|
|
250
|
+
B: Serialize + ?Sized,
|
|
251
|
+
Q: Serialize + ?Sized,
|
|
252
|
+
P: Serialize + ?Sized,
|
|
253
|
+
{
|
|
254
|
+
let (request, _) = prepare_request(
|
|
255
|
+
default_api_root,
|
|
256
|
+
segment_name,
|
|
257
|
+
controller_name,
|
|
258
|
+
handler_name,
|
|
259
|
+
body,
|
|
260
|
+
form,
|
|
261
|
+
query,
|
|
262
|
+
params,
|
|
263
|
+
headers,
|
|
264
|
+
api_root,
|
|
265
|
+
disable_client_validation,
|
|
266
|
+
).map_err(|e| HttpException {
|
|
267
|
+
message: e.to_string(),
|
|
268
|
+
status_code: 0,
|
|
269
|
+
cause: None,
|
|
270
|
+
})?;
|
|
271
|
+
|
|
272
|
+
let response = request.send().await.map_err(|e| HttpException {
|
|
273
|
+
message: e.to_string(),
|
|
274
|
+
status_code: 0,
|
|
275
|
+
cause: None,
|
|
276
|
+
})?;
|
|
277
|
+
|
|
278
|
+
let status = response.status();
|
|
279
|
+
let status_code = status.as_u16() as i32;
|
|
280
|
+
|
|
281
|
+
let content_type = response
|
|
282
|
+
.headers()
|
|
283
|
+
.get("Content-Type")
|
|
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
|
+
});
|
|
308
|
+
}
|
|
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,
|
|
326
|
+
cause: None,
|
|
327
|
+
});
|
|
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
|
+
})
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Request function specifically for streaming responses
|
|
339
|
+
#[allow(dead_code)]
|
|
340
|
+
pub async fn http_request_stream<T, B, Q, P>(
|
|
341
|
+
default_api_root: &str,
|
|
342
|
+
segment_name: &str,
|
|
343
|
+
controller_name: &str,
|
|
344
|
+
handler_name: &str,
|
|
345
|
+
body: Option<&B>,
|
|
346
|
+
form: Option<multipart::Form>,
|
|
347
|
+
query: Option<&Q>,
|
|
348
|
+
params: Option<&P>,
|
|
349
|
+
headers: Option<&HashMap<String, String>>,
|
|
350
|
+
api_root: Option<&str>,
|
|
351
|
+
disable_client_validation: bool,
|
|
352
|
+
) -> Result<Pin<Box<dyn Stream<Item = Result<T, HttpException>> + Send>>, HttpException>
|
|
353
|
+
where
|
|
354
|
+
T: DeserializeOwned + 'static,
|
|
355
|
+
B: Serialize + ?Sized,
|
|
356
|
+
Q: Serialize + ?Sized,
|
|
357
|
+
P: Serialize + ?Sized,
|
|
358
|
+
{
|
|
359
|
+
let (request, _) = prepare_request(
|
|
360
|
+
default_api_root,
|
|
361
|
+
segment_name,
|
|
362
|
+
controller_name,
|
|
363
|
+
handler_name,
|
|
364
|
+
body,
|
|
365
|
+
form,
|
|
366
|
+
query,
|
|
367
|
+
params,
|
|
368
|
+
headers,
|
|
369
|
+
api_root,
|
|
370
|
+
disable_client_validation,
|
|
371
|
+
).map_err(|e| HttpException {
|
|
372
|
+
message: e.to_string(),
|
|
373
|
+
status_code: 0,
|
|
374
|
+
cause: None,
|
|
375
|
+
})?;
|
|
376
|
+
|
|
377
|
+
let response = request.send().await.map_err(|e| HttpException {
|
|
378
|
+
message: e.to_string(),
|
|
379
|
+
status_code: 0,
|
|
380
|
+
cause: None,
|
|
381
|
+
})?;
|
|
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
|
+
})),
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
Err(e) => Some(Err(HttpException {
|
|
424
|
+
message: e.to_string(),
|
|
425
|
+
status_code,
|
|
426
|
+
cause: None,
|
|
427
|
+
})),
|
|
428
|
+
}
|
|
429
|
+
});
|
|
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))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Helper function to build query strings from nested JSON
|
|
459
|
+
fn build_query_string(data: &Value, prefix: &str) -> String {
|
|
460
|
+
match data {
|
|
461
|
+
Value::Object(map) => {
|
|
462
|
+
let parts: Vec<String> = map
|
|
463
|
+
.iter()
|
|
464
|
+
.map(|(k, v)| {
|
|
465
|
+
let new_prefix = if prefix.is_empty() {
|
|
466
|
+
k.to_string()
|
|
467
|
+
} else {
|
|
468
|
+
format!("{}[{}]", prefix, k)
|
|
469
|
+
};
|
|
470
|
+
build_query_string(v, &new_prefix)
|
|
471
|
+
})
|
|
472
|
+
.collect();
|
|
473
|
+
parts.join("&")
|
|
474
|
+
}
|
|
475
|
+
Value::Array(arr) => {
|
|
476
|
+
let parts: Vec<String> = arr
|
|
477
|
+
.iter()
|
|
478
|
+
.enumerate()
|
|
479
|
+
.map(|(i, v)| {
|
|
480
|
+
let new_prefix = format!("{}[{}]", prefix, i);
|
|
481
|
+
build_query_string(v, &new_prefix)
|
|
482
|
+
})
|
|
483
|
+
.collect();
|
|
484
|
+
parts.join("&")
|
|
485
|
+
}
|
|
486
|
+
Value::Null => String::new(),
|
|
487
|
+
_ => {
|
|
488
|
+
let value_str = match data {
|
|
489
|
+
Value::String(s) => s.clone(),
|
|
490
|
+
_ => data.to_string(),
|
|
491
|
+
};
|
|
492
|
+
format!("{}={}", prefix, urlencoding::encode(&value_str))
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|