@typokit/plugin-axum 0.2.1
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 +81 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.linux-x64-musl.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +57 -0
- package/src/index.ts +309 -0
- package/src/lib.rs +80 -0
- package/src/rust_codegen/database.rs +898 -0
- package/src/rust_codegen/handlers.rs +1111 -0
- package/src/rust_codegen/middleware.rs +156 -0
- package/src/rust_codegen/mod.rs +91 -0
- package/src/rust_codegen/project.rs +593 -0
- package/src/rust_codegen/router.rs +385 -0
- package/src/rust_codegen/services.rs +476 -0
- package/src/rust_codegen/structs.rs +1363 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
|
|
3
|
+
use typokit_transform_native::route_compiler::RouteEntry;
|
|
4
|
+
use super::GeneratedOutput;
|
|
5
|
+
|
|
6
|
+
/// Generate the Axum router file from compiled route entries.
|
|
7
|
+
///
|
|
8
|
+
/// Produces `.typokit/router.rs` containing a `pub fn create_router() -> Router<AppState>`
|
|
9
|
+
/// function with typed Axum handler registrations.
|
|
10
|
+
pub fn generate_router(routes: &[RouteEntry]) -> Vec<GeneratedOutput> {
|
|
11
|
+
let mut outputs = Vec::new();
|
|
12
|
+
|
|
13
|
+
let content = generate_router_file(routes);
|
|
14
|
+
outputs.push(GeneratedOutput {
|
|
15
|
+
path: ".typokit/router.rs".to_string(),
|
|
16
|
+
content,
|
|
17
|
+
overwrite: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
outputs
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Generate the router.rs file content.
|
|
24
|
+
fn generate_router_file(routes: &[RouteEntry]) -> String {
|
|
25
|
+
let mut output = String::new();
|
|
26
|
+
|
|
27
|
+
output.push_str("// AUTO-GENERATED by @typokit/transform-native — DO NOT EDIT\n\n");
|
|
28
|
+
|
|
29
|
+
// Collect unique handler module references for imports
|
|
30
|
+
let handler_modules = collect_handler_modules(routes);
|
|
31
|
+
|
|
32
|
+
// Imports
|
|
33
|
+
output.push_str("use axum::{\n");
|
|
34
|
+
output.push_str(" Router,\n");
|
|
35
|
+
|
|
36
|
+
// Collect needed routing methods
|
|
37
|
+
let methods = collect_routing_methods(routes);
|
|
38
|
+
if !methods.is_empty() {
|
|
39
|
+
output.push_str(&format!(" routing::{{{}}},\n", methods.join(", ")));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
output.push_str("};\n");
|
|
43
|
+
output.push_str("use crate::app::AppState;\n");
|
|
44
|
+
|
|
45
|
+
// Import handler modules
|
|
46
|
+
for module in &handler_modules {
|
|
47
|
+
output.push_str(&format!("use crate::handlers::{};\n", module));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
output.push_str("\n");
|
|
51
|
+
|
|
52
|
+
// Generate create_router function
|
|
53
|
+
output.push_str("/// Create the application router with all route registrations.\n");
|
|
54
|
+
output.push_str("pub fn create_router() -> Router<AppState> {\n");
|
|
55
|
+
output.push_str(" Router::new()\n");
|
|
56
|
+
|
|
57
|
+
// Group routes by path for chaining
|
|
58
|
+
let grouped = group_routes_by_path(routes);
|
|
59
|
+
for (path, route_methods) in &grouped {
|
|
60
|
+
let axum_path = to_axum_path(path);
|
|
61
|
+
let method_chains: Vec<String> = route_methods
|
|
62
|
+
.iter()
|
|
63
|
+
.map(|(method, handler_fn)| format!("{}({})", method, handler_fn))
|
|
64
|
+
.collect();
|
|
65
|
+
|
|
66
|
+
if method_chains.len() == 1 {
|
|
67
|
+
output.push_str(&format!(
|
|
68
|
+
" .route(\"{}\", {})\n",
|
|
69
|
+
axum_path, method_chains[0]
|
|
70
|
+
));
|
|
71
|
+
} else {
|
|
72
|
+
// Multiple methods on same path: chain with .method()
|
|
73
|
+
let chained = method_chains.join(".");
|
|
74
|
+
output.push_str(&format!(
|
|
75
|
+
" .route(\"{}\", {chained})\n",
|
|
76
|
+
axum_path,
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
output.push_str("}\n");
|
|
82
|
+
|
|
83
|
+
output
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Collect unique handler module names from route entries.
|
|
87
|
+
///
|
|
88
|
+
/// Derives entity module names from the handler_ref (e.g., "UsersRoutes#GET /users" → "users").
|
|
89
|
+
fn collect_handler_modules(routes: &[RouteEntry]) -> Vec<String> {
|
|
90
|
+
let mut modules = BTreeMap::new();
|
|
91
|
+
for route in routes {
|
|
92
|
+
let module = derive_handler_module(&route.handler_ref);
|
|
93
|
+
modules.entry(module).or_insert(());
|
|
94
|
+
}
|
|
95
|
+
modules.into_keys().collect()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Collect unique Axum routing method names needed (get, post, put, patch, delete).
|
|
99
|
+
fn collect_routing_methods(routes: &[RouteEntry]) -> Vec<String> {
|
|
100
|
+
let mut methods = BTreeMap::new();
|
|
101
|
+
for route in routes {
|
|
102
|
+
let method = route.method.to_lowercase();
|
|
103
|
+
methods.entry(method).or_insert(());
|
|
104
|
+
}
|
|
105
|
+
methods.into_keys().collect()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Group routes by path, collecting (axum_method_fn, handler_fn) pairs per path.
|
|
109
|
+
///
|
|
110
|
+
/// Uses BTreeMap for deterministic ordering.
|
|
111
|
+
fn group_routes_by_path(routes: &[RouteEntry]) -> BTreeMap<String, Vec<(String, String)>> {
|
|
112
|
+
let mut grouped: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
|
|
113
|
+
|
|
114
|
+
// Sort routes for deterministic output
|
|
115
|
+
let mut sorted_routes: Vec<&RouteEntry> = routes.iter().collect();
|
|
116
|
+
sorted_routes.sort_by(|a, b| a.path.cmp(&b.path).then(a.method.cmp(&b.method)));
|
|
117
|
+
|
|
118
|
+
for route in sorted_routes {
|
|
119
|
+
let method_fn = route.method.to_lowercase();
|
|
120
|
+
let handler_fn = derive_handler_fn(route);
|
|
121
|
+
grouped
|
|
122
|
+
.entry(route.path.clone())
|
|
123
|
+
.or_default()
|
|
124
|
+
.push((method_fn, handler_fn));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
grouped
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Derive the handler module name from a handler_ref.
|
|
131
|
+
///
|
|
132
|
+
/// Handler refs follow the pattern "EntityRoutes#METHOD /path" — we extract the entity
|
|
133
|
+
/// name, strip "Routes" suffix, and convert to snake_case for the module name.
|
|
134
|
+
fn derive_handler_module(handler_ref: &str) -> String {
|
|
135
|
+
// Split on '#' to get the contract interface name
|
|
136
|
+
let contract_name = handler_ref
|
|
137
|
+
.split('#')
|
|
138
|
+
.next()
|
|
139
|
+
.unwrap_or(handler_ref);
|
|
140
|
+
|
|
141
|
+
// Strip "Routes" suffix if present
|
|
142
|
+
let entity = contract_name
|
|
143
|
+
.strip_suffix("Routes")
|
|
144
|
+
.or_else(|| contract_name.strip_suffix("Route"))
|
|
145
|
+
.unwrap_or(contract_name);
|
|
146
|
+
|
|
147
|
+
to_snake_case(entity)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Derive the fully qualified handler function reference from a route entry.
|
|
151
|
+
///
|
|
152
|
+
/// Maps HTTP method + path to handler function following the pattern:
|
|
153
|
+
/// handlers::{entity}::{action}
|
|
154
|
+
fn derive_handler_fn(route: &RouteEntry) -> String {
|
|
155
|
+
let module = derive_handler_module(&route.handler_ref);
|
|
156
|
+
let action = derive_action_name(route);
|
|
157
|
+
format!("{}::{}", module, action)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Derive the handler action name from the HTTP method and path.
|
|
161
|
+
///
|
|
162
|
+
/// Follows REST conventions:
|
|
163
|
+
/// - GET /entities → list
|
|
164
|
+
/// - POST /entities → create
|
|
165
|
+
/// - GET /entities/:id → get_by_id
|
|
166
|
+
/// - PUT /entities/:id → update
|
|
167
|
+
/// - PATCH /entities/:id → update
|
|
168
|
+
/// - DELETE /entities/:id → delete
|
|
169
|
+
fn derive_action_name(route: &RouteEntry) -> String {
|
|
170
|
+
let has_param = route.segments.iter().any(|s| {
|
|
171
|
+
matches!(s, typokit_transform_native::route_compiler::PathSegment::Param(_))
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
match (route.method.as_str(), has_param) {
|
|
175
|
+
("GET", false) => "list".to_string(),
|
|
176
|
+
("GET", true) => "get_by_id".to_string(),
|
|
177
|
+
("POST", false) => "create".to_string(),
|
|
178
|
+
("PUT", true) | ("PATCH", true) => "update".to_string(),
|
|
179
|
+
("DELETE", true) => "delete".to_string(),
|
|
180
|
+
// Fallback: use method name as action
|
|
181
|
+
(method, _) => to_snake_case(&method.to_lowercase()),
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Convert Express-style path params (:id) to Axum-style (/:id).
|
|
186
|
+
///
|
|
187
|
+
/// Axum uses the same `:param` syntax as Express, so paths are mostly
|
|
188
|
+
/// compatible. We just ensure proper formatting.
|
|
189
|
+
fn to_axum_path(path: &str) -> String {
|
|
190
|
+
// Axum uses /:param syntax same as Express — paths are already compatible
|
|
191
|
+
path.to_string()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Convert a camelCase or PascalCase string to snake_case.
|
|
195
|
+
fn to_snake_case(s: &str) -> String {
|
|
196
|
+
let mut result = String::new();
|
|
197
|
+
for (i, c) in s.chars().enumerate() {
|
|
198
|
+
if c.is_uppercase() {
|
|
199
|
+
if i > 0 {
|
|
200
|
+
result.push('_');
|
|
201
|
+
}
|
|
202
|
+
result.push(c.to_lowercase().next().unwrap());
|
|
203
|
+
} else {
|
|
204
|
+
result.push(c);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
result
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[cfg(test)]
|
|
211
|
+
mod tests {
|
|
212
|
+
use super::*;
|
|
213
|
+
use typokit_transform_native::route_compiler::{PathSegment, RouteEntry, RouteTypeInfo};
|
|
214
|
+
|
|
215
|
+
fn make_route(method: &str, path: &str, handler_ref: &str) -> RouteEntry {
|
|
216
|
+
let segments = path
|
|
217
|
+
.split('/')
|
|
218
|
+
.filter(|s| !s.is_empty())
|
|
219
|
+
.map(|s| {
|
|
220
|
+
if let Some(param) = s.strip_prefix(':') {
|
|
221
|
+
PathSegment::Param(param.to_string())
|
|
222
|
+
} else {
|
|
223
|
+
PathSegment::Static(s.to_string())
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
.collect();
|
|
227
|
+
|
|
228
|
+
RouteEntry {
|
|
229
|
+
method: method.to_string(),
|
|
230
|
+
path: path.to_string(),
|
|
231
|
+
segments,
|
|
232
|
+
handler_ref: handler_ref.to_string(),
|
|
233
|
+
params_type: RouteTypeInfo::Void,
|
|
234
|
+
query_type: RouteTypeInfo::Void,
|
|
235
|
+
body_type: RouteTypeInfo::Void,
|
|
236
|
+
response_type: RouteTypeInfo::Void,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn test_generate_router_basic() {
|
|
242
|
+
let routes = vec![
|
|
243
|
+
make_route("GET", "/users", "UsersRoutes#GET /users"),
|
|
244
|
+
make_route("POST", "/users", "UsersRoutes#POST /users"),
|
|
245
|
+
make_route("GET", "/users/:id", "UsersRoutes#GET /users/:id"),
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
let outputs = generate_router(&routes);
|
|
249
|
+
assert_eq!(outputs.len(), 1);
|
|
250
|
+
assert_eq!(outputs[0].path, ".typokit/router.rs");
|
|
251
|
+
assert!(outputs[0].overwrite);
|
|
252
|
+
|
|
253
|
+
let content = &outputs[0].content;
|
|
254
|
+
assert!(content.contains("pub fn create_router() -> Router<AppState>"));
|
|
255
|
+
assert!(content.contains("use crate::handlers::users;"));
|
|
256
|
+
assert!(content.contains("users::list"));
|
|
257
|
+
assert!(content.contains("users::create"));
|
|
258
|
+
assert!(content.contains("users::get_by_id"));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn test_generate_router_multiple_entities() {
|
|
263
|
+
let routes = vec![
|
|
264
|
+
make_route("GET", "/users", "UsersRoutes#GET /users"),
|
|
265
|
+
make_route("GET", "/todos", "TodosRoutes#GET /todos"),
|
|
266
|
+
make_route("POST", "/todos", "TodosRoutes#POST /todos"),
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
let outputs = generate_router(&routes);
|
|
270
|
+
let content = &outputs[0].content;
|
|
271
|
+
|
|
272
|
+
assert!(content.contains("use crate::handlers::todos;"));
|
|
273
|
+
assert!(content.contains("use crate::handlers::users;"));
|
|
274
|
+
assert!(content.contains("todos::list"));
|
|
275
|
+
assert!(content.contains("todos::create"));
|
|
276
|
+
assert!(content.contains("users::list"));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[test]
|
|
280
|
+
fn test_generate_router_crud_operations() {
|
|
281
|
+
let routes = vec![
|
|
282
|
+
make_route("GET", "/users", "UsersRoutes#GET /users"),
|
|
283
|
+
make_route("POST", "/users", "UsersRoutes#POST /users"),
|
|
284
|
+
make_route("GET", "/users/:id", "UsersRoutes#GET /users/:id"),
|
|
285
|
+
make_route("PUT", "/users/:id", "UsersRoutes#PUT /users/:id"),
|
|
286
|
+
make_route("DELETE", "/users/:id", "UsersRoutes#DELETE /users/:id"),
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
let outputs = generate_router(&routes);
|
|
290
|
+
let content = &outputs[0].content;
|
|
291
|
+
|
|
292
|
+
assert!(content.contains("users::list"));
|
|
293
|
+
assert!(content.contains("users::create"));
|
|
294
|
+
assert!(content.contains("users::get_by_id"));
|
|
295
|
+
assert!(content.contains("users::update"));
|
|
296
|
+
assert!(content.contains("users::delete"));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#[test]
|
|
300
|
+
fn test_derive_handler_module() {
|
|
301
|
+
assert_eq!(derive_handler_module("UsersRoutes#GET /users"), "users");
|
|
302
|
+
assert_eq!(derive_handler_module("TodosRoutes#POST /todos"), "todos");
|
|
303
|
+
assert_eq!(
|
|
304
|
+
derive_handler_module("ProjectsRoute#GET /projects"),
|
|
305
|
+
"projects"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[test]
|
|
310
|
+
fn test_derive_action_name() {
|
|
311
|
+
let list = make_route("GET", "/users", "UsersRoutes#GET /users");
|
|
312
|
+
assert_eq!(derive_action_name(&list), "list");
|
|
313
|
+
|
|
314
|
+
let create = make_route("POST", "/users", "UsersRoutes#POST /users");
|
|
315
|
+
assert_eq!(derive_action_name(&create), "create");
|
|
316
|
+
|
|
317
|
+
let get = make_route("GET", "/users/:id", "UsersRoutes#GET /users/:id");
|
|
318
|
+
assert_eq!(derive_action_name(&get), "get_by_id");
|
|
319
|
+
|
|
320
|
+
let update = make_route("PUT", "/users/:id", "UsersRoutes#PUT /users/:id");
|
|
321
|
+
assert_eq!(derive_action_name(&update), "update");
|
|
322
|
+
|
|
323
|
+
let delete = make_route("DELETE", "/users/:id", "UsersRoutes#DELETE /users/:id");
|
|
324
|
+
assert_eq!(derive_action_name(&delete), "delete");
|
|
325
|
+
|
|
326
|
+
let patch = make_route("PATCH", "/users/:id", "UsersRoutes#PATCH /users/:id");
|
|
327
|
+
assert_eq!(derive_action_name(&patch), "update");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[test]
|
|
331
|
+
fn test_to_axum_path() {
|
|
332
|
+
assert_eq!(to_axum_path("/users"), "/users");
|
|
333
|
+
assert_eq!(to_axum_path("/users/:id"), "/users/:id");
|
|
334
|
+
assert_eq!(to_axum_path("/users/:id/todos"), "/users/:id/todos");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[test]
|
|
338
|
+
fn test_collect_routing_methods() {
|
|
339
|
+
let routes = vec![
|
|
340
|
+
make_route("GET", "/users", "UsersRoutes#GET /users"),
|
|
341
|
+
make_route("POST", "/users", "UsersRoutes#POST /users"),
|
|
342
|
+
make_route("GET", "/users/:id", "UsersRoutes#GET /users/:id"),
|
|
343
|
+
make_route("DELETE", "/users/:id", "UsersRoutes#DELETE /users/:id"),
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
let methods = collect_routing_methods(&routes);
|
|
347
|
+
assert_eq!(methods, vec!["delete", "get", "post"]);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[test]
|
|
351
|
+
fn test_same_path_multiple_methods() {
|
|
352
|
+
let routes = vec![
|
|
353
|
+
make_route("GET", "/users", "UsersRoutes#GET /users"),
|
|
354
|
+
make_route("POST", "/users", "UsersRoutes#POST /users"),
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
let outputs = generate_router(&routes);
|
|
358
|
+
let content = &outputs[0].content;
|
|
359
|
+
|
|
360
|
+
// Should combine methods on same path with method chaining
|
|
361
|
+
assert!(content.contains("get(users::list)"));
|
|
362
|
+
assert!(content.contains("post(users::create)"));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[test]
|
|
366
|
+
fn test_router_output_is_deterministic() {
|
|
367
|
+
let routes = vec![
|
|
368
|
+
make_route("DELETE", "/users/:id", "UsersRoutes#DELETE /users/:id"),
|
|
369
|
+
make_route("GET", "/users", "UsersRoutes#GET /users"),
|
|
370
|
+
make_route("POST", "/todos", "TodosRoutes#POST /todos"),
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
let output1 = generate_router(&routes);
|
|
374
|
+
let output2 = generate_router(&routes);
|
|
375
|
+
|
|
376
|
+
assert_eq!(output1[0].content, output2[0].content);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[test]
|
|
380
|
+
fn test_router_uses_auto_generated_header() {
|
|
381
|
+
let routes = vec![make_route("GET", "/health", "HealthRoutes#GET /health")];
|
|
382
|
+
let outputs = generate_router(&routes);
|
|
383
|
+
assert!(outputs[0].content.contains("AUTO-GENERATED"));
|
|
384
|
+
}
|
|
385
|
+
}
|