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 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
+ &nbsp;&nbsp;
14
+ <a href="https://vovk.dev/quick-install">Quick Start</a>
15
+ &nbsp;&nbsp;
16
+ <a href="https://vovk.dev/performance">Performance</a>
17
+ </p>
18
+
19
+ ---
20
+
21
+ ## vovk-python [![npm version](https://badge.fury.io/js/vovk-python.svg)](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 %> [![Rust](https://img.shields.io/badge/Rust-lang-000000.svg?style=flat&logo=rust)](https://www.rust-lang.org) [![Vovk.ts](https://badgen.net/badge/Built%20with/Vovk.ts/333333?icon=https://vovk.dev/icon-white.svg)](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
+