@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.
@@ -0,0 +1,615 @@
1
+ use std::collections::BTreeMap;
2
+ use serde::{Serialize, Deserialize};
3
+ use swc_ecma_ast::*;
4
+
5
+ // ─── Type Representation for Route Contract Parameters ───────
6
+
7
+ /// Rich type info extracted from RouteContract type parameters
8
+ #[derive(Debug, Clone)]
9
+ pub enum RouteTypeInfo {
10
+ Void,
11
+ Primitive(String),
12
+ Named(String),
13
+ Generic(String, Vec<RouteTypeInfo>),
14
+ Array(Box<RouteTypeInfo>),
15
+ ObjectLiteral(Vec<RouteObjectProp>),
16
+ Union(Vec<RouteTypeInfo>),
17
+ StringLiteral(String),
18
+ NumberLiteral(f64),
19
+ BooleanLiteral(bool),
20
+ }
21
+
22
+ #[derive(Debug, Clone)]
23
+ pub struct RouteObjectProp {
24
+ pub name: String,
25
+ pub type_info: RouteTypeInfo,
26
+ pub optional: bool,
27
+ }
28
+
29
+ // ─── Route Entry ─────────────────────────────────────────────
30
+
31
+ /// A parsed route entry from a route contract interface
32
+ #[derive(Debug, Clone)]
33
+ pub struct RouteEntry {
34
+ pub method: String,
35
+ pub path: String,
36
+ pub segments: Vec<PathSegment>,
37
+ pub handler_ref: String,
38
+ pub params_type: RouteTypeInfo,
39
+ pub query_type: RouteTypeInfo,
40
+ pub body_type: RouteTypeInfo,
41
+ pub response_type: RouteTypeInfo,
42
+ }
43
+
44
+ /// A segment in a route path
45
+ #[derive(Debug, Clone)]
46
+ pub enum PathSegment {
47
+ Static(String),
48
+ Param(String),
49
+ Wildcard(String),
50
+ }
51
+
52
+ // ─── Compiled Route Tree ─────────────────────────────────────
53
+
54
+ /// Handler entry in the compiled route table
55
+ #[derive(Debug, Clone, Serialize, Deserialize)]
56
+ pub struct RouteHandlerEntry {
57
+ #[serde(rename = "ref")]
58
+ pub ref_str: String,
59
+ pub middleware: Vec<String>,
60
+ }
61
+
62
+ /// A compiled radix tree node (matches @typokit/types CompiledRoute)
63
+ #[derive(Debug, Clone, Serialize, Deserialize)]
64
+ pub struct RouteNode {
65
+ pub segment: String,
66
+ #[serde(skip_serializing_if = "Option::is_none", rename = "paramName")]
67
+ pub param_name: Option<String>,
68
+ #[serde(skip_serializing_if = "Option::is_none")]
69
+ pub children: Option<BTreeMap<String, RouteNode>>,
70
+ #[serde(skip_serializing_if = "Option::is_none", rename = "paramChild")]
71
+ pub param_child: Option<Box<RouteNode>>,
72
+ #[serde(skip_serializing_if = "Option::is_none", rename = "wildcardChild")]
73
+ pub wildcard_child: Option<Box<RouteNode>>,
74
+ #[serde(skip_serializing_if = "Option::is_none")]
75
+ pub handlers: Option<BTreeMap<String, RouteHandlerEntry>>,
76
+ }
77
+
78
+ impl RouteNode {
79
+ pub fn new(segment: &str) -> Self {
80
+ Self {
81
+ segment: segment.to_string(),
82
+ param_name: None,
83
+ children: None,
84
+ param_child: None,
85
+ wildcard_child: None,
86
+ handlers: None,
87
+ }
88
+ }
89
+ }
90
+
91
+ // ─── Route Contract Extraction ───────────────────────────────
92
+
93
+ /// Extract route contracts from a parsed TypeScript module.
94
+ /// Looks for interfaces with string literal property keys matching "METHOD /path".
95
+ pub fn extract_route_contracts(module: &Module) -> Vec<RouteEntry> {
96
+ let mut entries = Vec::new();
97
+
98
+ for item in &module.body {
99
+ let iface = match item {
100
+ ModuleItem::Stmt(Stmt::Decl(Decl::TsInterface(iface))) => iface,
101
+ ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => {
102
+ if let Decl::TsInterface(iface) = &export.decl {
103
+ iface
104
+ } else {
105
+ continue;
106
+ }
107
+ }
108
+ _ => continue,
109
+ };
110
+
111
+ let iface_name = iface.id.sym.to_string();
112
+
113
+ for member in &iface.body.body {
114
+ if let TsTypeElement::TsPropertySignature(prop) = member {
115
+ let key_str = match &*prop.key {
116
+ Expr::Lit(Lit::Str(s)) => s.value.to_string_lossy().to_string(),
117
+ _ => continue,
118
+ };
119
+
120
+ let (method, path) = match parse_route_key(&key_str) {
121
+ Some(v) => v,
122
+ None => continue,
123
+ };
124
+
125
+ let (params_type, query_type, body_type, response_type) =
126
+ extract_route_contract_type_params(prop);
127
+
128
+ let segments = parse_path_segments(&path);
129
+ let handler_ref = format!("{}#{}", iface_name, key_str);
130
+
131
+ entries.push(RouteEntry {
132
+ method,
133
+ path,
134
+ segments,
135
+ handler_ref,
136
+ params_type,
137
+ query_type,
138
+ body_type,
139
+ response_type,
140
+ });
141
+ }
142
+ }
143
+ }
144
+
145
+ entries
146
+ }
147
+
148
+ fn parse_route_key(key: &str) -> Option<(String, String)> {
149
+ let parts: Vec<&str> = key.splitn(2, ' ').collect();
150
+ if parts.len() != 2 {
151
+ return None;
152
+ }
153
+ let method = parts[0].to_uppercase();
154
+ let path = parts[1].to_string();
155
+ match method.as_str() {
156
+ "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" => {}
157
+ _ => return None,
158
+ }
159
+ Some((method, path))
160
+ }
161
+
162
+ /// Parse a URL path into segments
163
+ pub fn parse_path_segments(path: &str) -> Vec<PathSegment> {
164
+ path.split('/')
165
+ .filter(|s| !s.is_empty())
166
+ .map(|s| {
167
+ if s.starts_with(':') {
168
+ PathSegment::Param(s[1..].to_string())
169
+ } else if s.starts_with('*') {
170
+ PathSegment::Wildcard(
171
+ if s.len() > 1 { s[1..].to_string() } else { "wildcard".to_string() },
172
+ )
173
+ } else {
174
+ PathSegment::Static(s.to_string())
175
+ }
176
+ })
177
+ .collect()
178
+ }
179
+
180
+ fn extract_route_contract_type_params(
181
+ prop: &TsPropertySignature,
182
+ ) -> (RouteTypeInfo, RouteTypeInfo, RouteTypeInfo, RouteTypeInfo) {
183
+ let default = (
184
+ RouteTypeInfo::Void,
185
+ RouteTypeInfo::Void,
186
+ RouteTypeInfo::Void,
187
+ RouteTypeInfo::Void,
188
+ );
189
+
190
+ let type_ann = match &prop.type_ann {
191
+ Some(ann) => &ann.type_ann,
192
+ None => return default,
193
+ };
194
+
195
+ if let TsType::TsTypeRef(type_ref) = &**type_ann {
196
+ let name = match &type_ref.type_name {
197
+ TsEntityName::Ident(ident) => ident.sym.to_string(),
198
+ _ => return default,
199
+ };
200
+
201
+ if name != "RouteContract" {
202
+ return default;
203
+ }
204
+
205
+ if let Some(type_params) = &type_ref.type_params {
206
+ let infos: Vec<RouteTypeInfo> = type_params
207
+ .params
208
+ .iter()
209
+ .map(|p| ts_type_to_route_info(p))
210
+ .collect();
211
+
212
+ return (
213
+ infos.first().cloned().unwrap_or(RouteTypeInfo::Void),
214
+ infos.get(1).cloned().unwrap_or(RouteTypeInfo::Void),
215
+ infos.get(2).cloned().unwrap_or(RouteTypeInfo::Void),
216
+ infos.get(3).cloned().unwrap_or(RouteTypeInfo::Void),
217
+ );
218
+ }
219
+ }
220
+
221
+ default
222
+ }
223
+
224
+ /// Convert a TsType AST node to a RouteTypeInfo representation
225
+ pub fn ts_type_to_route_info(ts_type: &TsType) -> RouteTypeInfo {
226
+ match ts_type {
227
+ TsType::TsKeywordType(kw) => match kw.kind {
228
+ TsKeywordTypeKind::TsVoidKeyword => RouteTypeInfo::Void,
229
+ TsKeywordTypeKind::TsStringKeyword => RouteTypeInfo::Primitive("string".into()),
230
+ TsKeywordTypeKind::TsNumberKeyword => RouteTypeInfo::Primitive("number".into()),
231
+ TsKeywordTypeKind::TsBooleanKeyword => RouteTypeInfo::Primitive("boolean".into()),
232
+ TsKeywordTypeKind::TsAnyKeyword => RouteTypeInfo::Primitive("any".into()),
233
+ TsKeywordTypeKind::TsUnknownKeyword => RouteTypeInfo::Primitive("unknown".into()),
234
+ TsKeywordTypeKind::TsNeverKeyword => RouteTypeInfo::Primitive("never".into()),
235
+ TsKeywordTypeKind::TsNullKeyword => RouteTypeInfo::Primitive("null".into()),
236
+ TsKeywordTypeKind::TsUndefinedKeyword => RouteTypeInfo::Primitive("undefined".into()),
237
+ _ => RouteTypeInfo::Primitive("unknown".into()),
238
+ },
239
+ TsType::TsTypeRef(type_ref) => {
240
+ let name = match &type_ref.type_name {
241
+ TsEntityName::Ident(ident) => ident.sym.to_string(),
242
+ TsEntityName::TsQualifiedName(qn) => qualified_name_to_string(qn),
243
+ };
244
+
245
+ if let Some(params) = &type_ref.type_params {
246
+ let args: Vec<RouteTypeInfo> =
247
+ params.params.iter().map(|p| ts_type_to_route_info(p)).collect();
248
+
249
+ // Normalize Array<T> to Array(T)
250
+ if name == "Array" && args.len() == 1 {
251
+ return RouteTypeInfo::Array(Box::new(args.into_iter().next().unwrap()));
252
+ }
253
+
254
+ RouteTypeInfo::Generic(name, args)
255
+ } else {
256
+ RouteTypeInfo::Named(name)
257
+ }
258
+ }
259
+ TsType::TsArrayType(arr) => {
260
+ RouteTypeInfo::Array(Box::new(ts_type_to_route_info(&arr.elem_type)))
261
+ }
262
+ TsType::TsTypeLit(type_lit) => {
263
+ let props = type_lit
264
+ .members
265
+ .iter()
266
+ .filter_map(|m| {
267
+ if let TsTypeElement::TsPropertySignature(prop) = m {
268
+ let name = match &*prop.key {
269
+ Expr::Ident(ident) => ident.sym.to_string(),
270
+ _ => return None,
271
+ };
272
+ let type_info = prop
273
+ .type_ann
274
+ .as_ref()
275
+ .map(|ann| ts_type_to_route_info(&ann.type_ann))
276
+ .unwrap_or(RouteTypeInfo::Primitive("unknown".into()));
277
+ Some(RouteObjectProp {
278
+ name,
279
+ type_info,
280
+ optional: prop.optional,
281
+ })
282
+ } else {
283
+ None
284
+ }
285
+ })
286
+ .collect();
287
+ RouteTypeInfo::ObjectLiteral(props)
288
+ }
289
+ TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(union)) => {
290
+ RouteTypeInfo::Union(
291
+ union.types.iter().map(|t| ts_type_to_route_info(t)).collect(),
292
+ )
293
+ }
294
+ TsType::TsLitType(lit) => match &lit.lit {
295
+ TsLit::Str(s) => RouteTypeInfo::StringLiteral(s.value.to_string_lossy().to_string()),
296
+ TsLit::Number(n) => RouteTypeInfo::NumberLiteral(n.value),
297
+ TsLit::Bool(b) => RouteTypeInfo::BooleanLiteral(b.value),
298
+ _ => RouteTypeInfo::Primitive("unknown".into()),
299
+ },
300
+ TsType::TsParenthesizedType(paren) => ts_type_to_route_info(&paren.type_ann),
301
+ _ => RouteTypeInfo::Primitive("unknown".into()),
302
+ }
303
+ }
304
+
305
+ fn qualified_name_to_string(qn: &TsQualifiedName) -> String {
306
+ let left = match &qn.left {
307
+ TsEntityName::Ident(ident) => ident.sym.to_string(),
308
+ TsEntityName::TsQualifiedName(qn) => qualified_name_to_string(qn),
309
+ };
310
+ format!("{}.{}", left, qn.right.sym)
311
+ }
312
+
313
+ // ─── Radix Tree Construction ─────────────────────────────────
314
+
315
+ /// Build a radix tree from route entries.
316
+ /// Returns an error on ambiguous routes (conflicting param names at same level).
317
+ pub fn build_radix_tree(entries: &[RouteEntry]) -> Result<RouteNode, String> {
318
+ let mut root = RouteNode::new("");
319
+
320
+ for entry in entries {
321
+ insert_route(
322
+ &mut root,
323
+ &entry.segments,
324
+ 0,
325
+ &entry.method,
326
+ &RouteHandlerEntry {
327
+ ref_str: entry.handler_ref.clone(),
328
+ middleware: vec![],
329
+ },
330
+ )?;
331
+ }
332
+
333
+ Ok(root)
334
+ }
335
+
336
+ fn insert_route(
337
+ node: &mut RouteNode,
338
+ segments: &[PathSegment],
339
+ index: usize,
340
+ method: &str,
341
+ handler: &RouteHandlerEntry,
342
+ ) -> Result<(), String> {
343
+ if index >= segments.len() {
344
+ let handlers = node.handlers.get_or_insert_with(BTreeMap::new);
345
+ if handlers.contains_key(method) {
346
+ return Err(format!(
347
+ "Duplicate route: {} handler already defined at this path",
348
+ method
349
+ ));
350
+ }
351
+ handlers.insert(method.to_string(), handler.clone());
352
+ return Ok(());
353
+ }
354
+
355
+ match &segments[index] {
356
+ PathSegment::Static(name) => {
357
+ let children = node.children.get_or_insert_with(BTreeMap::new);
358
+ let child = children
359
+ .entry(name.clone())
360
+ .or_insert_with(|| RouteNode::new(name));
361
+ insert_route(child, segments, index + 1, method, handler)
362
+ }
363
+ PathSegment::Param(param_name) => {
364
+ if let Some(existing) = &node.param_child {
365
+ let existing_name = existing.param_name.as_ref().unwrap();
366
+ if existing_name != param_name {
367
+ return Err(format!(
368
+ "Ambiguous route: param ':{}' conflicts with existing param ':{}' at the same path level",
369
+ param_name, existing_name
370
+ ));
371
+ }
372
+ }
373
+
374
+ if node.param_child.is_none() {
375
+ let mut param_node = RouteNode::new("");
376
+ param_node.param_name = Some(param_name.clone());
377
+ node.param_child = Some(Box::new(param_node));
378
+ }
379
+
380
+ insert_route(
381
+ node.param_child.as_mut().unwrap(),
382
+ segments,
383
+ index + 1,
384
+ method,
385
+ handler,
386
+ )
387
+ }
388
+ PathSegment::Wildcard(param_name) => {
389
+ if node.wildcard_child.is_some() {
390
+ return Err(format!(
391
+ "Ambiguous route: wildcard '*{}' conflicts with existing wildcard at the same path level",
392
+ param_name
393
+ ));
394
+ }
395
+
396
+ let mut wildcard_node = RouteNode::new("");
397
+ wildcard_node.param_name = Some(param_name.clone());
398
+ let handlers = wildcard_node.handlers.get_or_insert_with(BTreeMap::new);
399
+ handlers.insert(method.to_string(), handler.clone());
400
+ node.wildcard_child = Some(Box::new(wildcard_node));
401
+ Ok(())
402
+ }
403
+ }
404
+ }
405
+
406
+ // ─── TypeScript Serialization ────────────────────────────────
407
+
408
+ /// Serialize the compiled route tree as TypeScript code.
409
+ pub fn serialize_to_typescript(root: &RouteNode) -> String {
410
+ let json = serde_json::to_string_pretty(root).unwrap();
411
+ format!(
412
+ "// AUTO-GENERATED by @typokit/transform-native — DO NOT EDIT\n\
413
+ import type {{ CompiledRouteTable }} from \"@typokit/types\";\n\
414
+ \n\
415
+ export const routeTree: CompiledRouteTable = {} as const;\n",
416
+ json
417
+ )
418
+ }
419
+
420
+ #[cfg(test)]
421
+ mod tests {
422
+ use super::*;
423
+ use crate::parser::parse_typescript;
424
+
425
+ #[test]
426
+ fn test_extract_route_contracts_basic() {
427
+ let source = r#"
428
+ interface UsersRoutes {
429
+ "GET /users": RouteContract<void, void, void, PublicUser[]>;
430
+ "POST /users": RouteContract<void, void, CreateUserInput, PublicUser>;
431
+ "GET /users/:id": RouteContract<{ id: string }, void, void, PublicUser>;
432
+ }
433
+ "#;
434
+ let parsed = parse_typescript("test.ts", source).unwrap();
435
+ let entries = extract_route_contracts(&parsed.module);
436
+
437
+ assert_eq!(entries.len(), 3);
438
+ assert_eq!(entries[0].method, "GET");
439
+ assert_eq!(entries[0].path, "/users");
440
+ assert_eq!(entries[1].method, "POST");
441
+ assert_eq!(entries[1].path, "/users");
442
+ assert_eq!(entries[2].method, "GET");
443
+ assert_eq!(entries[2].path, "/users/:id");
444
+ }
445
+
446
+ #[test]
447
+ fn test_extract_route_contracts_with_query_params() {
448
+ let source = r#"
449
+ interface UsersRoutes {
450
+ "GET /users": RouteContract<void, { page?: number; pageSize?: number }, void, void>;
451
+ }
452
+ "#;
453
+ let parsed = parse_typescript("test.ts", source).unwrap();
454
+ let entries = extract_route_contracts(&parsed.module);
455
+
456
+ assert_eq!(entries.len(), 1);
457
+ match &entries[0].query_type {
458
+ RouteTypeInfo::ObjectLiteral(props) => {
459
+ assert_eq!(props.len(), 2);
460
+ assert_eq!(props[0].name, "page");
461
+ assert!(props[0].optional);
462
+ assert_eq!(props[1].name, "pageSize");
463
+ assert!(props[1].optional);
464
+ }
465
+ _ => panic!("Expected ObjectLiteral for query type"),
466
+ }
467
+ }
468
+
469
+ #[test]
470
+ fn test_extract_route_contracts_exported_interface() {
471
+ let source = r#"
472
+ export interface HealthRoutes {
473
+ "GET /health": RouteContract<void, void, void, { status: string }>;
474
+ }
475
+ "#;
476
+ let parsed = parse_typescript("test.ts", source).unwrap();
477
+ let entries = extract_route_contracts(&parsed.module);
478
+
479
+ assert_eq!(entries.len(), 1);
480
+ assert_eq!(entries[0].method, "GET");
481
+ assert_eq!(entries[0].path, "/health");
482
+ assert!(entries[0].handler_ref.contains("HealthRoutes"));
483
+ }
484
+
485
+ #[test]
486
+ fn test_extract_ignores_non_route_interfaces() {
487
+ let source = r#"
488
+ interface User {
489
+ id: string;
490
+ name: string;
491
+ }
492
+ interface UsersRoutes {
493
+ "GET /users": RouteContract<void, void, void, void>;
494
+ }
495
+ "#;
496
+ let parsed = parse_typescript("test.ts", source).unwrap();
497
+ let entries = extract_route_contracts(&parsed.module);
498
+
499
+ assert_eq!(entries.len(), 1);
500
+ assert_eq!(entries[0].path, "/users");
501
+ }
502
+
503
+ #[test]
504
+ fn test_build_radix_tree_basic() {
505
+ let entries = vec![
506
+ make_entry("GET", "/users"),
507
+ make_entry("POST", "/users"),
508
+ make_entry("GET", "/users/:id"),
509
+ make_entry("GET", "/health"),
510
+ ];
511
+
512
+ let tree = build_radix_tree(&entries).unwrap();
513
+
514
+ assert_eq!(tree.segment, "");
515
+ let children = tree.children.as_ref().unwrap();
516
+ assert!(children.contains_key("users"));
517
+ assert!(children.contains_key("health"));
518
+
519
+ let users = &children["users"];
520
+ let handlers = users.handlers.as_ref().unwrap();
521
+ assert!(handlers.contains_key("GET"));
522
+ assert!(handlers.contains_key("POST"));
523
+
524
+ let param_child = users.param_child.as_ref().unwrap();
525
+ assert_eq!(param_child.param_name, Some("id".to_string()));
526
+ }
527
+
528
+ #[test]
529
+ fn test_build_radix_tree_nested_params() {
530
+ let entries = vec![make_entry("GET", "/users/:userId/posts/:postId")];
531
+
532
+ let tree = build_radix_tree(&entries).unwrap();
533
+
534
+ let users = &tree.children.as_ref().unwrap()["users"];
535
+ let user_param = users.param_child.as_ref().unwrap();
536
+ assert_eq!(user_param.param_name, Some("userId".to_string()));
537
+
538
+ let posts = &user_param.children.as_ref().unwrap()["posts"];
539
+ let post_param = posts.param_child.as_ref().unwrap();
540
+ assert_eq!(post_param.param_name, Some("postId".to_string()));
541
+ }
542
+
543
+ #[test]
544
+ fn test_build_radix_tree_ambiguous_params() {
545
+ let entries = vec![
546
+ make_entry("GET", "/users/:id"),
547
+ make_entry("GET", "/users/:userId/posts"),
548
+ ];
549
+
550
+ let result = build_radix_tree(&entries);
551
+ assert!(result.is_err());
552
+ assert!(result.unwrap_err().contains("Ambiguous"));
553
+ }
554
+
555
+ #[test]
556
+ fn test_build_radix_tree_wildcard() {
557
+ let entries = vec![make_entry("GET", "/files/*path")];
558
+
559
+ let tree = build_radix_tree(&entries).unwrap();
560
+ let files = &tree.children.as_ref().unwrap()["files"];
561
+ let wildcard = files.wildcard_child.as_ref().unwrap();
562
+ assert_eq!(wildcard.param_name, Some("path".to_string()));
563
+ }
564
+
565
+ #[test]
566
+ fn test_build_radix_tree_static_and_param_coexist() {
567
+ let entries = vec![
568
+ make_entry("GET", "/users/me"),
569
+ make_entry("GET", "/users/:id"),
570
+ ];
571
+
572
+ let tree = build_radix_tree(&entries).unwrap();
573
+ let users = &tree.children.as_ref().unwrap()["users"];
574
+
575
+ // Static "me" child
576
+ assert!(users.children.as_ref().unwrap().contains_key("me"));
577
+ // Param child
578
+ assert!(users.param_child.is_some());
579
+ }
580
+
581
+ #[test]
582
+ fn test_serialize_to_typescript() {
583
+ let entries = vec![make_entry("GET", "/health")];
584
+ let tree = build_radix_tree(&entries).unwrap();
585
+ let ts = serialize_to_typescript(&tree);
586
+
587
+ assert!(ts.contains("AUTO-GENERATED"));
588
+ assert!(ts.contains("CompiledRouteTable"));
589
+ assert!(ts.contains("routeTree"));
590
+ assert!(ts.contains("health"));
591
+ }
592
+
593
+ #[test]
594
+ fn test_duplicate_handler_error() {
595
+ let entries = vec![make_entry("GET", "/users"), make_entry("GET", "/users")];
596
+
597
+ let result = build_radix_tree(&entries);
598
+ assert!(result.is_err());
599
+ assert!(result.unwrap_err().contains("Duplicate"));
600
+ }
601
+
602
+ fn make_entry(method: &str, path: &str) -> RouteEntry {
603
+ let segments = parse_path_segments(path);
604
+ RouteEntry {
605
+ method: method.to_string(),
606
+ path: path.to_string(),
607
+ segments,
608
+ handler_ref: format!("test#{} {}", method, path),
609
+ params_type: RouteTypeInfo::Void,
610
+ query_type: RouteTypeInfo::Void,
611
+ body_type: RouteTypeInfo::Void,
612
+ response_type: RouteTypeInfo::Void,
613
+ }
614
+ }
615
+ }