@typokit/transform-native 0.1.4
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/dist/index.d.ts +148 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +290 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
- package/src/env.d.ts +40 -0
- package/src/index.d.ts +135 -0
- package/src/index.test.ts +878 -0
- package/src/index.ts +437 -0
- package/src/lib.rs +388 -0
- package/src/openapi_generator.rs +525 -0
- package/src/output_pipeline.rs +234 -0
- package/src/parser.rs +105 -0
- package/src/route_compiler.rs +615 -0
- package/src/schema_differ.rs +393 -0
- package/src/test_stub_generator.rs +318 -0
- package/src/type_extractor.rs +370 -0
- package/src/typia_bridge.rs +179 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
2
|
+
use serde_json::{json, Value};
|
|
3
|
+
use crate::route_compiler::{RouteEntry, RouteTypeInfo, PathSegment};
|
|
4
|
+
use crate::type_extractor::TypeMetadata;
|
|
5
|
+
|
|
6
|
+
/// Generate an OpenAPI 3.1.0 specification from route entries and type metadata.
|
|
7
|
+
pub fn generate_openapi(
|
|
8
|
+
entries: &[RouteEntry],
|
|
9
|
+
type_map: &HashMap<String, TypeMetadata>,
|
|
10
|
+
) -> String {
|
|
11
|
+
let mut paths: BTreeMap<String, BTreeMap<String, Value>> = BTreeMap::new();
|
|
12
|
+
let mut referenced_schemas: HashSet<String> = HashSet::new();
|
|
13
|
+
|
|
14
|
+
for entry in entries {
|
|
15
|
+
let openapi_path = convert_path_to_openapi(&entry.path);
|
|
16
|
+
let method_lower = entry.method.to_lowercase();
|
|
17
|
+
let path_item = paths.entry(openapi_path).or_default();
|
|
18
|
+
let operation = build_operation(entry, &mut referenced_schemas);
|
|
19
|
+
path_item.insert(method_lower, operation);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let schemas = build_component_schemas(type_map, &referenced_schemas);
|
|
23
|
+
|
|
24
|
+
let paths_value: serde_json::Map<String, Value> = paths
|
|
25
|
+
.into_iter()
|
|
26
|
+
.map(|(path, methods)| {
|
|
27
|
+
let methods_obj: serde_json::Map<String, Value> = methods.into_iter().collect();
|
|
28
|
+
(path, Value::Object(methods_obj))
|
|
29
|
+
})
|
|
30
|
+
.collect();
|
|
31
|
+
|
|
32
|
+
let mut spec = json!({
|
|
33
|
+
"openapi": "3.1.0",
|
|
34
|
+
"info": {
|
|
35
|
+
"title": "API",
|
|
36
|
+
"version": "1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"paths": Value::Object(paths_value)
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if !schemas.is_empty() {
|
|
42
|
+
let schemas_obj: serde_json::Map<String, Value> = schemas.into_iter().collect();
|
|
43
|
+
spec["components"] = json!({
|
|
44
|
+
"schemas": Value::Object(schemas_obj)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
serde_json::to_string_pretty(&spec).unwrap()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn convert_path_to_openapi(path: &str) -> String {
|
|
52
|
+
path.split('/')
|
|
53
|
+
.map(|s| {
|
|
54
|
+
if s.starts_with(':') {
|
|
55
|
+
format!("{{{}}}", &s[1..])
|
|
56
|
+
} else if s.starts_with('*') {
|
|
57
|
+
format!("{{{}}}", if s.len() > 1 { &s[1..] } else { "wildcard" })
|
|
58
|
+
} else {
|
|
59
|
+
s.to_string()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.collect::<Vec<_>>()
|
|
63
|
+
.join("/")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn generate_operation_id(method: &str, path: &str) -> String {
|
|
67
|
+
let clean_path = path
|
|
68
|
+
.split('/')
|
|
69
|
+
.filter(|s| !s.is_empty())
|
|
70
|
+
.map(|s| {
|
|
71
|
+
if s.starts_with(':') || s.starts_with('*') {
|
|
72
|
+
capitalize(&s[1..])
|
|
73
|
+
} else {
|
|
74
|
+
capitalize(s)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.collect::<String>();
|
|
78
|
+
|
|
79
|
+
format!("{}{}", method.to_lowercase(), clean_path)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn capitalize(s: &str) -> String {
|
|
83
|
+
let mut chars = s.chars();
|
|
84
|
+
match chars.next() {
|
|
85
|
+
None => String::new(),
|
|
86
|
+
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn build_operation(entry: &RouteEntry, referenced_schemas: &mut HashSet<String>) -> Value {
|
|
91
|
+
let mut op = json!({
|
|
92
|
+
"operationId": generate_operation_id(&entry.method, &entry.path),
|
|
93
|
+
"responses": {
|
|
94
|
+
"200": build_response(&entry.response_type, referenced_schemas)
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Build parameters
|
|
99
|
+
let mut params: Vec<Value> = Vec::new();
|
|
100
|
+
|
|
101
|
+
// Path parameters from segments
|
|
102
|
+
for seg in &entry.segments {
|
|
103
|
+
if let PathSegment::Param(name) = seg {
|
|
104
|
+
params.push(json!({
|
|
105
|
+
"name": name,
|
|
106
|
+
"in": "path",
|
|
107
|
+
"required": true,
|
|
108
|
+
"schema": { "type": "string" }
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Query parameters from object literal types
|
|
114
|
+
if let RouteTypeInfo::ObjectLiteral(props) = &entry.query_type {
|
|
115
|
+
for prop in props {
|
|
116
|
+
params.push(json!({
|
|
117
|
+
"name": &prop.name,
|
|
118
|
+
"in": "query",
|
|
119
|
+
"required": !prop.optional,
|
|
120
|
+
"schema": type_info_to_schema(&prop.type_info, referenced_schemas)
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
} else if !matches!(entry.query_type, RouteTypeInfo::Void) {
|
|
124
|
+
collect_schema_refs(&entry.query_type, referenced_schemas);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if !params.is_empty() {
|
|
128
|
+
op["parameters"] = json!(params);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Request body
|
|
132
|
+
if !matches!(entry.body_type, RouteTypeInfo::Void) {
|
|
133
|
+
op["requestBody"] = json!({
|
|
134
|
+
"required": true,
|
|
135
|
+
"content": {
|
|
136
|
+
"application/json": {
|
|
137
|
+
"schema": type_info_to_schema(&entry.body_type, referenced_schemas)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
op
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn build_response(type_info: &RouteTypeInfo, referenced_schemas: &mut HashSet<String>) -> Value {
|
|
147
|
+
if matches!(type_info, RouteTypeInfo::Void) {
|
|
148
|
+
return json!({
|
|
149
|
+
"description": "No content"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
json!({
|
|
154
|
+
"description": "Successful response",
|
|
155
|
+
"content": {
|
|
156
|
+
"application/json": {
|
|
157
|
+
"schema": type_info_to_schema(type_info, referenced_schemas)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Convert RouteTypeInfo to JSON Schema value
|
|
164
|
+
fn type_info_to_schema(type_info: &RouteTypeInfo, referenced_schemas: &mut HashSet<String>) -> Value {
|
|
165
|
+
match type_info {
|
|
166
|
+
RouteTypeInfo::Void => json!({}),
|
|
167
|
+
RouteTypeInfo::Primitive(name) => match name.as_str() {
|
|
168
|
+
"string" => json!({ "type": "string" }),
|
|
169
|
+
"number" => json!({ "type": "number" }),
|
|
170
|
+
"boolean" => json!({ "type": "boolean" }),
|
|
171
|
+
"null" => json!({ "type": "null" }),
|
|
172
|
+
"any" | "unknown" => json!({}),
|
|
173
|
+
_ => json!({}),
|
|
174
|
+
},
|
|
175
|
+
RouteTypeInfo::Named(name) => {
|
|
176
|
+
referenced_schemas.insert(name.clone());
|
|
177
|
+
json!({ "$ref": format!("#/components/schemas/{}", name) })
|
|
178
|
+
}
|
|
179
|
+
RouteTypeInfo::Generic(name, args) => {
|
|
180
|
+
let schema_name = format_generic_schema_name(name, args);
|
|
181
|
+
referenced_schemas.insert(name.clone());
|
|
182
|
+
for arg in args {
|
|
183
|
+
collect_schema_refs(arg, referenced_schemas);
|
|
184
|
+
}
|
|
185
|
+
json!({ "$ref": format!("#/components/schemas/{}", schema_name) })
|
|
186
|
+
}
|
|
187
|
+
RouteTypeInfo::Array(inner) => {
|
|
188
|
+
json!({
|
|
189
|
+
"type": "array",
|
|
190
|
+
"items": type_info_to_schema(inner, referenced_schemas)
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
RouteTypeInfo::ObjectLiteral(props) => {
|
|
194
|
+
let mut properties = serde_json::Map::new();
|
|
195
|
+
let mut required: Vec<String> = Vec::new();
|
|
196
|
+
|
|
197
|
+
for prop in props {
|
|
198
|
+
properties.insert(
|
|
199
|
+
prop.name.clone(),
|
|
200
|
+
type_info_to_schema(&prop.type_info, referenced_schemas),
|
|
201
|
+
);
|
|
202
|
+
if !prop.optional {
|
|
203
|
+
required.push(prop.name.clone());
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let mut schema = json!({
|
|
208
|
+
"type": "object",
|
|
209
|
+
"properties": Value::Object(properties)
|
|
210
|
+
});
|
|
211
|
+
if !required.is_empty() {
|
|
212
|
+
schema["required"] = json!(required);
|
|
213
|
+
}
|
|
214
|
+
schema
|
|
215
|
+
}
|
|
216
|
+
RouteTypeInfo::Union(types) => {
|
|
217
|
+
json!({
|
|
218
|
+
"oneOf": types.iter()
|
|
219
|
+
.map(|t| type_info_to_schema(t, referenced_schemas))
|
|
220
|
+
.collect::<Vec<_>>()
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
RouteTypeInfo::StringLiteral(val) => {
|
|
224
|
+
json!({ "type": "string", "const": val })
|
|
225
|
+
}
|
|
226
|
+
RouteTypeInfo::NumberLiteral(val) => {
|
|
227
|
+
json!({ "type": "number", "const": val })
|
|
228
|
+
}
|
|
229
|
+
RouteTypeInfo::BooleanLiteral(val) => {
|
|
230
|
+
json!({ "type": "boolean", "const": val })
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn format_generic_schema_name(name: &str, args: &[RouteTypeInfo]) -> String {
|
|
236
|
+
let arg_names: Vec<String> = args.iter().map(type_info_short_name).collect();
|
|
237
|
+
format!("{}_{}", name, arg_names.join("_"))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn type_info_short_name(info: &RouteTypeInfo) -> String {
|
|
241
|
+
match info {
|
|
242
|
+
RouteTypeInfo::Named(n) => n.clone(),
|
|
243
|
+
RouteTypeInfo::Primitive(n) => n.clone(),
|
|
244
|
+
RouteTypeInfo::Generic(n, args) => format_generic_schema_name(n, args),
|
|
245
|
+
RouteTypeInfo::Array(inner) => format!("{}Array", type_info_short_name(inner)),
|
|
246
|
+
_ => "unknown".to_string(),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn collect_schema_refs(type_info: &RouteTypeInfo, referenced_schemas: &mut HashSet<String>) {
|
|
251
|
+
match type_info {
|
|
252
|
+
RouteTypeInfo::Named(name) => {
|
|
253
|
+
referenced_schemas.insert(name.clone());
|
|
254
|
+
}
|
|
255
|
+
RouteTypeInfo::Generic(name, args) => {
|
|
256
|
+
referenced_schemas.insert(name.clone());
|
|
257
|
+
for arg in args {
|
|
258
|
+
collect_schema_refs(arg, referenced_schemas);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
RouteTypeInfo::Array(inner) => collect_schema_refs(inner, referenced_schemas),
|
|
262
|
+
RouteTypeInfo::ObjectLiteral(props) => {
|
|
263
|
+
for prop in props {
|
|
264
|
+
collect_schema_refs(&prop.type_info, referenced_schemas);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
RouteTypeInfo::Union(types) => {
|
|
268
|
+
for t in types {
|
|
269
|
+
collect_schema_refs(t, referenced_schemas);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
_ => {}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Build component schemas from the type map for all referenced types
|
|
277
|
+
fn build_component_schemas(
|
|
278
|
+
type_map: &HashMap<String, TypeMetadata>,
|
|
279
|
+
referenced: &HashSet<String>,
|
|
280
|
+
) -> BTreeMap<String, Value> {
|
|
281
|
+
let mut schemas = BTreeMap::new();
|
|
282
|
+
|
|
283
|
+
for name in referenced {
|
|
284
|
+
if let Some(metadata) = type_map.get(name) {
|
|
285
|
+
let mut properties = serde_json::Map::new();
|
|
286
|
+
let mut required: Vec<String> = Vec::new();
|
|
287
|
+
|
|
288
|
+
for (prop_name, prop) in &metadata.properties {
|
|
289
|
+
properties.insert(
|
|
290
|
+
prop_name.clone(),
|
|
291
|
+
ts_type_string_to_schema(&prop.type_str),
|
|
292
|
+
);
|
|
293
|
+
if !prop.optional {
|
|
294
|
+
required.push(prop_name.clone());
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let mut schema = json!({
|
|
299
|
+
"type": "object",
|
|
300
|
+
"properties": Value::Object(properties)
|
|
301
|
+
});
|
|
302
|
+
if !required.is_empty() {
|
|
303
|
+
required.sort();
|
|
304
|
+
schema["required"] = json!(required);
|
|
305
|
+
}
|
|
306
|
+
schemas.insert(name.clone(), schema);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
schemas
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Convert a stringified TypeScript type to a basic JSON Schema
|
|
314
|
+
fn ts_type_string_to_schema(type_str: &str) -> Value {
|
|
315
|
+
match type_str {
|
|
316
|
+
"string" => json!({ "type": "string" }),
|
|
317
|
+
"number" => json!({ "type": "number" }),
|
|
318
|
+
"boolean" => json!({ "type": "boolean" }),
|
|
319
|
+
"null" => json!({ "type": "null" }),
|
|
320
|
+
"any" | "unknown" => json!({}),
|
|
321
|
+
s if s.ends_with("[]") => {
|
|
322
|
+
json!({
|
|
323
|
+
"type": "array",
|
|
324
|
+
"items": ts_type_string_to_schema(&s[..s.len() - 2])
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
s if s.starts_with('"') && s.ends_with('"') => {
|
|
328
|
+
json!({ "type": "string", "const": &s[1..s.len() - 1] })
|
|
329
|
+
}
|
|
330
|
+
s if s.contains(" | ") => {
|
|
331
|
+
let types: Vec<Value> = s
|
|
332
|
+
.split(" | ")
|
|
333
|
+
.map(|t| ts_type_string_to_schema(t.trim()))
|
|
334
|
+
.collect();
|
|
335
|
+
json!({ "oneOf": types })
|
|
336
|
+
}
|
|
337
|
+
_ => {
|
|
338
|
+
// Assume it's a named type reference
|
|
339
|
+
json!({ "$ref": format!("#/components/schemas/{}", type_str) })
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#[cfg(test)]
|
|
345
|
+
mod tests {
|
|
346
|
+
use super::*;
|
|
347
|
+
use crate::route_compiler::{RouteEntry, RouteTypeInfo, RouteObjectProp, parse_path_segments};
|
|
348
|
+
use crate::type_extractor::PropertyMetadata;
|
|
349
|
+
|
|
350
|
+
#[test]
|
|
351
|
+
fn test_generate_openapi_basic() {
|
|
352
|
+
let entries = vec![make_entry("GET", "/users")];
|
|
353
|
+
let type_map = HashMap::new();
|
|
354
|
+
let spec = generate_openapi(&entries, &type_map);
|
|
355
|
+
let parsed: Value = serde_json::from_str(&spec).unwrap();
|
|
356
|
+
|
|
357
|
+
assert_eq!(parsed["openapi"], "3.1.0");
|
|
358
|
+
assert!(parsed["paths"]["/users"]["get"].is_object());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
fn test_generate_openapi_path_params() {
|
|
363
|
+
let entries = vec![make_entry("GET", "/users/:id")];
|
|
364
|
+
let type_map = HashMap::new();
|
|
365
|
+
let spec = generate_openapi(&entries, &type_map);
|
|
366
|
+
let parsed: Value = serde_json::from_str(&spec).unwrap();
|
|
367
|
+
|
|
368
|
+
// Path should use {id} format
|
|
369
|
+
assert!(parsed["paths"]["/users/{id}"]["get"].is_object());
|
|
370
|
+
let params = &parsed["paths"]["/users/{id}"]["get"]["parameters"];
|
|
371
|
+
assert_eq!(params[0]["name"], "id");
|
|
372
|
+
assert_eq!(params[0]["in"], "path");
|
|
373
|
+
assert_eq!(params[0]["required"], true);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[test]
|
|
377
|
+
fn test_generate_openapi_query_params() {
|
|
378
|
+
let mut entry = make_entry("GET", "/users");
|
|
379
|
+
entry.query_type = RouteTypeInfo::ObjectLiteral(vec![
|
|
380
|
+
RouteObjectProp {
|
|
381
|
+
name: "page".into(),
|
|
382
|
+
type_info: RouteTypeInfo::Primitive("number".into()),
|
|
383
|
+
optional: true,
|
|
384
|
+
},
|
|
385
|
+
RouteObjectProp {
|
|
386
|
+
name: "pageSize".into(),
|
|
387
|
+
type_info: RouteTypeInfo::Primitive("number".into()),
|
|
388
|
+
optional: true,
|
|
389
|
+
},
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
let type_map = HashMap::new();
|
|
393
|
+
let spec = generate_openapi(&[entry], &type_map);
|
|
394
|
+
let parsed: Value = serde_json::from_str(&spec).unwrap();
|
|
395
|
+
|
|
396
|
+
let params = &parsed["paths"]["/users"]["get"]["parameters"];
|
|
397
|
+
assert_eq!(params.as_array().unwrap().len(), 2);
|
|
398
|
+
assert_eq!(params[0]["name"], "page");
|
|
399
|
+
assert_eq!(params[0]["in"], "query");
|
|
400
|
+
assert_eq!(params[0]["required"], false);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#[test]
|
|
404
|
+
fn test_generate_openapi_request_body() {
|
|
405
|
+
let mut entry = make_entry("POST", "/users");
|
|
406
|
+
entry.body_type = RouteTypeInfo::Named("CreateUserInput".into());
|
|
407
|
+
|
|
408
|
+
let type_map = HashMap::new();
|
|
409
|
+
let spec = generate_openapi(&[entry], &type_map);
|
|
410
|
+
let parsed: Value = serde_json::from_str(&spec).unwrap();
|
|
411
|
+
|
|
412
|
+
let body = &parsed["paths"]["/users"]["post"]["requestBody"];
|
|
413
|
+
assert_eq!(body["required"], true);
|
|
414
|
+
assert!(body["content"]["application/json"]["schema"]["$ref"]
|
|
415
|
+
.as_str()
|
|
416
|
+
.unwrap()
|
|
417
|
+
.contains("CreateUserInput"));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[test]
|
|
421
|
+
fn test_generate_openapi_response_type_with_schema() {
|
|
422
|
+
let mut entry = make_entry("GET", "/users");
|
|
423
|
+
entry.response_type = RouteTypeInfo::Named("PublicUser".into());
|
|
424
|
+
|
|
425
|
+
let mut type_map = HashMap::new();
|
|
426
|
+
type_map.insert(
|
|
427
|
+
"PublicUser".into(),
|
|
428
|
+
TypeMetadata {
|
|
429
|
+
name: "PublicUser".into(),
|
|
430
|
+
properties: {
|
|
431
|
+
let mut props = HashMap::new();
|
|
432
|
+
props.insert(
|
|
433
|
+
"id".into(),
|
|
434
|
+
PropertyMetadata {
|
|
435
|
+
type_str: "string".into(),
|
|
436
|
+
optional: false,
|
|
437
|
+
jsdoc: None,
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
props.insert(
|
|
441
|
+
"name".into(),
|
|
442
|
+
PropertyMetadata {
|
|
443
|
+
type_str: "string".into(),
|
|
444
|
+
optional: false,
|
|
445
|
+
jsdoc: None,
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
props
|
|
449
|
+
},
|
|
450
|
+
jsdoc: None,
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
let spec = generate_openapi(&[entry], &type_map);
|
|
455
|
+
let parsed: Value = serde_json::from_str(&spec).unwrap();
|
|
456
|
+
|
|
457
|
+
// Check response references the schema
|
|
458
|
+
let schema = &parsed["paths"]["/users"]["get"]["responses"]["200"]["content"]
|
|
459
|
+
["application/json"]["schema"];
|
|
460
|
+
assert!(schema["$ref"].as_str().unwrap().contains("PublicUser"));
|
|
461
|
+
|
|
462
|
+
// Check component schema exists
|
|
463
|
+
let component = &parsed["components"]["schemas"]["PublicUser"];
|
|
464
|
+
assert_eq!(component["type"], "object");
|
|
465
|
+
assert!(component["properties"]["id"].is_object());
|
|
466
|
+
assert!(component["properties"]["name"].is_object());
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#[test]
|
|
470
|
+
fn test_generate_openapi_validates_structure() {
|
|
471
|
+
let entries = vec![
|
|
472
|
+
make_entry("GET", "/users"),
|
|
473
|
+
make_entry("POST", "/users"),
|
|
474
|
+
make_entry("GET", "/users/:id"),
|
|
475
|
+
];
|
|
476
|
+
let type_map = HashMap::new();
|
|
477
|
+
let spec = generate_openapi(&entries, &type_map);
|
|
478
|
+
let parsed: Value = serde_json::from_str(&spec).unwrap();
|
|
479
|
+
|
|
480
|
+
// Basic OpenAPI 3.1 structure validation
|
|
481
|
+
assert_eq!(parsed["openapi"], "3.1.0");
|
|
482
|
+
assert!(parsed["info"]["title"].is_string());
|
|
483
|
+
assert!(parsed["info"]["version"].is_string());
|
|
484
|
+
assert!(parsed["paths"].is_object());
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
#[test]
|
|
488
|
+
fn test_generate_openapi_operation_ids() {
|
|
489
|
+
let entries = vec![
|
|
490
|
+
make_entry("GET", "/users"),
|
|
491
|
+
make_entry("POST", "/users"),
|
|
492
|
+
make_entry("GET", "/users/:id"),
|
|
493
|
+
];
|
|
494
|
+
let type_map = HashMap::new();
|
|
495
|
+
let spec = generate_openapi(&entries, &type_map);
|
|
496
|
+
let parsed: Value = serde_json::from_str(&spec).unwrap();
|
|
497
|
+
|
|
498
|
+
assert_eq!(
|
|
499
|
+
parsed["paths"]["/users"]["get"]["operationId"],
|
|
500
|
+
"getUsers"
|
|
501
|
+
);
|
|
502
|
+
assert_eq!(
|
|
503
|
+
parsed["paths"]["/users"]["post"]["operationId"],
|
|
504
|
+
"postUsers"
|
|
505
|
+
);
|
|
506
|
+
assert_eq!(
|
|
507
|
+
parsed["paths"]["/users/{id}"]["get"]["operationId"],
|
|
508
|
+
"getUsersId"
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fn make_entry(method: &str, path: &str) -> RouteEntry {
|
|
513
|
+
let segments = parse_path_segments(path);
|
|
514
|
+
RouteEntry {
|
|
515
|
+
method: method.to_string(),
|
|
516
|
+
path: path.to_string(),
|
|
517
|
+
segments,
|
|
518
|
+
handler_ref: format!("test#{} {}", method, path),
|
|
519
|
+
params_type: RouteTypeInfo::Void,
|
|
520
|
+
query_type: RouteTypeInfo::Void,
|
|
521
|
+
body_type: RouteTypeInfo::Void,
|
|
522
|
+
response_type: RouteTypeInfo::Void,
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|